Drift (Moor) — 什么是它,响应式 ORM 和数据库操作

作者: IT Sectr 发布日期: 2026-03-13 阅读时间: 9 分钟

Drift(原名 Moor)——适用于 Flutter 和 Dart 的响应式 ORM,构建在 SQLite 之上并拥有自己的 DSL 用于查询。与传统 ORM 不同,Drift 在构建时将 Dart 查询编译为 SQL,消除了运行时错误。根据 Drift Docs, 2024Drift 生成的代码比手动 SQL 查询多 40%,但完全消除了手动编写 SQL,用类型安全的 Dart 语法取而代之。

要点

  • Drift — 在构建时将查询编译为 SQL 的响应式 ORM
  • DSL 查询 — 在 Dart 中使用流畅接口,无需编写 SQL 字符串
  • 响应性 — 用于实时 UI 更新的 Stream 和自动更新查询
  • 跨平台 — Android、iOS、Web、macOS、Linux、Windows
  • 迁移 — 自动版本控制和通过 SQL 进行的手动迁移

什么是 Drift?

Drift ——适用于 Dart 和 Flutter 的 ORM,以前称为 Moor。由 Simon Binder 于 2019 年开发,此后经历了多个主要版本。Drift 使用 drift_devbuild_runner 在构建时将 Dart 查询编译为 SQL,提供完全的类型安全并消除运行时的 SQL 语法错误。

与 Floor 不同,Drift 使用自己的 DSL(领域特定语言)来构建查询——开发人员用 Dart 编写,生成器将其转换为 SQL。这使得 IDE 能够检查语法、自动完成表字段并重构数据模型,而无需担心破坏查询。

根据 Drift(2024)的数据,该库被用于 8000 多个 Flutter 项目。它支持所有主流平台:通过 sqflite 支持 Android,通过 sqflite 支持 iOS,通过 sqlite3 WASM 支持 Web,通过 sqlite3 原生驱动支持桌面端。

更名历史:Moor → Drift

Moor 在 2.0 版本(2022 年)更名为 Drift。原因是与其他项目名称冲突以及希望与旧代码保持距离。API 保持兼容:迁移只需将 import 从 moor 改为 drift 并更新依赖项即可。

关键功能

Drift 提供:内置的 Stream 查询,在数据更改时自动更新;支持带回滚的事务;通过 rawQuery 进行的自定义 SQL 查询;用于封装逻辑的 DAO 模式;跨平台迁移以及与 Riverpod 和 BLoC 的集成(通过 drift_riverpod 和 drift_bloc 包)。

Drift 如何工作?

Drift 在编译时使用代码生成。开发人员通过 @DataClass 注解或扩展 Table 的 Dart 类来描述表。生成器创建辅助类:Companion(用于插入/更新时的可空字段)、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 无法覆盖的复杂查询:窗口函数、递归 CTE、特定 SQLite 扩展。

Drift DSL vs SQL:方法比较

Drift 提供两种编写查询的方式:Dart DSL(原生)和 raw SQL(用于复杂情况)。DSL 在 90% 的场景中是首选:它更安全、更易读并支持重构。Raw SQL 仅在 DSL 无法覆盖所需结构时使用。

方面Drift DSLDrift 中的 Raw SQL
类型安全完全(编译时)无(运行时)
自动补全是(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、带 CASE 的批量 UPDATE,以及性能至关重要且 DSL 无法生成最优执行计划的查询。Raw SQL 可以写在 .sql 文件中,通过 drift_dev 支持类型化。

Drift 代码示例

Drift 使用扩展 Table 的类或 @DataClass 注解。下面是使用 DSL、raw SQL 和响应式更新的 Task 模型完整示例。运行 build_runner 后,所有生成的类都已就绪。

定义表和数据库

Tasks 类扩展 Table 并定义列。每个列都是 Column<T> 类型的表达式。参数: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);
}

通过 DSL 进行 CRUD 操作

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 自动生成迁移。如果更改不破坏现有数据(添加可空字段),可以使用带版本检查和执行 ALTER TABLE 的 beforeOpen

手动迁移

对于复杂更改(重命名表、合并数据、更改列类型),Drift 需要手动 SQL 迁移。迁移通过数据库类中的 migrations 参数设置。每个迁移都是带有 from/to 编号和 SQL 查询的对象。

Drift 和测试

Drift 支持通过 NativeDatabase.memory() 在测试模式下运行。内存数据库在每个测试前从头创建,测试后销毁。对于模拟,使用带有 mocked QueryExecutor 的 mocktail 包。Drift 还提供 DatabaseTestHelper,用于带有迁移和查询检查的集成测试。

Drift 中的 DAO 模式

Drift 通过带有 @DriftAccessor 注解的抽象类支持 DAO(数据访问对象)。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 在 @Query 注解中使用 SQL 字符串。Drift 还支持更多平台(包括 Web),并具有通过 Stream 的内置响应性,而在 Floor 中需要手动声明 Stream。

Drift 是否支持不丢失数据的迁移?

是的,Drift 支持保留数据的迁移。要添加列,请在 Migration 中使用 addColumn。对于复杂转换(重命名、合并),在迁移中编写 raw SQL。如果未指定迁移,Drift 会在模式不匹配时重新创建数据库并丢失数据。

是否可以在没有 build_runner 的情况下使用 Drift?

Drift 需要通过 build_runner 和 drift_dev 进行代码生成。没有生成器就无法创建类型化查询。然而,对于小型项目,Drift 支持 sqlparser——手动编写 SQL 文件并自动类型化,但这仍然需要生成阶段。

如何将 Drift 与 Riverpod 集成?

要与 Riverpod 集成,请使用 drift_riverpod 包。它为 Database、DAO 和 Stream 查询提供 provider。示例:final tasksProvider = databaseProvider.select((db) => db.select(db.tasks).watch())——数据更改时 UI 会自动重建。

Drift 是否支持 SQLite 加密?

Drift 没有内置加密,但支持连接带有 SEE(SQLite 加密扩展)的自定义 sqlite3 库。对于移动平台,使用 sqflite_sqlcipher 作为 QueryExecutor——Drift 通过抽象 QueryExecutor 与任何 SQLite 实现兼容。

总结

  • Drift — 用于 Flutter 和 Dart 的响应式 ORM,在构建时将查询编译为 SQL
  • DSL 语法 — 具有完全类型安全和 IDE 自动补全的 Dart 查询
  • 响应性 — 用于自动 UI 更新的 Stream 和自动更新查询
  • 跨平台 — Android、iOS、Web (WASM)、macOS、Linux、Windows
  • 迁移 — 自动用于简单更改,手动 SQL 用于复杂更改
  • 生态系统 — 与 Riverpod (drift_riverpod) 和 BLoC (drift_bloc) 集成
  • 建议 — 对于重视响应性、类型安全和支持所有 Flutter 平台的项目,选择 Drift

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读