[官网文档翻译]Flutter持久化库drift - 增删改查(Dart)

4,251 阅读7分钟

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」。

Flutter持久化库drift(原moor)官方文档翻译汇总 - 掘金 (juejin.cn)

本文翻译自 drift 的 官方文档 Writing queries (simonbinder.eu)

肉翻多有不足,不吝赐教。


重要通知: moor 已改名为 drift 。更多信息[中文]。

编写查询

我们来学习一下如何在纯 Dart 代码中使用 drift 来写数据库的查询。

本文假定你已经完成了 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 创建这样的表达式。 整数列也可以用 isBiggerThanisSmallerThan 比较。 也可以用 a & ba | ba.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 会使用 getSinglewatchSingle 来解决这个问题。

Stream<Todo> entryById(int id) {
  return (select(todos)..where((t) => t.id.equals(id))).watchSingle();
}

如果存在一个带 id 的实体,它会被发送到流。否则 null 会被加到流里。如果一个查询使用了 watchSingle,但有时返回了多于一个的实体(在上例中不可能),一个 Error 会被加到流里。

映射

可以在调用 watchget (或 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))),
  );
}

唯一约束 和 冲突目标 insertOnConflictUpdateonConflict: DoUpdate 在 sql中使用 DO UPDATE 的插更。这需要我们提供称作 冲突目标 的数据列集合,用来检查唯一性违反。默认情况下,drift 会使用表的主键作为冲突目标。大多数情况下这样是运转的,但是如果有基于某些列的自定义唯一约束,就需要在 DoUpdatetarget 参数包含这些列。

注意这需要 sqlite3 的最近版本(3.24.0)。 在旧一些的 Android 设备上,包含最新的 sqlite 的moor_flutter.NativeDatabasessqlite3_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 设定到返回的数据行中。