Drift (ранее Moor) — реактивная ORM для Flutter и Dart, построенная поверх SQLite с собственным DSL для запросов. В отличие от традиционных ORM, Drift компилирует Dart-запросы в SQL на этапе сборки, исключая runtime-ошибки. По данным Drift Docs, 2024, Drift генерирует до 40% больше кода, чем ручные SQL-запросы, но полностью исключает ручное написание SQL, заменяя его типобезопасным Dart-синтаксисом.
Главное
Drift — ORM для Dart и Flutter, ранее известная как Moor. Разработана Симоном Биндером в 2019 году и с тех пор прошла несколько мажорных версий. Drift компилирует Dart-запросы в SQL на этапе сборки с помощью drift_dev и build_runner, что даёт полную типобезопасность и исключает синтаксические ошибки SQL в runtime.
В отличие от Floor, Drift использует собственный DSL (Domain-Specific Language) для построения запросов — разработчик пишет на Dart, а генератор транслирует это в SQL. Это позволяет IDE проверять синтаксис, автодополнять поля таблиц и рефакторить модель данных без страха сломать запрос.
По данным Drift (2024), библиотека используется более чем в 8000 Flutter-проектах. Она поддерживает все популярные платформы: Android через sqflite, iOS через sqflite, web через sqlite3 WASM, десктоп через sqlite3 нативный драйвер.
Moor был переименован в Drift в версии 2.0 (2022). Причина — конфликт названий с другими проектами и желание дистанцироваться от старого кода. API осталось совместимым: для миграции достаточно заменить import с moor на drift и обновить зависимости.
Drift предоставляет: встроенные Stream-запросы с автоматическим обновлением при изменении данных, поддержку транзакций с откатом, кастомные SQL-запросы через rawQuery, DAO-паттерн для инкапсуляции логики, кросс-платформенные миграции и интеграцию с Riverpod и BLoC через пакеты drift_riverpod и drift_bloc.
Drift использует генерацию кода на этапе компиляции. Разработчик описывает таблицы через @DataClass аннотации или Dart-классы, расширяющие Table. Генератор создаёт вспомогательные классы: Companion (для nullable полей при вставке/обновлении), DriftDatabase (точка входа) и DAO-реализации.
SQLite-запросы Drift не выполняет напрямую. Вместо этого разработчик пишет на Dart: select(tasks).where(tasks.priority.greaterThan(3)).build(). Генератор транслирует это в SQL, а во время выполнения Drift просто отправляет готовый SQL-запрос в SQLite. Это сочетает удобство Dart-синтаксиса с производительностью нативного SQL.
Drift поддерживает два режима запросов: DSL (рекомендуемый) и raw SQL. DSL-запросы писать безопаснее — компилятор проверяет имена полей, типы и совместимость. Raw SQL нужен для сложных запросов, не покрываемых DSL: оконные функции, recursive CTE, специфические SQLite-расширения.
Drift предлагает два способа написания запросов: Dart DSL (нативный) и raw SQL (для сложных случаев). DSL предпочтительнее для 90% сценариев: он безопаснее, читаемее и поддерживает рефакторинг. Raw SQL используется только когда DSL не покрывает требуемую конструкцию.
| Аспект | Drift DSL | Raw SQL в Drift |
|---|---|---|
| Типобезопасность | Полная (компиляция) | Нет (runtime) |
| Автодополнение | Да (IDE) | Только в sql-файлах |
| Рефакторинг | Автоматический | Ручной поиск по строкам |
| Сложные JOIN | Поддерживаются | Полная свобода |
| Оконные функции | Ограничено | Полная поддержка |
| Реактивность | Встроенная (Stream) | Через .watch() |
Drift DSL — основной способ работы. Он покрывает SELECT, INSERT, UPDATE, DELETE, WHERE, ORDER BY, LIMIT, JOIN и группировку. Для всех типовых запросов CRUD используйте DSL: он короче, безопаснее и автоматически обновляет Stream при изменениях.
Raw SQL в Drift нужен для: кастомных SQLite-функций (FTS5, JSON1), сложных подзапросов с EXISTS, INSERT OR REPLACE, массовых UPDATE с CASE, а также для запросов, где производительность критична и DSL не генерирует оптимальный план выполнения. Raw SQL можно писать в .sql-файлах с поддержкой типизации через drift_dev.
Drift использует классы, расширяющие Table, или аннотацию @DataClass. Ниже — полный пример модели Task с запросами через DSL, raw SQL и реактивным обновлением. После сборки run build_runner, все сгенерированные классы готовы.
Класс Tasks расширяет Table и определяет столбцы. Каждый столбец — это выражение типа Column
class Tasks extends Table {
IntColumn get id => integer().autoIncrement();
TextColumn get title => text().withDefault(const Constant(''))();
BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
IntColumn get priority => integer().withDefault(const Constant(0))();
}
@DriftDatabase(tables: [Tasks])
class AppDatabase extends $AppDatabase {
AppDatabase(QueryExecutor e) : super(e);
}
Drift генерирует методы into(tasks).insert(), select(tasks), update(tasks) и delete(tasks) для таблиц. Все операции возвращают Future — работа с SQLite асинхронна. Для отслеживания изменений используйте .watch() вместо .get().
// Запись
await into(tasks).insert(TasksCompanion.insert(
title: Value('Купить продукты'),
priority: Value(3),
));
// Чтение с фильтром
final highPriority = await (select(tasks)
..where((t) => t.priority.greaterThan(2))
..orderBy([(t) => OrderingTerm(expression: t.priority, mode: OrderingMode.desc)]))
.get();
// Реактивное наблюдение
select(tasks).watch().listen((tasksList) {
// tasksList — List, обновляется при каждом изменении таблицы
updateUi(tasksList);
});
Для сложных запросов Drift позволяет писать raw SQL, но при этом сохранять типизацию. Метод customSelect принимает строку запроса и возвращает типизированный результат через генератор кода. Такой подход сочетает гибкость SQL с безопасностью типов Drift.
final result = await customSelect(
'SELECT title, COUNT(*) as cnt FROM tasks GROUP BY title',
readsFrom: { tasks },
).get();
for (final row in result) {
print('${row.readString("title")}: ${row.readInt("cnt")}');
}
Drift поддерживает как автоматические миграции (для простых изменений), так и ручные (для сложных трансформаций). Версия базы задаётся в конструкторе AppDatabase. При несовпадении версии Drift применяет все незакрытые миграции последовательно.
Для добавления столбца с default-значением Drift может сгенерировать миграцию автоматически через MigrationStrategy. Если изменение не нарушает существующие данные (добавление nullable-поля), можно использовать beforeOpen с проверкой версии и выполнением ALTER TABLE.
Для сложных изменений (переименование таблицы, объединение данных, изменение типа столбца) Drift требует ручной SQL-миграции. Миграции задаются через параметр migrations в классе базы данных. Каждая миграция — это объект с номерами from/to и SQL-запросами.
Drift поддерживает запуск в тестовом режиме через NativeDatabase.memory(). База в памяти создаётся с нуля перед каждым тестом и уничтожается после. Для мокирования используйте пакет mocktail с замоканным QueryExecutor. Drift также предоставляет DatabaseTestHelper для интеграционных тестов с проверкой миграций и запросов.
Drift поддерживает DAO (Data Access Object) через абстрактные классы с аннотацией @DriftAccessor. DAO инкапсулирует запросы к одной или нескольким таблицам и может быть протестирован отдельно от базы данных. В отличие от прямых запросов через Database, DAO позволяет переиспользовать логику запросов между разными частями приложения и упрощает модульное тестирование.
@DriftDatabase(tables: [Tasks])
class AppDatabase extends $AppDatabase {
AppDatabase(QueryExecutor e) : super(e) {
migrations.add(Migration(1, 2, (m) async {
await m.addColumn(tasks, tasks.dueDate);
await m.createIndex(tasks.idxPriority);
}));
}
}
Часто задаваемые вопросы
Drift использует собственный DSL вместо SQL-строк, что даёт полную типобезопасность и автодополнение в IDE. Floor использует SQL-строки в аннотации @Query. Drift также поддерживает больше платформ (включая web) и имеет встроенную реактивность через Stream, тогда как в Floor Stream нужно объявлять вручную.
Да, Drift поддерживает миграции с сохранением данных. Для добавления столбцов используйте addColumn в Migration. Для сложных трансформаций (переименование, объединение) пишите raw SQL внутри миграции. Если миграция не указана, Drift пересоздаёт базу с потерей данных при несовпадении схемы.
Drift требует кодогенерации через build_runner и drift_dev. Без генерации невозможно создать типизированные запросы. Однако для небольших проектов Drift поддерживает sqlparser — ручное написание SQL-файлов с автоматической типизацией, но это всё равно требует этапа генерации.
Для интеграции с Riverpod используйте пакет drift_riverpod. Он предоставляет провайдеры для Database, DAO и Stream-запросов. Пример: final tasksProvider = databaseProvider.select((db) => db.select(db.tasks).watch()) — UI автоматически перестраивается при изменении данных.
Drift не имеет встроенного шифрования, но поддерживает подключение кастомных sqlite3-библиотек с SEE (SQLite Encryption Extension). Для мобильных платформ используйте sqflite_sqlcipher в качестве QueryExecutor — Drift работает с любой реализацией SQLite через абстрактный QueryExecutor.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также