heroes
, by storing our heroes in a database. This will let us to edit our heroes and keep the changes when we restart the application.heroes
application, we will have one type of entity - a "hero". To create a new entity, we subclass ManagedObject<T>
. Create a new directory lib/model/
and then add a new file to this directory named hero.dart
. Add the following code:Hero
entity. Entities are always made up of two classes._Hero
class is a direct mapping of a database table. This table's name will have the same name as the class: _Hero
. Every property declared in this class will have a corresponding column in this table. Therefore, the _Hero
table will have two columns - id
and name
. The id
column is this table's primary key (a unique identifier for each hero). The name of each hero must be unique.Hero
, is what we work with in our code - when we fetch heroes from a database, they will be instances of Hero
.Hero
class is called the instance type of the entity, because that's what we have instances of. _Hero
is the table definition of the entity. You won't use the table definition for anything other than describing the database table.Hero
all of the properties of _Hero
. An instance type must extend ManagedObject<T>
, where T
is also the table definition. ManagedObject<T>
has behavior for automatically transferring objects to the database and back (among other things).firstName
and lastName
, but it's useful in some places to have a fullName
property. Declaring the fullName
property in the instance type means we have easy access to the full name, but we still store the first and last name individually.channel.dart
, add a new property context
and update prepare()
:ManagedDataModel.fromCurrentMirrorSystem()
will find all of our ManagedObject<T>
subclasses and 'compile' them into a data model. A PostgreSQLPersistentStore
takes database connection information that it will use to connect and send queries to a database. Together, these objects are packaged in a ManagedContext
.HeroesController
to have access to the context.heroes_controller.dart
, add a property and create a new constructor:HeroesController
requires a context in its constructor, we need to pass it the context we created in prepare()
. Update entryPoint
in channel.dart
.HeroesController
constructor, each HeroesController
can execute database queries.HeroesController
currently return heroes from an in-memory list. To fetch data from a database instead of this list, we create and execute instances of Query<T>
in our ManagedContext
.getAllHeroes
in heroes_controller.dart
. Make sure to import your model/hero.dart
file at the top:Query<Hero>
and then execute its fetch()
method. The type argument to Query<T>
is an instance type; it lets the query know which table to fetch rows from and the type of objects that are returned by the query. The context argument tells it which database to fetch it from. The fetch()
execution method returns a List<Hero>
. We write that list to the body of the response.getHeroByID
to fetch a single hero from the database.where
method to filter heroes that have the same id
as the path variable. For example, /heroes/1
will fetch a hero with an id
of 1
. This works because Query.where
adds a SQL WHERE clause to the query. We'd get the following SQL:where
method uses the property selector syntax. This syntax is a closure that takes an argument of the type being queried, and must return a property of that object. This creates an expression object that targets the selected property. By invoking methods like equalTo
on this expression object, a boolean expression is added to the query.fetchOne()
execution method will fetch a single object that fulfills all of the expressions applied to the query. If no database row meets the criteria, null
is returned. Our controller returns a 404 Not Found response in that scenario.ManagedContext.fetchObjectWithID
. When fetching with fetchOne
, make sure the search criteria is guaranteed to be unique.+
button on the bottom left corner of the screen to create a new database server. Choose a version (at least 9.6, but the most recent version is best), name the server whatever you like, and leave the rest of the options unchanged before clicking Create Server
. Once the server has been created, click Start
.psql
command-line tool.psql
should be available in your $PATH
. You can also add Postgres.app
's psql
to your path with the directions here.psql
, create a new database and a user to manage it.migrations/
. Open migrations/00000001_initial.migration.dart
, it should look like this:_Hero
with columns for id
and name
. Before we run it, we should seed the database with some initial heroes. In the seed()
method, add the following:heroes
database with the following command in the project directory:conduit serve
. Then, reload http://conduit-tutorial.conduit.dart.io. Your dashboard of heroes and detail page for each will still show up - but this time, they are sourced from a database.seed()
method, we executed SQL queries instead of using the Conduit ORM. It is very important that you do not use Query<T>
, ManagedObject<T>
or other elements of the Conduit ORM in migration files. Migration files represent an ordered series of historical steps that describe your database schema. If you replay those steps (which is what executing a migration file does), you will end up with the same database schema every time. However, a ManagedObject<T>
subclass changes over time - the definition of a managed object is not historical, it only represents the current point in time. Since a ManagedObject<T>
subclass can change, using one in our migration file would mean that our migration file could change.GET /heroes
. For example, if you entered the text abc
, it'd make this request:heroes_controller.dart
, modify getAllHeroes()
to bind the 'name' query parameter:@Bind.query('name')
annotation will bind the value of the 'name' query parameter if it is included in the request URL. Otherwise, name
will be null.name
is an optional parameter (it is surrounded by curly brackets). An optional parameter in an operation method is also optional in the HTTP request. If we removed the curly brackets from this binding, the 'name' query parameter would become required and the request GET /heroes
without ?name=x
would fail with a 400 Bad Request.int
and DateTime
). For more information, see ResourceControllers.