「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」。
Flutter持久化库drift(原moor)官方文档翻译汇总 - 掘金 (juejin.cn)
本文翻译自 drift 的 官方文档 Expressions (simonbinder.eu)。
肉翻多有不足,不吝赐教。
表达式
深入探讨下在 Dart 中可以编写的 sql 表达式的种类。
表达式是 sql 语句块,在数据库解释执行后会返回一个值。 drift 中的 Dart api 允许使用 Dart 编写大多数的表达式,然后把它们转换为 sql 。表达式可以用于所有情况。例如: where 期望一个返回 boolean 值的表达式。
大多数情况,可以写一个表达式绑定其它表达式。任何列名都是有效的表达式,所以对于大多数 where 子句可能需要写一个表达式,来包装用于各种比较的列名。
比较
每个表达式都可以用 equals 来比较一个值。如果想比较一个表达式和另一个表达式,可以使用 equalsExpr。对于 数值和日期时间表达式,可以使用多种方法来比较。如: isSmallerThan , isSmallerOrEqual 等。
// 查找少于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));
计算
对于 int 和 double 表达式,可以使用 + ,- ,* 和 / 操作符。如果要在 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 ,否则是 false 。 isNotNull 则相反。
要在表达式的评估结果是 null时使用回退值,可以使用 coalesce 函数。它有一个表达式列表参数,第一个评估结果不是 null 的值会被返回。
final category = coalesce([todos.category, const Constant(1)]);
这对应 Dart 中的 ?? 操作符。
日期和时间
如果数据列或者表达式返回一个 DateTime,可以使用 year 、month 、day 、hour 、minute 和 second 的 getter 从日期中抽取单个时间域。
select(users)..where((u) => u.birthDate.year.isLessThan(1950))
单个时间域如 year , month 等本身也是表达式。这意味着可以对它们使用操作符和比较。为了获取当前日期或当前时间作为表达式,使用 drift 提供的 currentDate 和 currentDateAndTime 的常量。
对时间数据列,可以使用 + 和 - 操作符来加上或减去一个时间间隔。
final toNextWeek = TasksCompanion.custom(dueDate: tasks.dueDate + Duration(weeks: 1));
update(tasks).write(toNextWeek);
IN 和 NOT IN
使用 isIn 和 isNotIn 方法,可以检查表达式(的结果)是否在值的列表中。
select(animals)..where((a) => a.amountOfLegs.isIn([3, 7, 4, 2]);
isNotIn 也是相反的情况。
聚集函数(如 count 和 sum)
Dart api 中有可用的 聚集函数 。不像常规的函数,聚集函数同时操作多行数据。默认情况下,这些函数会结合 select 语句结果的所有数据行,然后(将计算结果)放到单个值里。可以使用 group by 让这些函数运行在不同的分组上,获取其结果。
比较
可以对数值或日期表达式使用 min 或 max 方法,各自返回结果集中的最小值或最大值。
运算
有 avg , sum , total 方法可用。例如:如果想监视一个 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();
}
分割符默认使用逗号,并且逗号前后没有空格。可以通过 groupConcat 的 separator 参数来改变分割符。
数学函数和正则表达式
使用 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 查询
和 isIn 和 isNotIn 类似,可以使用 isInQuery 传递一个子查询,代替传递值的直接集合。
存在
existsQuery 和 notExistsQuery 函数用来检查一个子查询是否包含任意数据行。例如,用这个来查找空的 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 ,它用起来是类型安全的。