skip to content

In Drift for Flutter, what do you write by hand for a table, a database and a DAO, and what does the generator produce?

level: juniorimportance: should knowfreq 45%

answer

  1. you declare, the builder writes the rest
  2. Table subclass with column getters
  3. @DriftDatabase(tables:, daos:)
  4. Habit row class, HabitsCompanion for writes
  5. @DriftAccessor plus DatabaseAccessor mixin

basics

~20 s

You write Table subclasses with column getters, a database class annotated @DriftDatabase that extends the generated _$AppDatabase and declares schemaVersion, and optional @DriftAccessor DAOs. Drift's generator writes typed row classes, companions, table getters and DAO mixins.

solid answer

~30 s

By hand you declare each table as a class extending `Table`, with getters such as `integer().autoIncrement()()` or `text().withLength(max: 40)()`. A database class annotated `@DriftDatabase(tables: [Habits, CheckIns], daos: [HabitsDao])` extends the generated `_$AppDatabase`, takes a `QueryExecutor` in its constructor and overrides `schemaVersion`. The generator then writes an immutable row class per table (`Habits` becomes `Habit`), a `HabitsCompanion` used for inserts and partial updates, one table getter per table on the database, and a `_$HabitsDaoMixin` for each `@DriftAccessor` class, which extends `DatabaseAccessor<AppDatabase>` and shares the database's connection. The generated code lives in a `part` file, so nothing is reflected at runtime.

code

dart · 30 lines
dart
import 'package:drift/drift.dart';

part 'database.g.dart';

class Habits extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text().withLength(min: 1, max: 40)();
}

class CheckIns extends Table {
  IntColumn get id => integer().autoIncrement()();
  IntColumn get habit => integer().references(Habits, #id)();
  DateTimeColumn get day => dateTime()();
}

@DriftAccessor(tables: [Habits, CheckIns])
class HabitsDao extends DatabaseAccessor<AppDatabase> with _$HabitsDaoMixin {
  HabitsDao(super.attachedDatabase);

  Future<int> addHabit(String name) =>
      into(habits).insert(HabitsCompanion.insert(name: name));
}

@DriftDatabase(tables: [Habits, CheckIns], daos: [HabitsDao])
class AppDatabase extends _$AppDatabase {
  AppDatabase(super.e);

