Drift(原名 Moor)——适用于 Flutter 和 Dart 的响应式 ORM,构建在 SQLite 之上并拥有自己的 DSL 用于查询。与传统 ORM 不同,Drift 在构建时将 Dart 查询编译为 SQL,消除了运行时错误。根据 Drift Docs, 2024,Drift 生成的代码比手动 SQL 查询多 40%,但完全消除了手动编写 SQL,用类型安全的 Dart 语法取而代之。
要点
Drift ——适用于 Dart 和 Flutter 的 ORM,以前称为 Moor。由 Simon Binder 于 2019 年开发,此后经历了多个主要版本。Drift 使用 drift_dev 和 build_runner 在构建时将 Dart 查询编译为 SQL,提供完全的类型安全并消除运行时的 SQL 语法错误。
与 Floor 不同,Drift 使用自己的 DSL(领域特定语言)来构建查询——开发人员用 Dart 编写,生成器将其转换为 SQL。这使得 IDE 能够检查语法、自动完成表字段并重构数据模型,而无需担心破坏查询。
根据 Drift(2024)的数据,该库被用于 8000 多个 Flutter 项目。它支持所有主流平台:通过 sqflite 支持 Android,通过 sqflite 支持 iOS,通过 sqlite3 WASM 支持 Web,通过 sqlite3 原生驱动支持桌面端。
Moor 在 2.0 版本(2022 年)更名为 Drift。原因是与其他项目名称冲突以及希望与旧代码保持距离。API 保持兼容:迁移只需将 import 从 moor 改为 drift 并更新依赖项即可。
Drift 提供:内置的 Stream 查询,在数据更改时自动更新;支持带回滚的事务;通过 rawQuery 进行的自定义 SQL 查询;用于封装逻辑的 DAO 模式;跨平台迁移以及与 Riverpod 和 BLoC 的集成(通过 drift_riverpod 和 drift_bloc 包)。
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 提供两种编写查询的方式:Dart DSL(原生)和 raw SQL(用于复杂情况)。DSL 在 90% 的场景中是首选:它更安全、更易读并支持重构。Raw SQL 仅在 DSL 无法覆盖所需结构时使用。
| 方面 | Drift DSL | Drift 中的 Raw SQL |
|---|---|---|
| 类型安全 | 完全(编译时) | 无(运行时) |
| 自动补全 | 是(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、带 CASE 的批量 UPDATE,以及性能至关重要且 DSL 无法生成最优执行计划的查询。Raw SQL 可以写在 .sql 文件中,通过 drift_dev 支持类型化。
Drift 使用扩展 Table 的类或 @DataClass 注解。下面是使用 DSL、raw SQL 和响应式更新的 Task 模型完整示例。运行 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 允许在保持类型化的同时编写 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 会按顺序应用所有未完成的迁移。
要添加带有默认值的列,Drift 可以通过 MigrationStrategy 自动生成迁移。如果更改不破坏现有数据(添加可空字段),可以使用带版本检查和执行 ALTER TABLE 的 beforeOpen。
对于复杂更改(重命名表、合并数据、更改列类型),Drift 需要手动 SQL 迁移。迁移通过数据库类中的 migrations 参数设置。每个迁移都是带有 from/to 编号和 SQL 查询的对象。
Drift 支持通过 NativeDatabase.memory() 在测试模式下运行。内存数据库在每个测试前从头创建,测试后销毁。对于模拟,使用带有 mocked QueryExecutor 的 mocktail 包。Drift 还提供 DatabaseTestHelper,用于带有迁移和查询检查的集成测试。
Drift 通过带有 @DriftAccessor 注解的抽象类支持 DAO(数据访问对象)。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 在 @Query 注解中使用 SQL 字符串。Drift 还支持更多平台(包括 Web),并具有通过 Stream 的内置响应性,而在 Floor 中需要手动声明 Stream。
是的,Drift 支持保留数据的迁移。要添加列,请在 Migration 中使用 addColumn。对于复杂转换(重命名、合并),在迁移中编写 raw SQL。如果未指定迁移,Drift 会在模式不匹配时重新创建数据库并丢失数据。
Drift 需要通过 build_runner 和 drift_dev 进行代码生成。没有生成器就无法创建类型化查询。然而,对于小型项目,Drift 支持 sqlparser——手动编写 SQL 文件并自动类型化,但这仍然需要生成阶段。
要与 Riverpod 集成,请使用 drift_riverpod 包。它为 Database、DAO 和 Stream 查询提供 provider。示例:final tasksProvider = databaseProvider.select((db) => db.select(db.tasks).watch())——数据更改时 UI 会自动重建。
Drift 没有内置加密,但支持连接带有 SEE(SQLite 加密扩展)的自定义 sqlite3 库。对于移动平台,使用 sqflite_sqlcipher 作为 QueryExecutor——Drift 通过抽象 QueryExecutor 与任何 SQLite 实现兼容。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。