Drift (раніше Moor) — це реактивна ORM для Flutter та Dart, побудована на SQLite зі власним DSL для запитів. На відміну від традиційних ORM, Drift компілює Dart-запити в SQL на етапі збірки, уникаючи помилок часу виконання. За даними Drift Docs, 2024, Drift генерує до 40% більше коду, ніж ручні SQL-запити, але повністю виключає ручне написання SQL, замінюючи його типобезпечним Dart-синтаксисом.
Головне
Drift — це ORM для Dart та Flutter, раніше відомий як Moor. Був розроблений Саймоном Біндером у 2019 році і з того часу пройшов кілька мажорних версій. Drift компілює Dart-запити в SQL на етапі збірки за допомогою drift_dev та build_runner, що забезпечує повну типобезпеку та уникає синтаксичних помилок SQL під час виконання.
На відміну від Floor, Drift використовує власний DSL (мова предметної області) для побудови запитів — розробник пише на 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 (для полів, що можуть бути null під час вставки/оновлення), DriftDatabase (точка входу) та реалізації DAO.
Drift не виконує SQLite-запити безпосередньо. Натомість розробник пише на Dart: select(tasks).where(tasks.priority.greaterThan(3)).build(). Генератор перекладає це в SQL, а під час виконання Drift просто надсилає готовий SQL-запит до SQLite. Це поєднує зручність Dart-синтаксису з продуктивністю нативного SQL.
Drift підтримує два режими запитів: DSL (рекомендований) та сирий SQL. DSL-запити писати безпечніше — компілятор перевіряє назви полів, типи та сумісність. Сирий SQL потрібен для складних запитів, не покритих DSL: віконні функції, рекурсивні CTE, специфічні розширення SQLite.
Drift пропонує два способи написання запитів: Dart DSL (нативний) та сирий SQL (для складних випадків). DSL краще підходить для 90% сценаріїв: він безпечніший, читабельніший та підтримує рефакторинг. Сирий SQL використовується лише коли DSL не покриває необхідної конструкції.
| Аспект | Drift DSL | Сирий SQL в Drift |
|---|---|---|
| Типобезпека | Повна (компіляція) | Немає (виконання) |
| Автодоповнення | Так (IDE) | Тільки в sql-файлах |
| Рефакторинг | Автоматичний | Ручний пошук по рядках |
| Складні JOIN | Підтримуються | Повна свобода |
| Віконні функції | Обмежено | Повна підтримка |
| Реактивність | Вбудована (Stream) | Через .watch() |
Drift DSL — основний спосіб роботи. Він охоплює SELECT, INSERT, UPDATE, DELETE, WHERE, ORDER BY, LIMIT, JOIN та групування. Для всіх типових CRUD-запитів використовуйте DSL: він коротший, безпечніший та автоматично оновлює Stream при змінах.
Сирий SQL в Drift потрібен для: користувацьких функцій SQLite (FTS5, JSON1), складних підзапитів з EXISTS, INSERT OR REPLACE, масових UPDATE з CASE, а також для запитів, де продуктивність критична і DSL не генерує оптимальний план виконання. Сирий SQL можна писати в .sql-файлах з підтримкою типізації через drift_dev.
Drift використовує класи, що розширюють Table, або анотацію @DataClass. Нижче наведено повний приклад моделі Task з запитами через DSL, сирий SQL та реактивним оновленням. Після запуску build_runner, всі згенеровані класи готові.
Клас Tasks розширює Table та визначає стовпці. Кожен стовпець — це вираз типу Column<T>. Параметри: withDefault() задає значення за замовчуванням, autoIncrement() задає автоматичне збільшення. База даних — це абстрактний клас, що розширює $DriftDatabase.
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 дозволяє писати сирий 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 застосовує всі незакриті міграції послідовно.
Для додавання стовпця зі значенням за замовчуванням Drift може автоматично згенерувати міграцію через MigrationStrategy. Якщо зміна не порушує існуючі дані (додавання поля, що може бути null), можна використовувати beforeOpen з перевіркою версії та виконанням ALTER TABLE.
Для складних змін (перейменування таблиці, об'єднання даних, зміна типу стовпця) Drift потребує ручної SQL-міграції. Міграції задаються через параметр migrations в класі бази даних. Кожна міграція — це об'єкт з номерами from/to та SQL-запитами.
Drift підтримує запуск в тестовому режимі через NativeDatabase.memory(). База даних в пам'яті створюється з нуля перед кожним тестом та знищується після. Для мокування використовуйте пакет mocktail з імітованим QueryExecutor. Drift також надає DatabaseTestHelper для інтеграційних тестів з перевіркою міграцій та запитів.
Drift підтримує DAO (Об'єкт доступу до даних) через абстрактні класи з анотацією @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. Для складних трансформацій (перейменування, об'єднання) пишіть сирий 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). Для мобільних платформ використовуйте sqflite_sqlcipher як QueryExecutor — Drift працює з будь-якою реалізацією SQLite через абстрактний QueryExecutor.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також