  @override
  int get schemaVersion => 1;
}

go deeper

for a junior

Recall the three things you write (tables, the annotated database, optional DAOs) and the two things you get back per table: a row class and a companion.

for a middle

Explain why companions use Value<T>, how row class names are derived, and that a DAO shares the database's executor and streams.

for a senior

Show how you split queries into DAOs by feature, and remind the interviewer that any table change also needs a schemaVersion bump and a tested migration.

for a principal

Weigh generated typed schemas against raw SQL access for a large team: compile-time safety and review diffs versus a build step and generated files to keep in sync.

## What Drift is and why it generates code **Drift** (drift 2.35 at the time of writing) is a typed persistence library for Dart and Flutter built on SQLite. Its central idea is that you describe your schema in ordinary Dart and a **code generator** (`drift_dev`) turns that description into strongly typed classes. Because the generated code is written to a `part` file (`database.g.dart`) at build time, a Flutter release build never inspects your classes at runtime, and a column you misspell is a compile error rather than a crash on a user's phone. In a habit tracker, the schema might be two tables: the habits a user tracks and the check-ins that record each day a habit was done. ## What you write by hand 1. **Table classes.** Each table is a class that extends `Table`. Each column is a getter that builds a column with a fluent API and ends with an extra pair of parentheses: `IntColumn get id => integer().autoIncrement()();`. Modifiers include `nullable()`, `withLength(min:, max:)`, `withDefault(...)`, `clientDefault(...)` and `references(Habits, #id)` for a foreign key. 2. **The database class.** A class annotated `@DriftDatabase(tables: [...])` that extends `_$AppDatabase` (the generated superclass, named after your class). It must provide a constructor that passes a `QueryExecutor` to `super`, and it must override `int get schemaVersion`, which has to be positive; drift throws a `StateError` on open if it is zero or less. 3. **Optional DAOs.** A class annotated `@DriftAccessor(tables: [...])` that extends `DatabaseAccessor<AppDatabase>` and mixes in the generated `_$HabitsDaoMixin`. Its constructor forwards `attachedDatabase` to `super`. You register it with `daos: [HabitsDao]` on `@DriftDatabase`. ## What the generator produces | You declare | Drift generates | Used for | |---|---|---| | `class Habits extends Table` | row class `Habit` | typed results from `get()` and `watch()` | | `class Habits extends Table` | `HabitsCompanion` | inserts and partial updates | | `@DriftDatabase(tables: [Habits])` | `_$AppDatabase` with a `habits` getter | building queries | | `@DriftAccessor(tables: [Habits])` | `_$HabitsDaoMixin` | table getters inside the DAO | | `daos: [HabitsDao]` | a `habitsDao` getter on the database | reaching the DAO | A few naming rules are worth knowing: - The row class name is derived by stripping a plural: `Habits` becomes `Habit`, `Categories` becomes `Category`, and a name ending in `ss` or `us` gets a `Data` suffix instead. `@DataClassName('...')` overrides the guess. - Column and table names are re-cased to `snake_case` in SQL by default, so a `createdAt` getter becomes a `created_at` column. - A **companion** wraps every field in `Value<T>`, so it can express "leave this column alone" (`Value.absent()`). Its `insert` constructor makes non-nullable columns without defaults required and the auto-increment id optional. ## Why DAOs exist A DAO (data access object) is only an organisational tool. It does **not** open a second connection: it holds a reference to the attached database and runs every query through it, so its writes update the same stream queries as the database's own. Teams use DAOs to keep, say, all streak queries in a `HabitsDao` instead of one very large database class. A DAO lists only the tables it needs, and the generated mixin gives it getters for exactly those. ## Using the generated code Once the generator has run, everyday code reads naturally and is fully typed: - `into(habits).insert(HabitsCompanion.insert(name: 'Read 10 pages'))` returns the new row id as a `Future<int>`. - `select(habits).get()` returns `Future<List<Habit>>`, and `watch()` returns a stream of the same type. - `(update(habits)..where((h) => h.id.equals(id))).write(HabitsCompanion(name: Value('Read 20 pages')))` changes only the named column, because every other companion field is absent. - `(delete(checkIns)..where((c) => c.habit.equals(id))).go()` removes a habit's check-ins. Drift also generates a **manager** API (`managers.habits.filter(...)`) that reads more like an ORM; it produces the same typed rows and is optional. Whichever style a team picks, the compiler checks every column name and type, so renaming a getter surfaces every affected query as a build error instead of a runtime SQL failure. ## Common misunderstandings - Drift does not map rows by reflection; `dart:mirrors` is unavailable in Flutter anyway, which is why the generator exists. - A `Table` subclass is a schema description, never a container for row data; rows are instances of the generated row class. - You do not insert a row class with a made-up id; you insert a companion and let SQLite assign the key. - After changing a table you must regenerate code, and for an existing install you must also bump `schemaVersion` and write a migration.

  • Why does a Drift companion wrap each field in Value<T> instead of using nullable fields?
    Because null is a legitimate column value. `Value.absent()` means "do not touch this column", while `Value(null)` means "write NULL". That distinction lets one `HabitsCompanion` serve inserts, where absent columns take their defaults, and partial updates, where absent columns keep their current values.
  • Does a Drift DAO registered with @DriftDatabase(daos: [...]) have its own connection or transaction scope?
    No. It extends `DatabaseAccessor` and runs everything through `attachedDatabase`, so it shares the same executor, transactions and stream-query store. A write made through the DAO re-runs `watch()` queries created on the database and vice versa.

saying these in an interview costs you the question

  • Drift maps rows to classes by runtime reflection.
  • A Table subclass instance holds one row's values.
  • You insert the generated row class and invent the id yourself.
  • Each DAO opens its own separate SQLite connection.
  • schemaVersion can start at 0 for a brand-new database.