Floor — 适用于 Flutter 的 ORM(对象关系映射),在 SQLite 之上提供类型化层。与原始 SQLite 查询不同,Floor 从带注解的 Dart 模型生成 DAO 类。根据 Pub.dev,2024 的数据,Floor 被用于超过 3500 个 Flutter 项目中,并与 drift 和 hive 一起成为最流行的本地数据存储 ORM 解决方案之一。
要点
Floor — 是一个基于 SQLite 构建的适用于 Flutter 和 Dart 的 ORM 库。它使用注解来描述实体(Entity)、数据访问对象(DAO)和数据库(Database)。代码生成通过 build_runner 和 floor_generator 完成 — 编译器创建 DAO 实现和数据库管理类。与原始 sqflite 不同,Floor 完全消除了手动将 ResultSet 转换为 Dart 对象的需要,通过类型反射自动将列映射到 Entity 字段。
Floor 遵循 Repository + DAO 模式,Android 开发人员从 Room 中就很熟悉。每个表由一个带有 @Entity 注解的 Dart 类表示,SQL 查询在带有 @dao 注解的接口中分组,数据库在带有 @Database 的抽象类中组装。这种方法严格分离了数据模型和查询逻辑。
根据 Flutter Pulse(2023),Floor 在 28% 需要本地数据库的 Flutter 项目中被选用。选择的主要原因 — 熟悉 SQL(无需学习新的查询语言)和在编译时检查查询。此外,Floor 生成可读的代码,与具有自定义 DSL 的更抽象的 ORM 不同,它易于调试,从而降低了团队中新开发人员的入门门槛。
Floor 由三层组成:Entity(表模型)、DAO(查询接口)和 Database(入口点)。生成器创建 _$_Entity 实现用于字段映射,以及 _$_Dao 用于执行 SQL。当 Entity 或 DAO 发生变化时,只需重新启动 build_runner — 代码将自动更新。对于模式版本之间的迁移,Floor 使用顺序版本号,这保证了在用户设备上更新应用程序时的数据完整性。
Floor 通过 sqflite 包使用 SQLite 用于平台构建,以及 sqlite3 用于桌面和 Web。在应用程序启动时,Floor 创建或打开 SQLite 文件,应用迁移并准备 DAO 方法以执行查询。所有操作通过 Future 和 Stream 以异步模式执行。
Floor 中的代码生成按以下原理工作:解析器从源代码读取注解,创建模型和查询的 AST(抽象语法树),然后生成带有 _$ 前缀的 Dart 文件。生成的代码包括 ResultSet → Entity 及反向映射器。
Floor 在单个隔离区中使用 SQLite 工作。所有查询异步执行,但并发写入在 SQLite 级别被阻止。对于事务,使用 @transaction 注解,它保证查询组的原子性和错误时的回滚。
Floor 和 Drift 都是基于 SQLite 的 ORM,但它们的理念不同。Floor 更接近 Android 中的 Room,Drift 则更具响应性,具有内置的 Stream API 和通过 SQL 文件的查询编译。它们之间的选择取决于团队的经验和所需的响应性。
| 特性 | Floor | Drift |
|---|---|---|
| 查询类型 | @Query 中的 SQL 字符串 | Dart 方法 + sql 文件 |
| 代码生成 | floor_generator (build_runner) | drift_dev (build_runner) |
| 响应性 | 来自 DAO 的 Stream | 内置 Stream API + 自动更新 |
| 复杂性 | 低(熟悉的 SQL) | 中等(自己的 DSL) |
| 迁移 | 手动 SQL 脚本 | 自动 + 手动 |
| 兼容性 | Android、iOS、macOS | Android、iOS、Web、macOS、Linux |
Floor — 适合已经熟悉 SQL 和 Android Room 的团队。如果开发人员习惯于手动编写 SQL 查询并希望在 SQLite 之上有一个最小包装 — Floor 提供了无需学习新 DSL 的类型化。它也更易于调试,因为生成的代码可读且可预测。
Drift 提供更强大的响应性并支持更多平台。如果应用程序积极使用 Stream 更新 UI,需要带有 JOIN 和子查询的复杂查询,或者为 Web 构建 — Drift 更可取。然而,由于需要学习自己的 DSL,其入门门槛更高。
Floor 围绕注解构建。下面是一个用于任务列表应用程序的 Entity、DAO 和 Database 的完整示例。运行 build_runner 后,生成的类即可使用。
TaskEntity 类带有 @Entity 注解,映射到 task 表。带有 @primaryKey 的字段成为主键。TaskDao 接口包含用于表操作的方法 — 每个方法使用 @Query、@Insert、@Update 或 @Delete 注解。
@entity
class TaskEntity {
@PrimaryKey(autoGenerate: true)
final int id;
final String title;
final bool isCompleted;
final int priority;
TaskEntity({this.id, required this.title,
this.isCompleted = false, this.priority = 0});
}
@dao
abstract class TaskDao {
@Query('SELECT * FROM TaskEntity ORDER BY priority DESC')
Future<List<TaskEntity>> getAllTasks();
@Insert
Future<int> insertTask(TaskEntity task);
@Update
Future<void> updateTask(TaskEntity task);
@Query('SELECT * FROM TaskEntity WHERE isCompleted = :status')
Stream<List<TaskEntity>> watchTasks(bool status);
}
带有 @Database 注解的抽象类连接 Entity 和 DAO。databaseBuilder 方法创建数据库实例。调用 build 后,数据库准备就绪:Floor 打开 SQLite 文件,应用迁移并返回 DAO 以供使用。
@Database(version: 1, entities: [TaskEntity])
abstract class AppDatabase extends FloorDatabase {
TaskDao get taskDao;
}
// 使用
final database = await $FloorAppDatabase.databaseBuilder('app.db').build();
final taskDao = database.taskDao;
final tasks = await taskDao.getAllTasks();
Floor 支持从 DAO 方法返回 Stream。当表中的任何更改发生时,Stream 发出一个新列表。这与 Flutter 中的 StreamBuilder 集成 — 在添加、修改或删除记录时 UI 会自动更新。
@Query('SELECT * FROM TaskEntity ORDER BY priority DESC')
Stream<List<TaskEntity>> watchAllTasks();
// 在 Flutter 组件中
StreamBuilder<List<TaskEntity>>(
stream: taskDao.watchAllTasks(),
builder: (context, snapshot) {
final tasks = snapshot.data ?? [];
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (_, i) => TaskTile(tasks[i]),
);
},
)
Floor 通过 @Database 注解中的 version 参数支持数据库版本管理。当 Entity 发生变化(添加或删除字段)时,需要增加版本并添加迁移。迁移是一个 Dart 函数,它接收一个事务并执行 ALTER TABLE SQL 查询。
假设在版本 2 中,我们在 TaskEntity 中添加了 dueDate 字段。迁移通过 ALTER TABLE SQL 查询执行。如果未指定迁移,Floor 会调用 MigrationStrategy,可以在其中设置回退(例如,重建表并丢失数据)。
Floor 不提供内置的模拟框架,但数据库可以在测试中轻松替换。创建 inMemoryDatabaseBuilder — 它在内存中创建一个 SQLite 数据库,模式与生产环境相同。每次测试后,通过 deleteDatabase 清除数据以隔离测试场景。
Floor 通过 DAO 方法上的 @transaction 注解支持事务。在事务内部,多个查询按顺序执行,并在出错时保证回滚。通过带有 List<T> 参数的 @Insert 进行批量插入优化了单次调用中插入多个记录 — 这比在循环中逐个插入快几倍。对于大量操作,使用 100–200 条记录的批量插入:这是在资源受限的移动设备上执行速度和 RAM 消耗之间的最佳平衡。
final migration1to2 = Migration(1, 2, (database) async {
await database.execute(
'ALTER TABLE TaskEntity ADD COLUMN dueDate TEXT'
);
});
final database = await $FloorAppDatabase.databaseBuilder('app.db')
.addMigrations([migration1to2])
.build();
常见问题
sqflite 需要手动编写 SQL 查询并将 ResultSet 映射到对象。Floor 自动生成此代码:您描述 Entity 和 DAO,类型化方法返回准备好的 Dart 对象。Floor 还通过注解在编译时检查 SQL 查询。
Floor 没有像 Room 那样内置的关系注解(ForeignKey、@Relation)。关系通过 @Query 中的手动 SQL JOIN 查询实现。对于复杂的关系模式,最好考虑 Drift 及其内置的关系支持。
Floor 允许在创建 DatabaseBuilder 时启用 callback 回调 — 向其传递 sqflite.Database 实例,可以在其上附加记录器。或者,使用 floor_doctor 在开发模式下可视化模式和数据。
Floor 使用在 Web 环境中不工作的 sqflite。对于 Web,需要通过 WASM 使用 sqlite3 进行单独构建。在当前版本中,Floor 正式支持 Android、iOS 和 macOS。对于 Web,使用带有 sqlite3 适配器的 Drift。
Floor 没有内置缓存 — 每个查询都执行到 SQLite。对于重复查询的缓存,使用带有内存缓存的 Repository 层(例如 dart_cache)。Floor 仅生成用于使用 SQLite 的代码,不会在其上添加层。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。