Skip to content

Generated row classes

For each table you define, drift generates two associated classes:

  1. 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.
  2. 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'));
final User firstUser = await db.managers.users.limit(1).getSingle();
print("Hello ${firstUser.username}!");
final User firstUser = await (db.users.select()..limit(1)).getSingle();
print("Hello ${firstUser.username}!");

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 with s removed.
    • Example: Users -> User
  • Otherwise, the dataclass name will be the name with Data appended.
    • Example: UserInfo -> UserInfoData

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:

  1. 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.
  2. We need a distinction between NULL (in SQL) and absent columns: For updates, setting a column to NULL is not the same thing as not changing it at all (and thus keeping the previous value in the database). There's only one null in Dart though, so we need a different structure.

To solve this problem, companions represent partial rows by using Drift's Value class. Values 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")));
  1. Since the id is autoIncrement(), the database will pick a value for us and no value is provided explicitly. Since Value.absent() is also the default, this could be omitted.
  2. To simplify the common scenarios of inserts, drift generates a .insert() constructor on companions that avoids Value wrappers where they are not required. This insert could be written as UsersCompanion.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:

await db
    .update(db.users)
    .write(UsersCompanion.custom(username: db.users.username.lower()));

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:

CREATE VIEW my_view WITH ExistingClass AS SELECT ...

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.

  1. Nested tables: When the SELECT table.** syntax is used in a query, drift will pack columns from table into a nested object instead of generating fields for every column.
  2. Nested list results: The LIST() macro can be used to expose results of a subquery as a list.
  3. 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.
  4. 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.
  5. 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:

  1. For a nested table selected with **, your field needs to store a structure compatible with the result set the nested column points to. For my_table.**, that field could either be the generated row class for MyTable or a custom class as described by rule 3.
  2. For nested list results, you have to use a List<T>. The T has to be compatible with the inner result set of the LIST() as described by these rules.
  3. 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.
  4. For a single-column result, you may either use that type directly or a single-field class wrapping it.
  5. 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.

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)();
}
{
  "id": 1,
  "title": "Todo 1",
  "created_at": "2024-02-29T12:00:00Z"
}

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)();
{
  "id": 1,
  "title": "Todo 1",
  "created": "2024-02-29T12:00:00Z"
}

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.

build.yaml
targets:
  $default:
    builders:
      drift_dev:
        options:
          use_sql_column_name_as_json_key : true
For more details on customizing column names in SQL, refer to the column name documentation.