[官网文档翻译]Flutter持久化库drift - 高级特性 - 表达式

1,089 阅读6分钟

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

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

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

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


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

表达式

深入探讨下在 Dart 中可以编写的 sql 表达式的种类。

表达式是 sql 语句块,在数据库解释执行后会返回一个值。 drift 中的 Dart api 允许使用 Dart 编写大多数的表达式,然后把它们转换为 sql 。表达式可以用于所有情况。例如: where 期望一个返回 boolean 值的表达式。

大多数情况,可以写一个表达式绑定其它表达式。任何列名都是有效的表达式,所以对于大多数 where 子句可能需要写一个表达式,来包装用于各种比较的列名。

比较

每个表达式都可以用 equals 来比较一个值。如果想比较一个表达式和另一个表达式,可以使用 equalsExpr。对于 数值和日期时间表达式,可以使用多种方法来比较。如: isSmallerThanisSmallerOrEqual 等。

// 查找少于5条腿的动物:
(select(animals)..where((a) => a.amountOfLegs.isSmallerThanValue(5))).get();

// find all animals who's average livespan is shorter than their amount of legs (poor flies)
// 查找所有平均寿命少于它们腿的数量的动物(可怜的苍蝇)
(select(animals)..where((a) => a.averageLivespan.isSmallerThan(a.amountOfLegs)));

Future<List<Animal>> findAnimalsByLegs(int legCount) {
  return (select(animals)..where((a) => a.legs.equals(legCount))).get();
}

逻辑运算

可以使用 &| 操作符和 drift 暴露的 not 方法来嵌套逻辑表达式。

// 查找所有有4条腿的非哺乳动物
select(animals)..where((a) => a.isMammal.not() & a.amountOfLegs.equals(4));

// 查找所有的哺乳动物或有2条腿的动物
select(animals)..where((a) => a.isMammal | a.amountOfLegs.equals(2));

计算

对于 intdouble 表达式,可以使用 +-*/ 操作符。如果要在 sql 表达式和 Dart 值之间运行计算,需要用 Variable 包装起来。

Future<List<Product>> canBeBought(int amount, int price) {
  return (select(products)..where((p) {
    final totalPrice = p.price * Variable(amount);
    return totalPrice.isSmallerOrEqualValue(price);
  })).get();
}

字符串表达式也定义了一个 + 操作符。正如所期望的,它用来在 sql 中连接(字符串)。

判空

要使用表达式来检查在 sql 中是否为 NULL,可以使用 isNull 扩展:

final withoutCategories = select(todos)..where((row) => row.category.isNull());

如果内部的表达式的结果是 null , 返回的表达式结果会是 true ,否则是 falseisNotNull 则相反。

要在表达式的评估结果是 null时使用回退值,可以使用 coalesce 函数。它有一个表达式列表参数,第一个评估结果不是 null 的值会被返回。

final category = coalesce([todos.category, const Constant(1)]);

这对应 Dart 中的 ?? 操作符。

日期和时间

如果数据列或者表达式返回一个 DateTime,可以使用 yearmonthdayhourminutesecond 的 getter 从日期中抽取单个时间域。

select(users)..where((u) => u.birthDate.year.isLessThan(1950))

单个时间域如 yearmonth 等本身也是表达式。这意味着可以对它们使用操作符和比较。为了获取当前日期或当前时间作为表达式,使用 drift 提供的 currentDatecurrentDateAndTime 的常量。

对时间数据列,可以使用 +- 操作符来加上或减去一个时间间隔。

final toNextWeek = TasksCompanion.custom(dueDate: tasks.dueDate + Duration(weeks: 1));
update(tasks).write(toNextWeek);

IN 和 NOT IN

使用 isInisNotIn 方法,可以检查表达式(的结果)是否在值的列表中。

