Skip to content

表达式(Expressions)

摘要

深入探讨可以在 Dart 中编写什么样的 SQL 表达式。


表达式是 SQL 的一部分,当数据库解释它们时会返回一个值。Drift 允许你在 Dart 中编写大多数表达式,然后将它们转换为 SQL。表达式用于各种情况。例如,where 需要一个返回布尔值的表达式。

在大多数情况下,你编写的表达式会组合其他表达式。任何列名都是一个有效的表达式,因此对于大多数 where 子句,你将编写一个将列名包装在某种比较中的表达式。

比较

每个表达式都可以通过使用 equals 与一个值进行比较。如果你想将一个表达式与另一个表达式进行比较,你可以使用 equalsExpr。对于数字和日期时间表达式,你还可以使用各种方法,如 isSmallerThanisSmallerOrEqual 等来进行比较:

dart
// 查找所有腿少于 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 方法来嵌套布尔表达式:

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

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

如果你有一个需要匹配其中一个或所有谓词的列表,你可以分别使用 Expression.orExpression.and

算术

对于 intdouble 表达式,你可以使用 +-*/ 运算符。要在 SQL 表达式和 Dart 值之间进行计算,请将其包装在 Variable 中:

字符串表达式也定义了一个 + 运算符。正如你所期望的,它在 SQL 中执行连接操作。

对于整数值,你可以使用 ~bitwiseAndbitwiseOr 来执行按位操作:

BigInt

虽然 SQLite 和 Dart VM 使用 64 位整数,但编译为 JavaScript 的 Dart 应用程序却不使用。因此,为了在编译到 Web 时表示大的整数结果,你可能需要将表达式转换为 BigInt

使用 dartCast<BigInt>() 将确保 drift 将结果解释为 BigInt。这不会改变生成的 SQL,drift 对所有数据库都使用 64 位整数类型。

示例: 对于表达式 (table.columnA * table.columnB).dartCast<BigInt>(),即使 columnAcolumnB 被定义为常规整数,drift 也会将结果值报告为 BigInt

Null 检查

要检查表达式在 SQL 中是否计算为 NULL,你可以使用 isNull 扩展:

如果内部表达式解析为 null,则返回的表达式将解析为 true,否则解析为 false。正如你所期望的,isNotNull 的工作方式相反。

要在表达式计算为 null 时使用回退值,你可以使用 coalesce 函数。它接受一个表达式列表,并计算为第一个不为 null 的表达式:

这对应于 Dart 中的 ?? 运算符。

日期和时间

对于返回 DateTime 的列和表达式,你可以使用 yearmonthdayhourminutesecond getter 来从该日期中提取单个字段:

yearmonth 等单个字段本身就是表达式。这意味着你可以在它们上面使用运算符和比较。要获取当前日期或当前时间作为表达式,请使用 drift 提供的 currentDatecurrentDateAndTime 常量。

你还可以使用 +- 运算符从时间列中添加或减去一个持续时间:

dart
Future<void> increaseDueDates() async {
  final change = TodoItemsCompanion.custom(
    dueDate: todoItems.dueDate + Duration(days: 1),
  );
  await update(todoItems).write(change);
}

对于更复杂的日期时间转换,modifymodifyAll 函数很有用。例如,这将每个待办事项的 dueDate 值增加到星期一的同一时间:

dart
Future<void> moveDueDateToNextMonday() async {
  final change = TodoItemsCompanion.custom(
    dueDate: todoItems.dueDate.modify(
      DateTimeModifier.weekday(DateTime.monday),
    ),
  );
  await update(todoItems).write(change);
}

INNOT IN

你可以通过使用 isInisNotIn 方法来检查表达式是否在值列表中:

同样,isNotIn 函数的工作方式相反。

JSON

通过 package:drift/extensions/json1.dart 提供了对常见 JSON 运算符的支持。这提供了诸如 jsonExtract 来从 JSON 中提取字段或 jsonEach 来查询嵌套的 JSON 结构之类的东西。有关更多详细信息,请参阅 select 页面上的 JSON 支持 部分或这个更复杂的示例

聚合函数(如 count 和 sum)

聚合函数可从 Dart api 获得。与常规函数不同,聚合函数一次对多行进行操作。默认情况下,它们将 select 语句将返回的所有行组合成一个单一的值。你还可以通过使用 group by 使它们在结果中的不同组上运行。

比较

你可以在数字和日期时间表达式上使用 minmax 方法。它们分别返回结果集中的最小值或最大值。

算术

avgsumtotal 方法可用。例如,你可以使用此查询来观察待办事项的平均长度:

注意:我们使用的是 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

isInisNotIn 函数类似,你可以使用 isInQuery 来传递子查询而不是直接的值集。

子查询必须只返回一列,但允许返回多于一行。如果该值存在于查询中,isInQuery 返回 true。

Exists

existsQuerynotExistsQuery 函数可用于检查子查询是否包含任何行。例如,我们可以使用它来查找空类别:

完整子查询

Drift 还支持出现在 JOIN 中的子查询,这在连接文档中有描述。

自定义表达式

如果你想在 Dart 查询中内联自定义 SQL,你可以使用 CustomExpression 类。它接受一个 sql 参数,让你编写自定义表达式:

注意:过多地使用 CustomExpressions 很容易编写出无效的查询。如果你觉得需要使用它们是因为你使用的功能在 drift 中不可用,请考虑创建一个问题让我们知道。如果你只是更喜欢 SQL,你也可以看看编译的 SQL,它是类型安全的。

特别是当自定义表达式需要嵌入子表达式时,CustomExpression 有点局限性。一个更复杂的替代方案是直接实现 Expression,这可以让你完全控制如何将代码片段写入 SQL。例如,这是一个使用 Drift 的查询构建器实现行值的表达式:

dart
/// 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(')');
  }
}

然后可以像这样使用它:

dart
void rowValuesUsage() {
  select(animals).where((row) {
    // Generates (amount_of_legs, average_livespan) < (?, ?)
    return RowValues([
      row.amountOfLegs,
      row.averageLivespan,
    ]).isSmallerThan(RowValues([Variable(2), Variable(10)]));
  });
}