「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」。
Flutter持久化库drift(原moor)官方文档翻译汇总 - 掘金 (juejin.cn)
本文翻译自 drift 的 官方文档 Writing queries (simonbinder.eu) 。
肉翻多有不足,不吝赐教。
编写查询
我们来学习一下如何在纯 Dart 代码中使用 drift 来写数据库的查询。
每个已在数据库类的@DriftDatabase 注解里指定的 表,都会生成相应的 getter 。这些 getter 可以用来执行语句。
@DriftDatabase(tables: [Todos, Categories])
class MyDatabase extends _$MyDatabase {
// 加载所有的 Todo 实体
Future<List<Todo>> get allTodoEntries => select(todos).get();
// 监听所有的指定 category 的 todo 实体。
// 如果底层数据修改,这个流会自动发出新的项目,即跟着底层数据实时更新。
Stream<List<Todo>> watchEntriesInCategory(Category c) {
return (select(todos)..where((t) => t.category.equals(c.id))).watch();
}
}
Select 语句
以 select(表名) 开始来创建 select 语句,表名是 drift 生成的域(field)。每个在数据库中用到的表都有一个匹配的域(field)。使用 get() 运行任意查询一次或使用 watch() 将任意查询转换为自动更新的流。
Where条件
可以调用 where() 方法来过滤查询。 where 方法带有一个匹配指定表并返回逻辑表达式的函数。
一个常用的方式是在表达式中用 equals 创建这样的表达式。
整数列也可以用 isBiggerThan 和 isSmallerThan 比较。
也可以用 a & b,a | b 和 a.not() 组成表达式。
关于表达式的更多细节可以参考指南。
Limit
可以在查询上调用 limit 方法来限定查询结果返回的记录数。
这个方法接收 返回的记录数 和 可选和偏移量 两个参数。
Future<List<Todo>> limitTodos(int limit, {int offset}) {
return (select(todos)..limit(limit, offset: offset)).get();
}
排序
可以在 select 语句上调用 orderBy 方法来排序。
它需要一个的函数列表的参数,这些函数从表中选取单独的 ordering term (排序术语)。
可以使用任意表达式作为 ordering term (排序术语)。
单条记录
如果知道查询永远不会返回多于一条的数据,再用 List 来包装结果就比较多余了。drift 会使用 getSingle 和 watchSingle 来解决这个问题。
Stream<Todo> entryById(int id) {
return (select(todos)..where((t) => t.id.equals(id))).watchSingle();
}
如果存在一个带 id 的实体,它会被发送到流。否则 null 会被加到流里。如果一个查询使用了 watchSingle,但有时返回了多于一个的实体(在上例中不可能),一个 Error 会被加到流里。
映射
可以在调用 watch 或 get (或 the single variants (单个变体?)) 之前,使用 map 来改变查询结果。
Stream<List<String>> contentWithLongTitles() {
final query = select(todos)
..where((t) => t.title.length.isBiggerOrEqualValue(16));
return query
.map((row) => row.content)
.watch();
}
延迟获取 VS 监听
如果想要把查询当作可消费的 Future 或者 Stream,可以使用 某个 Selectable 的抽象基类作为返回类型。
// 暴露为 `get` 和 `watch`
MultiSelectable<Todo> pageOfTodos(int page, {int pageSize = 10}) {
return select(todos)..limit(pageSize, offset: page);
}
// 暴露为 `getSingle` 和 `watchSingle`
SingleSelectable<Todo> entryById(int id) {
return select(todos)..where((t) => t.id.equals(id));
}
// 暴露为 `getSingleOrNull` 和 `watchSingleOrNull`
SingleOrNullSelectable<Todo> entryFromExternalLink(int id) {
return select(todos)..where((t) => t.id.equals(id));
}
这些基类不包含查询构建或映射方法,它们向消费方发送信号,告诉消费方它们是完整的结果。
如果需要更多复杂的查询( join 或自定义列),请查看这里。
更新和删除
可以使用生成的类来更新任意数据行的单独的列。
Future moveImportantTasksIntoCategory(Category target) {
// 对于更新,使用 生成类的 '姊妹' 版。
// 这个用 "Value" 封装了数据列,可以使用 "Value.absent()" 将字段设置成可不赋值的。这允许我们区分开 "SET category = NULL" (`category: Value(null)`) 和 用 `category: Value.absent()` 实现不进行更新。
// 即可区分开是设为 NULL 值,还是不更新。
return (update(todos)
..where((t) => t.title.like('%Important%'))
).write(TodosCompanion(
category: Value(target.id),
),
);
}
Future update(Todo entry) {
// 使用 replace 会用实体来更新所有没有标记为主键的字段。
// 这也能确保有相同主键的实体会被更新。
// 在这里是指,有相同 id 的数据行会被实体更新来反映实体的 title、content 和 category。
// where 子句会自动设置,所以不能和 where 同时使用。
return update(todos).replace(entry);
}
Future feelingLazy() {
// 删除9个旧任务。
return (delete(todos)..where((t) => t.id.isSmallerThanValue(10))).go();
}
⚠️警告: 如果没有明确地添加 where 子句来更新或删除,语句会影响表中的所有数据。
实体、实体姊妹类 - 为什么两者都需要?
你可能已经注意到了,(上面示例中的)第一次更新我们使用
TodosCampoin来代替Todo。drift 生成 Todo 实体类(也被称作表的数据类) 保持一行数据的完整内容。使用部分数据的话,用 compaion 更好。在上例中,只设定了category, 所以用的是 companion。 有这个必要? 如果一个字段设定为null,我们并不知道是否需要把 null 值再设定到数据库里,还是应该只是让其保持不变。companion 里的字段有一个Value.absent()状态可以明确这一点。 companinon 还有一个特殊的构造函数,会用@required注解来标记所有插入时无默认值且不为空的列。companion 的这个机制使插入数据变得更简单,因为你知道需要给哪些字段设置。
插入
可以很容易地向表中插入任何有效的数据。使用companion 版本,一些值可不设定(如不必明确设置默认值)
// 返回生成的 id
Future<int> addTodo(TodosCompanion entry) {
return into(todos).insert(entry);
}
生成的所有的数据行类都会有一个构造函数用来创建对象。
addTodo(
TodosCompanion(
title: Value('Important task'),
content: Value('Refactor persistence code'),
),
);
如果某列可空或有默认值(包括自增列),字段可不设值。其它字段必须设为非空值,否则 insert 方法会抛出(异常?)。
使用批处理,可以高效地运行多行插入语句。可以在 batch 里使用 insertAll 来做到这一点。
Future<void> insertMultipleEntries() async{
await batch((batch) {
// batch 中的函数不需要 await,只需要在整个 batch 前 await。
batch.insertAll(todos, [
TodosCompanion.insert(
title: 'First entry',
content: 'My content',
),
TodosCompanion.insert(
title: 'Another entry',
content: 'More content',
// 插入非必需的列,也需要用 Value 封装一下。
category: Value(3),
),
// ...
]);
});
}
插更(插入或更新)
插更是较新的 sqlite3 的新特性,插入数据时如果和现有数据行有冲突,会转换为更新数据。
当主键是操作对象数据的一部分时,这个特性允许我们创建或者覆写一个现有数据行。
class Users extends Table {
TextColumn get email => text()();
TextColumn get name => text()();
@override
Set<Column> get primaryKey => {email};
}
Future<void> createOrUpdateUser(User user) {
return into(users).insertOnConflictUpdate(user);
}
调用 createOrUpdateUser(),相同 email 的数据已存在时,name 会被更新。否则会向数据库里插入一个新 User 。
插入也可和更多高级查询一起使用。 例如,我们构建一个字典,然后想要追踪碰到某个单词的次数,表可能会如下:
class Words extends Table {
TextColumn get word => text()();
IntColumn get usages => integer().withDefault(const Constant(1))();
@override
Set<Column> get primaryKey => {word};
}
使用自定义的插更,可以插入一个新单词或单词已存在时更新 usages 计数。
Future<void> trackWord(String word) {
return into(words).insert(
WordsCompanion.insert(word: word),
onConflict: DoUpdate((old) => WordsCompanion.custom(usages: old.usages + Constant(1))),
);
}
唯一约束 和 冲突目标
insertOnConflictUpdate和onConflict: DoUpdate在 sql中使用DO UPDATE的插更。这需要我们提供称作冲突目标的数据列集合,用来检查唯一性违反。默认情况下,drift 会使用表的主键作为冲突目标。大多数情况下这样是运转的,但是如果有基于某些列的自定义唯一约束,就需要在DoUpdate的target参数包含这些列。
注意这需要 sqlite3 的最近版本(3.24.0)。
在旧一些的 Android 设备上,包含最新的 sqlite 的moor_flutter.NativeDatabases 和 sqlite3_flutter_libs 有可能不可用。 所以如果想要支持插更时需要考虑一下是否使用。
返回值
可以使用 insertReturning 来插入一条数据行或者数据行的姊妹数据,然后立即获取刚插入的数据行。返回的数据行中包含所有生成的默认值和自增 ID 。
注: 这用到了在 sqlite3 3.35版本中添加的 Returning 语法。这个语法在大多数操作系统中默认是不可用的。使用这个方法时,确保有最新的 sqlite3 版本可用。````sqlite3_flutter_libs``` 就尾于这种情况。
例如:考虑一下 开始指南中的这段代码:
final row = await into(todos).insertReturning(TodosCompanion.insert(
title: 'A todo entry',
content: 'A description',
));
返回的数据行含有 id 属性的集合。如果表有更多的默认值,包括像 CURRENT_TIME 这样的动态值,这些都会被 insertReturning 设定到返回的数据行中。