select(animals)..where((a) => a.amountOfLegs.isIn([3, 7, 4, 2]);

isNotIn 也是相反的情况。

聚集函数(如 count 和 sum)

Dart api 中有可用的 聚集函数 。不像常规的函数,聚集函数同时操作多行数据。默认情况下,这些函数会结合 select 语句结果的所有数据行,然后(将计算结果)放到单个值里。可以使用 group by 让这些函数运行在不同的分组上,获取其结果。

比较

可以对数值或日期表达式使用 minmax 方法,各自返回结果集中的最小值或最大值。

运算

avgsumtotal 方法可用。例如:如果想监视一个 todo 项目的(内容的)平均长度,可以使用如下查询:

Stream<double> averageItemLength() {
  final avgLength = todos.content.length.avg();

  final query = selectOnly(todos)
    ..addColumns([avgLength]);

  return query.map((row) => row.read(avgLength)).watchSingle();
}

注: 使用 selectOnly 代替 select ,是因为我们对 todos 提供的任意列都不感兴趣 - 我们只关注平均长度。这里有更多详情。

计数

有时,计算下分组中展现了多少条数据行是有用的。 借用示例中的表结构,以下查询会报告每种 category 关联的 todo 实体有多少:

final amountOfTodos = todos.id.count();

final query = db.select(categories).join([
  innerJoin(
    todos,
    todos.category.equalsExp(categories.id),
    useColumns: false,
  )
]);
query
  ..addColumns([amountOfTodos])
  ..groupBy([categories.id]);

如果不想对重复值计数,可以使用 count(distince:true) 。有时,只需要对满足条件的数据计数。可以在 count 中使用 filter 参数。要计数所有行(而不是单个值),可以使用顶级函数 countAll()

关于如何用 drift 的 Dart api 编写聚集查询的更多信息,这里有更多可用信息。

group_concat (分组连接)

groupConcat 函数可以把多个值结合为一个字符串:

Stream<String> allTodoContent() {
  final allContent = todos.content.groupConcat();
  final query = selectOnly(todos)..addColumns(allContent);

  return query.map((row) => row.read(query)).watchSingle();
}

分割符默认使用逗号,并且逗号前后没有空格。可以通过 groupConcatseparator 参数来改变分割符。

数学函数和正则表达式

使用 NativeDatabase,会有一个三角函数的基础集。它( NativeDatabase )也定义了 REGEXP (正则表达式)函数,允许在 sql 查询中使用 a REGEXP b 这样的正则表达式。更多信息参考函数列表

子查询

drift 有对表达式中子查询的基本支持。

数量子查询

数量子查询是一个 select 语句,只返回一条单列 的数据行。因为它只返回一个值,所以可以如下使用:

Future<List<Todo>> findTodosInCategory(String description) async {
  final groupId = selectOnly(categories)
    ..addColumns([categories.id])
    ..where(categories.description.equals(description));

  return select(todos)..where((row) => row.category.equalsExp(subqueryExpression(groupId)));
}

在这里, groudId 是一个常规的 select 语句。 drift 默认会选取所有列,所以使用 selectOnly 只加载我们关注的 category 的 id。然后可以使用 subqueryExpression 将这个查询嵌入到一个作为条件过滤的表达式中。

isIn 查询

isInisNotIn 类似,可以使用 isInQuery 传递一个子查询,代替传递值的直接集合。

存在

existsQuerynotExistsQuery 函数用来检查一个子查询是否包含任意数据行。例如,用这个来查找空的 category :

Future<List<Category>> emptyCategories() {
  final hasNoTodo = notExistsQuery(
      select(todos)..where((row) => row.category.equalsExp(categories.id)));
  return select(categories)..where((row) => hasNoTodo);
}

自定义表达式

如果想要内联自定义 sql 到 Dart 查询中,可以使用 CustomExpression 类。它接收一个 sql 参数,用来编写自定义表达式。

const inactive = CustomExpression<bool, BoolType>("julianday('now') - julianday(last_login) > 60");
select(users)..where((u) => inactive);

注: 过多使用 CustomExpressions 容易写出不正确的查询。如果是因为想要使用的特性在 drift 中不可用,而认为需要使用这些(自定义表达式),考虑下创建一个 issue 让我们知道。如果只是因为更想用 sql , 也可以看一下 编译后的 sql ,它用起来是类型安全的。