Drift (Moor) — какво е, реактивна ORM и работа с бази данни

Автор: IT Sectr Публикувано: 2026-03-13 Време за четене: 9 мин

Drift (преди известен като Moor) — реактивна ORM за Flutter и Dart, изградена върху SQLite със собствен DSL за заявки. За разлика от традиционните ORM, Drift компилира Dart заявките в SQL по време на компилация, елиминирайки runtime грешките. По данни на Drift Docs, 2024, Drift генерира до 40% повече код отколкото ръчните SQL заявки, но напълно изключва ръчното писане на SQL, заменяйки го с типобезопасен Dart синтаксис.

Основни неща

  • Drift — реактивна ORM с компилация на заявките в SQL по време на компилация
  • DSL заявки — fluent интерфейс на Dart без писане на SQL низове
  • Реактивност — Stream и auto-updating заявки за обновяване на UI в реално време
  • Крос-платформеност — Android, iOS, Web, macOS, Linux, Windows
  • Миграции — автоматично версиониране и ръчни миграции чрез SQL

Какво е Drift?

Drift — ORM за Dart и Flutter, известна преди като Moor. Разработена от Саймън Биндер през 2019 г. и оттогава е преминала през няколко мажорни версии. Drift компилира Dart заявките в SQL по време на компилация с помощта на drift_dev и build_runner, което дава пълна типобезопасност и изключва синтактичните грешки в SQL по време на изпълнение.

За разлика от Floor, Drift използва собствен DSL (Domain-Specific Language) за изграждане на заявки — разработчикът пише на Dart, а генераторът транслира това в SQL. Това позволява на IDE да проверява синтаксиса, да допълва автоматично полетата на таблиците и да рефакторира модела на данните без страх да счупи заявка.

По данни на Drift (2024), библиотеката се използва в повече от 8000 Flutter проекта. Тя поддържа всички популярни платформи: Android чрез sqflite, iOS чрез sqflite, web чрез sqlite3 WASM, desktop чрез нативния драйвер на sqlite3.

История на преименуването: Moor → Drift

Moor беше преименуван на Drift във версия 2.0 (2022). Причината — конфликт на имена с други проекти и желанието да се дистанцират от стария код. API остава съвместимо: за миграция е достатъчно да замените import от moor на drift и да обновите зависимостите.

Ключови възможности

Drift предоставя: вградени Stream заявки с автоматично обновяване при промяна на данните, поддръжка на транзакции с отмяна (rollback), кастомни SQL заявки чрез rawQuery, DAO модела за капсулиране на логиката, крос-платформени миграции и интеграция с Riverpod и BLoC чрез пакетите drift_riverpod и drift_bloc.

Как работи Drift?

Drift използва генериране на код по време на компилация. Разработчикът описва таблиците чрез @DataClass анотации или Dart класове, които разширяват Table. Генераторът създава спомагателните класове: Companion (за nullable полета при вмъкване/обновяване), DriftDatabase (точка на влизане) и DAO реализации.

Drift не изпълнява SQLite заявките директно. Вместо това разработчикът пише на 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 DSL срещу SQL: сравнение на подходите

Drift предлага два начина за писане на заявки: Dart DSL (нативен) и raw SQL (за сложни случаи). DSL е за предпочитане за 90% от сценариите: той е по-безопасен, по-четим и поддържа рефакторинг. Raw SQL се използва само когато DSL не покрива търсената конструкция.

АспектDrift DSLRaw SQL в Drift
ТипобезопасностПълна (компилация)Не (runtime)
АвтодопълванеДа (IDE)Само в sql файлове
РефакторингАвтоматиченРъчно търсене по редове
Сложни JOINПоддържат сеПълна свобода
Прозоречни функцииОграниченоПълна поддръжка
РеактивностВградена (Stream)Чрез .watch()

Кога да използвате DSL

Drift DSL — основният начин на работа. Той покрива SELECT, INSERT, UPDATE, DELETE, WHERE, ORDER BY, LIMIT, JOIN и групирането. За всички типични CRUD заявки използвайте DSL: той е по-кратък, по-безопасен и автоматично обновява Stream при промени.

Кога да използвате raw SQL

Raw SQL в Drift е нужен за: кастомни SQLite функции (FTS5, JSON1), сложни подзаявки с EXISTS, INSERT OR REPLACE, масови UPDATE с CASE, както и за заявки, където производителността е критична и DSL не генерира оптимален план за изпълнение. Raw SQL може да се пише в .sql файлове с поддръжка на типизация чрез drift_dev.

Примери за код с Drift

Drift използва класове, които разширяват Table, или анотацията @DataClass. По-долу — пълен пример за модела Task със заявки чрез DSL, raw SQL и реактивно обновяване. След компилация пуснете build_runner и всички генерирани класове са готови.

Дефиниране на таблицата и базата данни

