表达式(Expressions)
摘要
深入探讨可以在 Dart 中编写什么样的 SQL 表达式。
表达式是 SQL 的一部分,当数据库解释它们时会返回一个值。Drift 允许你在 Dart 中编写大多数表达式,然后将它们转换为 SQL。表达式用于各种情况。例如,where 需要一个返回布尔值的表达式。
在大多数情况下,你编写的表达式会组合其他表达式。任何列名都是一个有效的表达式,因此对于大多数 where 子句,你将编写一个将列名包装在某种比较中的表达式。
比较
每个表达式都可以通过使用 equals 与一个值进行比较。如果你想将一个表达式与另一个表达式进行比较,你可以使用 equalsExpr。对于数字和日期时间表达式,你还可以使用各种方法,如 isSmallerThan、isSmallerOrEqual 等来进行比较:
// 查找所有腿少于 5 条的动物:
(select(animals)..where((a) => a.amountOfLegs.isSmallerThanValue(5))).get();
// 查找所有平均寿命比腿数短的动物(可怜的苍蝇)
(select(animals)
..where((a) => a.averageLivespan.isSmallerThan(a.amountOfLegs)));
Future<List<Animal>> findAnimalsByLegs(int legCount) {
return (select(
animals,
)..where((a) => a.amountOfLegs.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));如果你有一个需要匹配其中一个或所有谓词的列表,你可以分别使用 Expression.or 和 Expression.and:
算术
对于 int 和 double 表达式,你可以使用 +、-、* 和 / 运算符。要在 SQL 表达式和 Dart 值之间进行计算,请将其包装在 Variable 中:
字符串表达式也定义了一个 + 运算符。正如你所期望的,它在 SQL 中执行连接操作。
对于整数值,你可以使用 ~、bitwiseAnd 和 bitwiseOr 来执行按位操作:
BigInt
虽然 SQLite 和 Dart VM 使用 64 位整数,但编译为 JavaScript 的 Dart 应用程序却不使用。因此,为了在编译到 Web 时表示大的整数结果,你可能需要将表达式转换为 BigInt。
使用 dartCast<BigInt>() 将确保 drift 将结果解释为 BigInt。这不会改变生成的 SQL,drift 对所有数据库都使用 64 位整数类型。
示例: 对于表达式 (table.columnA * table.columnB).dartCast<BigInt>(),即使 columnA 和 columnB 被定义为常规整数,drift 也会将结果值报告为 BigInt。
Null 检查
要检查表达式在 SQL 中是否计算为 NULL,你可以使用 isNull 扩展:
如果内部表达式解析为 null,则返回的表达式将解析为 true,否则解析为 false。正如你所期望的,isNotNull 的工作方式相反。
要在表达式计算为 null 时使用回退值,你可以使用 coalesce 函数。它接受一个表达式列表,并计算为第一个不为 null 的表达式:
这对应于 Dart 中的 ?? 运算符。
日期和时间
对于返回 DateTime 的列和表达式,你可以使用 year、month、day、hour、minute 和 second getter 来从该日期中提取单个字段:
像 year、month 等单个字段本身就是表达式。这意味着你可以在它们上面使用运算符和比较。要获取当前日期或当前时间作为表达式,请使用 drift 提供的 currentDate 和 currentDateAndTime 常量。
你还可以使用 + 和 - 运算符从时间列中添加或减去一个持续时间:
Future<void> increaseDueDates() async {
final change = TodoItemsCompanion.custom(
dueDate: todoItems.dueDate + Duration(days: 1),
);
await update(todoItems).write(change);
}对于更复杂的日期时间转换,modify 和 modifyAll 函数很有用。例如,这将每个待办事项的 dueDate 值增加到星期一的同一时间:
Future<void> moveDueDateToNextMonday() async {
final change = TodoItemsCompanion.custom(
dueDate: todoItems.dueDate.modify(
DateTimeModifier.weekday(DateTime.monday),
),
);
await update(todoItems).write(change);
}IN 和 NOT IN
你可以通过使用 isIn 和 isNotIn 方法来检查表达式是否在值列表中:
同样,isNotIn 函数的工作方式相反。
JSON
通过 package:drift/extensions/json1.dart 提供了对常见 JSON 运算符的支持。这提供了诸如 jsonExtract 来从 JSON 中提取字段或 jsonEach 来查询嵌套的 JSON 结构之类的东西。有关更多详细信息,请参阅 select 页面上的 JSON 支持 部分或这个更复杂的示例。
聚合函数(如 count 和 sum)
聚合函数可从 Dart api 获得。与常规函数不同,聚合函数一次对多行进行操作。默认情况下,它们将 select 语句将返回的所有行组合成一个单一的值。你还可以通过使用 group by 使它们在结果中的不同组上运行。
比较
你可以在数字和日期时间表达式上使用 min 和 max 方法。它们分别返回结果集中的最小值或最大值。
算术
avg、sum 和 total 方法可用。例如,你可以使用此查询来观察待办事项的平均长度:
注意:我们使用的是 selectOnly 而不是 select,因为我们对 todos 提供的任何列都不感兴趣 - 我们只关心平均长度。更多详细信息可在此处获得 here。
计数
有时,计算一个组中存在多少行是很有用的。通过使用示例中的表布局,此查询将报告每个类别关联了多少个待办事项:
如果你不想计算重复的值,你可以使用 count(distinct: true)。有时,你只需要计算匹配条件的行。为此,你可以在 count 上使用 filter 参数。要计算所有行(而不是单个值),你可以使用顶级的 countAll() 函数。
有关如何使用 drift 的 Dart api 编写聚合查询的更多信息,请参见此处
group_concat
groupConcat 函数可用于将多个值连接成一个字符串:
分隔符默认为逗号,不带周围的空格,但可以使用 groupConcat 上的 separator 参数进行更改。
窗口函数
除了聚合表达式和 groupBy,drift 还支持窗口函数。与将一组行折叠成单个值的常规聚合不同,窗口函数允许对与当前行相关的行子集运行聚合。例如,你可以使用它来跟踪值的运行总计:
窗口函数的一个有趣用途是确定如果按某个列对行进行排序,则行将具有的排名(而无需实际返回所有行或按该列对它们进行排序)。此排名可以附加到每一行:
数学函数和 regexp
当使用 NativeDatabase 时,将提供一组基本的三角函数。它还定义了 REGEXP 函数,允许你在 SQL 查询中使用 a REGEXP b。有关更多信息,请参阅此处的函数列表。
子查询
Drift 对表达式中的子查询有基本支持。
标量子查询
_标量子查询_是一个 select 语句,它只返回一个带有一列的一行。由于它只返回一个值,因此可以在另一个查询中使用:
在这里,groupId 是一个常规的 select 语句。默认情况下,drift 会选择所有列,因此我们使用 selectOnly 来只加载我们关心的类别的 id。然后,我们可以使用 subqueryExpression 将该查询嵌入到我们用作过滤器的表达式中。
isInQuery
与 isIn 和 isNotIn 函数类似,你可以使用 isInQuery 来传递子查询而不是直接的值集。
子查询必须只返回一列,但允许返回多于一行。如果该值存在于查询中,isInQuery 返回 true。
Exists
existsQuery 和 notExistsQuery 函数可用于检查子查询是否包含任何行。例如,我们可以使用它来查找空类别:
完整子查询
Drift 还支持出现在 JOIN 中的子查询,这在连接文档中有描述。
自定义表达式
如果你想在 Dart 查询中内联自定义 SQL,你可以使用 CustomExpression 类。它接受一个 sql 参数,让你编写自定义表达式:
注意:过多地使用 CustomExpressions 很容易编写出无效的查询。如果你觉得需要使用它们是因为你使用的功能在 drift 中不可用,请考虑创建一个问题让我们知道。如果你只是更喜欢 SQL,你也可以看看编译的 SQL,它是类型安全的。
特别是当自定义表达式需要嵌入子表达式时,CustomExpression 有点局限性。一个更复杂的替代方案是直接实现 Expression,这可以让你完全控制如何将代码片段写入 SQL。例如,这是一个使用 Drift 的查询构建器实现行值的表达式:
/// Writes row values (`(1, 2, 3)`) into SQL. We use [Never] as a bound because
/// this expression cannot be evaluated, it's only useful as a subexpression.
final class RowValues extends Expression<Never> {
final List<Expression> expressions;
RowValues(this.expressions);
@override
Precedence get precedence => Precedence.primary;
@override
void writeInto(GenerationContext context) {
context.buffer.write('(');
for (final (i, expr) in expressions.indexed) {
if (i != 0) context.buffer.write(', ');
expr.writeInto(context);
}
context.buffer.write(')');
}
}然后可以像这样使用它:
void rowValuesUsage() {
select(animals).where((row) {
// Generates (amount_of_legs, average_livespan) < (?, ?)
return RowValues([
row.amountOfLegs,
row.averageLivespan,
]).isSmallerThan(RowValues([Variable(2), Variable(10)]));
});
}