Generated row classes¶
For each table you define, drift generates two associated classes:
- A row class: This class represents a full row of the table. Drift automatically returns instances of these classes for queries on tables, allowing you to access rows with type safety.
- A companion class: While row classes represent a full row as it appears in the database, sometimes you also need a partial row (e.g. for updates or inserts which don't have values for auto-incrementing primary keys yet). For this, drift generates a companion class primarily used for inserts and updates.
Drift's row classes come with built-in equality, hashing, and basic serialization support. They also include a copyWith
method for easy modification.
Example¶
A simple table to store usernames shows how the generated row and companion classes behave:
class Users extends Table {
late final id = integer().autoIncrement()();
late final username = text()();
}
For this table, drift generates a User
class which roughly looks like this (with a few
additional convenience methods now shown here):
// Simplified version of the row class generated by drift:
class User {
final int id;
final String username;
const User({required this.id, required this.username});
@override
String toString() {
// ...
}
@override
int get hashCode => Object.hash(id, username);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is User && other.id == this.id && other.username == this.username);
}
Note that User.id
is a non-nullable field, reflecting that the column is also non-nullable
in the database.
When you're inserting a new User
however, there's no value you could provide to id
because the
actual value is determined by the database. For this reason, drift also has companion classes to
represent partial rows:
class UsersCompanion extends UpdateCompanion<User> {
final Value<int> id;
final Value<String> username;
const UsersCompanion({
this.id = const Value.absent(),
this.username = const Value.absent(),
});
UsersCompanion.insert({
this.id = const Value.absent(),
required String username,
}) : username = Value(username);
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
// ...
}
}
In a companion, all fields are wrapped in a Value
class that represents whether a column is present
or not.
Using row classes¶
With the two generated classes, database rows can be created and read in a type-safe and structured way:
await db.managers.users.create((row) => row(username: 'firstuser'));
Note that the manager API doesn't use companions and instead has required
columns as
required
parameters on the function used to create new rows.
The special UsersCompanion.insert
constructor has required parameters for all columns that don't
have a default in the database:
await db.users.insertOne(UsersCompanion.insert(username: 'firstuser'));
Dataclass Name¶
By default, the dataclass name is derived from the table name.
- If the name ends in
s
, the dataclass name will be the name withs
removed.- Example:
Users
->User
- Example:
- Otherwise, the dataclass name will be the name with
Data
appended.- Example:
UserInfo
->UserInfoData
- Example:
To make drift use a different name, use the @DataClassName
annotation:
@DataClassName('Category')
class Categories extends Table {
late final id = integer().autoIncrement()();
late final title = text()();
}
void readCategories(Database db) async {
// Thanks to @DataClassName, the generated class is `Category` instead of
// `Categorie`.
final List<Category> categories = await db.categories.all().get();
print('Current categories: $categories');
}
Companions and Value
¶
Companion classes are used to represent a partial row where not all columns are present. This class is introduced for two reasons:
- Dart has a null-safe type system: If we only had a single row class, all values generated by the
database (like
autoIncrement()
columns) would have to be nullable when creating new rows. That would make the class unfit for queries though. - We need a distinction between
NULL
(in SQL) and absent columns: For updates, setting a column toNULL
is not the same thing as not changing it at all (and thus keeping the previous value in the database). There's only onenull
in Dart though, so we need a different structure.
To solve this problem, companions represent partial rows by using Drift's Value
class.
Value
s store a value (which can be nullable) or explicitly indicate that a value is absent:
await db.users.insertOne(UsersCompanion(
id: Value.absent(), // (1)!
username: Value('user'), // (2)!
));
await (db.update(db.users)..where((tbl) => tbl.id.equals(1)))
.write(UsersCompanion(username: Value("Updated name")));
- Since the
id
isautoIncrement()
, the database will pick a value for us and no value is provided explicitly. SinceValue.absent()
is also the default, this could be omitted. - To simplify the common scenarios of inserts, drift generates a
.insert()
constructor on companions that avoidsValue
wrappers where they are not required. This insert could be written asUsersCompanion.insert(username: 'user')
Updating with SQL expressions¶
Companions also provide a .custom
method used when mixing values and SQL expressions for
updates or deletes.
For instance, this update statement changes all the names of all rows in the users
table
to be lower-case:
Custom dataclass¶
The generated dataclass works well for most cases, but you might want to use your own class to
represent rows.
This can be useful when these classes should extend, implement or mix-in other classes, or if you
want to apply other builders like json_serializable
too.
Row Class
In the documentation, we use the terms row class and dataclass interchangeably. Both refer to a class that represents a row of a database table.
To use a custom row class, simply annotate your table definition with @UseRowClass
.
@UseRowClass(User)
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
DateTimeColumn get birthday => dateTime()();
}
class User {
final int id;
final String name;
final DateTime birthday;
User({required this.id, required this.name, required this.birthday});
}
In the default configuration, row classes must adhere to the following requirements:
- They must have an unnamed constructor.
- Each constructor argument must have the name of a drift column (matching the getter name in the table definition).
- The type of a constructor argument must be equal to the type of a column, including nullability and applied type converters.
On the other hand, note that:
- A custom row class can have additional fields and constructor arguments, as long as they're not required. Drift will ignore those parameters when mapping a database row.
- A table can have additional columns not reflected in a custom data class. Drift will simply not load those columns when mapping a row.
Using another constructor¶
By default, drift will use the default, unnamed constructor to map a row to the class.
If you want to use another constructor, set the constructor
parameter on the
@UseRowClass
annotation:
@UseRowClass(User, constructor: 'fromDb')
class Users extends Table {
// ...
}
class User {
final int id;
final String name;
final DateTime birthday;
User.fromDb({required this.id, required this.name, required this.birthday});
}
Custom companions¶
In most cases, generated companion classes are the right tool for updates and inserts.
If you prefer to use your custom row class for inserts, just make it implement Insertable<T>
, where
T
is the tye of your row class itself.
For instance, the previous class could be changed like this:
class User implements Insertable<User> {
final int id;
final String name;
final DateTime birthDate;
User({required this.id, required this.name, required this.birthDate});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
return UsersCompanion(
id: Value(id),
name: Value(name),
birthDate: Value(birthDate),
).toColumns(nullToAbsent);
}
}
Static and asynchronous factories¶
Starting with drift 2.0, the custom constructor set with the constructor
parameter on the @UseRowClass
annotation may also refer to a static method
defined on the class to load.
That method must either return the row class or a Future
of that type.
Unlike a named constructor or a factory, this can be useful in case the mapping
from SQL to Dart needs to be asynchronous:
class User {
// ...
static Future<User> load(int id, String name, DateTime birthday) async {
// ...
}
}
Custom dataclass in drift files¶
To use existing row classes in drift files, use the WITH
keyword at the end of the
table declaration. Also, don't forget to import the Dart file declaring the row
class into the drift file.
import 'user.dart'; -- or what the Dart file is called
CREATE TABLE users(
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
birth_date DATETIME NOT NULL
) WITH User;
This feature is also supported for views. Simply add the WITH ClassName
syntax
after the name of the view in the CREATE VIEW
statement:
You can make drift target named constructors too:
CREATE TABLE users(
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
birth_date DATETIME NOT NULL
) WITH User.myNamedConstructor;
Custom dataclass for queries¶
Existing row classes may also be applied to named queries defined in a .drift
file.
They have a similar syntax, adding the WITH
keyword after the name of the query:
import 'my_existing_class.dart';
/*
Assuming a Dart class like the following:
class MyExistingClass {
final String name;
final double avgAge;
MyExistingClass(this.name, this.avgAge);
}
*/
myQuery WITH MyExistingClass: SELECT name, AVG(age) AS avg_age FROM entries GROUP BY category;
Again, you can also target a named constructor:
/*
class MyExistingClass {
final String name;
final double avgAge;
MyExistingClass.fromSql(this.name, this.avgAge);
}
*/
myQuery WITH MyExistingClass.fromSql: SELECT name, AVG(age) AS avg_age FROM entries GROUP BY category;
For your convenience, drift is using different generation strategies even for queries without an existing row class. It is helpful to enumerate them because they affect the allowed type for fields in existing types as well.
- Nested tables: When the
SELECT table.**
syntax is used in a query, drift will pack columns fromtable
into a nested object instead of generating fields for every column. - Nested list results: The
LIST()
macro can be used to expose results of a subquery as a list. - Single-table results: When a select statement reads all columns from a table (and no additional columns),
like in
SELECT * FROM table
, drift will use the data class of the table instead of generating a new one. - Single-column results: When a select statement only has a single column, but doesn't represent a full table, drift will not generate a full result class for it. Instead, the value is returned directly.
- Other results: This is probably the most common case in practice: All result sets that don't fall into one of the existing categorizations are said to have a normal result set.
Depending on what kind of result set your query has, you can use different fields for the existing Dart class:
- For a nested table selected with
**
, your field needs to store a structure compatible with the result set the nested column points to. Formy_table.**
, that field could either be the generated row class forMyTable
or a custom class as described by rule 3. - For nested list results, you have to use a
List<T>
. TheT
has to be compatible with the inner result set of theLIST()
as described by these rules. - For a single-table result, you can use the table class, regardless of whether the table uses an existing table class or whether it is generated by drift. If that matches the intention of your query better, you may also choose to use a different class for nested tables, provided that all fields of that class can be mapped to a column as described by these rules.
- For a single-column result, you may either use that type directly or a single-field class wrapping it.
- For normal results, each field of your result class must match the name of a column in that result set.
The type of the column must be assignable to the field in your class, drift will also take type converters
into account here.
- For a
**
column in a normal result set, see rule 1. - For a
LIST()
column in a normal result set, see rule 2.
- For a
While these rules may seem complicated when entirely spelled out, they are designed to match the intuitive mapping one would expect. Consider this example:
CREATE TABLE employees(
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
supervisor INTEGER REFERENCES employees(id)
);
employeeWithStaff WITH EmployeeWithStaff: SELECT
self.**,
supervisor.name,
LIST(SELECT * FROM employees WHERE supervisor = self.id) AS staff
FROM employees AS self
INNER JOIN employees supervisor ON supervisor.id = self.supervisor
WHERE id = ?;
Using the rules as defined above, let's see how the EmployeeWithStaff
class can look like:
The outermost result set has three columns: A **
column, a simple expression column and a LIST
column. That means that this query falls under rule 5.
This essentially means that we get to write a class.
For the simple column that references the name
column, we know it must be a string because the
column was defined with TEXT
.
class EmployeeWithStaff {
final T1 self;
final String supervisor;
final T3 staff;
EmployeeWithStaff(this.self, this.supervisor, this.staff);
}
As self
is a **
column, rule 1 applies. self
references a table, employees
.
By rule 3, this means that T1
can be a Employee
, the row class for the employees
table.
On the other hand, staff
is a LIST()
column and rule 2 applies here. This means that T3
must
be a List<Something>
.
The inner result set of the LIST
references all columns of employees
and nothing more, so rule
3 applies. Thus, we can either use Employee
again, or another custom row class referencing columns
from that table.
The final class can now look like this:
class IdAndName {
final int id;
final String name;
// This class can be used since id and name column are available from the list query.
// We could have also used the `Employee` class or a record like `(int, String)`.
IdAndName(this.id, this.name);
}
class EmployeeWithStaff {
final Employee self;
final String supervisor;
// We could have also picked List<Employee> for this field
final List<IdAndName> staff;
EmployeeWithStaff(this.self, this.supervisor, this.staff);
}
In practice, the rules should be intuitive while also being flexible enough for you to design the result classes the way you like.
If you have questions about existing result classes, or think you have found an edge-case not properly handled, please start a discussion in the drift repository, thanks!
JSON serialization¶
Generated row classes can be converted from and to JSON:
Drift serialization status
Serialization has been added to drift in a very early version with an unfortunate design that
can only cover simple serialization needs. Advanced JSON options that dedicated packages like
json_serializable
, built_value
or freezed
offer are superior to drift's serialization
capabilities.
Drift implementing serialization capabilities violates separation of concerns, and this feature will not be expanded. A better approach is to write your own custom row classes and apply another JSON serialization builder on them.
final User user = User.fromJson({'id': 3, 'username': 'awesomeuser'});
print('Deserialized user: ${user.username}');
Key names¶
By default, drift uses column names in snake_case
as JSON keys:
class Todos extends Table {
late final id = integer().autoIncrement()();
late final title = text()();
late final createdAt = dateTime().withDefault(currentDateAndTime)();
}
Custom json keys¶
To use a custom name for JSON serialization, use the @JsonKey
annotation.
Note that the @JsonKey
class from package:drift
is not same as the @JsonKey
annotation from package:json_annotation
, and the two are not compatible with each other.
Example¶
@JsonKey('created')
late final createdAt = dateTime().withDefault(currentDateAndTime)();
If you prefer to use the actual column name in SQL as the JSON key, set use_sql_column_name_as_json_key
to true
in the build.yaml
file.