Класът Tasks разширява Table и дефинира колоните. Всяка колона е израз от тип Column. Параметрите: withDefault() задава стойност по подразбиране, autoIncrement() — автоинкремент. Базата данни е абстрактен клас, който разширява $DriftDatabase.

dart
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);
}

CRUD операции чрез DSL

Drift генерира методите into(tasks).insert(), select(tasks), update(tasks) и delete(tasks) за таблиците. Всички операции връщат Future — работата със SQLite е асинхронна. За проследяване на промените използвайте .watch() вместо .get().

dart
// Запис
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);
});

Raw SQL с типизиран резултат

За сложни заявки Drift позволява да пишете raw SQL, като същевременно запазва типизацията. Методът customSelect приема низ за заявка и връща типизиран резултат чрез генератора на код. Този подход съчетава гъвкавостта на SQL с безопасността на типовете в Drift.

dart
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

Drift поддържа както автоматични миграции (за прости промени), така и ръчни (за сложни трансформации). Версията на базата се задава в конструктора на AppDatabase. При несъвпадение на версията Drift прилага последователно всички незатворени миграции.

Автоматични миграции

За добавяне на колона със стойност по подразбиране Drift може да генерира миграцията автоматично чрез MigrationStrategy. Ако промяната не нарушава съществуващите данни (добавяне на nullable поле), можете да използвате beforeOpen с проверка на версията и изпълнение на ALTER TABLE.

Ръчни миграции

За сложни промени (преименуване на таблица, обединяване на данни, промяна на типа на колона) Drift изисква ръчна SQL миграция. Миграциите се задават чрез параметъра migrations в класа на базата данни. Всяка миграция е обект с номера from/to и SQL заявки.

Drift и тестване

Drift поддържа стартиране в тестов режим чрез NativeDatabase.memory(). Базата в паметта се създава от нулата преди всеки тест и се унищожава след него. За мокиране използвайте пакета mocktail със замокнат QueryExecutor. Drift също предоставя DatabaseTestHelper за интеграционни тестове с проверка на миграциите и заявките.

DAO модел в Drift

Drift поддържа DAO (Data Access Object) чрез абстрактни класове с анотация @DriftAccessor. DAO капсулира заявките към една или няколко таблици и може да бъде тестван отделно от базата данни. За разлика от директните заявки през Database, DAO позволява преизползване на логиката на заявките между различни части на приложението и опростява модулното тестване.

dart
@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 се различава от Floor?

Drift използва собствен DSL вместо SQL низове, което дава пълна типобезопасност и автоматично допълване в IDE. Floor използва SQL низове в анотацията @Query. Drift също поддържа повече платформи (включително web) и има вградена реактивност чрез Stream, докато при Floor Stream трябва да се декларира ръчно.

Поддържа ли Drift миграции без загуба на данни?

Да, Drift поддържа миграции със запазване на данните. За добавяне на колони използвайте addColumn в Migration. За сложни трансформации (преименуване, обединяване) пишете raw SQL в рамките на миграцията. Ако миграция не е зададена, Drift пресъздава базата със загуба на данни при несъвпадение на схемата.

Може ли да се използва Drift без build_runner?

Drift изисква генериране на код чрез build_runner и drift_dev. Без генериране е невъзможно да се създадат типизирани заявки. Въпреки това за малки проекти Drift поддържа sqlparser — ръчно писане на SQL файлове с автоматична типизация, но това все пак изисква етап на генериране.

Как да интегрирате Drift с Riverpod?

За интеграция с Riverpod използвайте пакета drift_riverpod. Той предоставя провайдери за Database, DAO и Stream заявки. Пример: final tasksProvider = databaseProvider.select((db) => db.select(db.tasks).watch()) — UI автоматично се преизгражда при промяна на данните.

Поддържа ли Drift криптиране на SQLite?

Drift няма вградено криптиране, но поддържа свързване на кастомни sqlite3 библиотеки със SEE (SQLite Encryption Extension). За мобилни платформи използвайте sqflite_sqlcipher като QueryExecutor — Drift работи с всяка реализация на SQLite чрез абстрактния QueryExecutor.

Изводи

  • Drift — реактивна ORM за Flutter и Dart с компилация на заявките в SQL по време на компилация
  • DSL синтаксис — Dart заявки с пълна типобезопасност и автоматично допълване в IDE
  • Реактивност — Stream и auto-updating заявки за автоматично обновяване на UI
  • Крос-платформеност — Android, iOS, Web (WASM), macOS, Linux, Windows
  • Миграции — автоматични за прости промени и ръчни SQL за сложни
  • Екосистема — интеграция с Riverpod (drift_riverpod) и BLoC (drift_bloc)
  • Препоръка — избирайте Drift за проекти, където са важни реактивността, типобезопасността и поддръжката на всички платформи на Flutter

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също