Skip to content

管理器(Manager)

摘要

为常用查询使用更简单的绑定。


Drift 提供了两种在 Dart 中编写查询的方式:一种是与 Dart 中的 SQL 非常相似的查询构建器,另一种是本页介绍的新的生成管理器界面。管理器界面的设计旨在使最常见的查询更容易编写。特别是,如果你是从另一个持久性库转向 drift,或者没有太多 SQL 经验,它们应该会很有帮助。

本页的示例使用了与设置说明中类似的数据库模式:

Select

当管理器生成被启用时(默认情况下是启用的),drift 将为数据库中的每个表生成一个管理器。这些管理器的集合通过数据库类上的 managers getter 来访问。除非表使用自定义行类,否则每个表都会为其生成一个管理器。

管理器简化了从表中检索行的过程。使用它来从表中读取行或监视更改。

dart
Future<void> selectTodoItems() async {
  // 获取所有项目
  managers.todoItems.get();

  // 所有待办事项的流,实时更新
  managers.todoItems.watch();

  // 要获取单个项目,请应用过滤器并调用 `getSingle`
  await managers.todoItems.filter((f) => f.id(1)).getSingle();
}

管理器提供了一个非常易于使用的 API,用于从表中选择行。这些可以与 |& 以及括号组合以构建更复杂的查询。使用 .not 来否定一个条件。

dart
Future<void> filterTodoItems() async {
  // 所有标题为 "Title" 的项目
  managers.todoItems.filter((f) => f.title("Title"));

  // 所有标题为 "Title" 且内容为 "Content" 的项目
  managers.todoItems.filter((f) => f.title("Title") & f.content("Content"));

  // 所有标题为 "Title" 或内容不为 null 的项目
  managers.todoItems.filter((f) => f.title("Title") | f.content.not.isNull());
}

每个列都有用于相等、不相等和可空性检查的过滤器。开箱即用地包含了 intdoubleInt64DateTimeString 的类型特定过滤器。

dart
Future<void> filterWithType() async {
  // 过滤自 7 天前以来创建的所有项目
  managers.todoItems.filter(
    (f) => f.createdAt.isAfter(DateTime.now().subtract(Duration(days: 7))),
  );

  // 过滤所有标题以 "Title" 开头的项目
  managers.todoItems.filter((f) => f.title.startsWith('Title'));
}

引用其他表

管理器还通过使用 withReferences 方法,使得查询实体的引用字段变得容易。这将返回一个包含实体和包含引用字段的 refs 对象的记录。

dart
Future<void> references() async {
  /// 获取每个待办事项及其类别
  final todosWithRefs = await managers.todoItems.withReferences().get();
  for (final (todo, refs) in todosWithRefs) {
    final category = await refs.category?.getSingle();
  }

  /// 这也反向适用
  final categoriesWithRefs = await managers.todoCategory
      .withReferences()
      .get();
  for (final (category, refs) in categoriesWithRefs) {
    final todos = await refs.todoItemsRefs.get();
  }
}

上述方法的问题在于,它将为结果集中的每一行发出一个单独的查询。如果你有大量的行,这可能会非常低效。如果有 1000 个待办事项,这将发出 1000 个查询来获取每个待办事项的类别。

关于外键的过滤

当对引用列进行过滤时,drift 会将过滤器应用于列本身,而不是连接被引用的表。例如,todos.filter((f) => f.category.id(1)) 将对 todos 表上的 category 列进行过滤,而不是连接两个表并对 categories 表的 id 列进行过滤。

这对我有什么影响?

如果你启用了外键约束(PRAGMA foreign_keys = ON),这不会影响你。数据库将强制 categories 表上的 id 列与 todos 表上的 category 列相同。

如果你没有启用外键约束,你应该意识到上面的查询不会检查 id 为 1 的类别是否存在。它只会检查 todos 表上的 category 列是否为 1。

预取引用

Drift 提供了一种在单个查询中预取引用的方法,以避免低效的查询。这是通过在 withReferences 方法中使用回调来完成的。然后,被引用的项将在被引用的管理器的 prefetchedData 字段中可用。

dart
Future<void> referencesPrefetch() async {
  /// 获取每个待办事项及其类别
  final todosWithRefs = await managers.todoItems
      .withReferences((prefetch) => prefetch(category: true))
      .get();
  for (final (todo, refs) in todosWithRefs) {
    final category = refs.category?.prefetchedData?.firstOrNull;
    // 不再需要
    // final category = await refs.category?.getSingle();
  }

  /// 这也反向适用
  final categoriesWithRefs = await managers.todoCategory
      .withReferences((prefetch) => prefetch(todoItemsRefs: true))
      .get();
  for (final (category, refs) in categoriesWithRefs) {
    final todos = refs.todoItemsRefs.prefetchedData;
    // 不再需要
    //final todos = await refs.todoItemsRefs.get();
  }
}

跨表过滤

你可以通过使用生成的引用过滤器来跨表引用进行过滤。你可以根据需要将它们嵌套得尽可能深,管理器将在幕后负责添加别名连接。

dart
Future<void> relationalFilter() async {
  // 获取类别描述为 "School" 的所有项目
  managers.todoItems.filter((f) => f.category.description("School"));

  // 这些可以与其他过滤器组合
  // 例如,获取所有标题为 "Title" 或类别描述为 "School" 的项目
  await managers.todoItems
      .filter((f) => f.title("Title") | f.category.description("School"))
      .exists();
}

你还可以跨反向引用进行过滤。当存在一对多关系并且你希望根据子表过滤父表时,这很有用。

dart
Future<void> reverseRelationalFilter() async {
  // 获取具有 id 为 1 的待办事项的类别
  managers.todoCategory.filter((f) => f.todoItemsRefs((f) => f.id(1)));

  // 这些可以与其他过滤器组合
  // 例如,获取所有描述为 "School" 或具有 id 为 1 的待办事项的类别
  managers.todoCategory.filter(
    (f) => f.description("School") | f.todoItemsRefs((f) => f.id(1)),
  );
}

代码生成器将使用被引用的表的名称来命名此过滤器集。在上面的示例中,过滤器集名为 todoItemsRefs,因为正在引用 TodoItems 表。但是,你也可以使用 @ReferenceName(...) 注解在外键上为过滤器集指定一个自定义名称。如果你对同一个表有多个引用,这可能是必要的,请看以下示例:

我们现在可以在查询中像这样使用它们:

dart
Future<void> reverseNamedRelationalFilter() async {
  // 获取所有是名称包含 "Business" 的组的管理员的用户
  // 或者拥有 id 为 1、2、4 或 5 的组的用户
  managers.users.filter(
    (f) =>
        f.administeredGroups((f) => f.name.contains("Business")) |
        f.ownedGroups((f) => f.id.isIn([1, 2, 4, 5])),
  );
}

在此示例中,如果我们没有为引用指定自定义名称,代码生成器将为对 User 表的两个引用都将两个过滤器集都命名为 userRefs。这将导致冲突。通过指定自定义名称,我们可以避免此问题。

名称冲突

Drift 会根据你的表和字段的名称自动生成过滤器和排序。但是,很多时候,会出现重复。发生这种情况时,你将看到来自生成器的警告消息。要解决此问题,请使用 @ReferenceName() 注解来指定我们应该如何命名过滤器/排序。

排序

你还可以使用 orderBy 方法对查询结果进行排序。语法类似于 filter 方法。使用 & 组合多个排序。排序按其添加顺序应用。你还可以像使用过滤器一样跨多个表使用排序。

dart
Future<void> orderWithType() async {
  // 按创建日期升序对所有项目进行排序
  managers.todoItems.orderBy((o) => o.createdAt.asc());

  // 按标题升序,然后按内容升序对所有项目进行排序
  managers.todoItems.orderBy((o) => o.title.asc() & o.content.asc());
}

orderBy 中包含可空列时,你可能希望控制 NULL 值是放在结果的开头还是结尾。这可以通过 asc()desc() 上的 nulls 参数来实现。例如,你可以编写 o.title.asc(nulls: NullsOrder.first) 来请求没有标题的待办事项出现在有标题的待办事项之前。

Count 和 exists

管理器可以轻松检查行是否存在或计算满足特定条件的行数。

dart
Future<void> count() async {
  // 计算所有项目
  await managers.todoItems.count();

  // 计算所有标题为 "Title" 的项目
  await managers.todoItems.filter((f) => f.title("Title")).count();
}
dart
Future<void> exists() async {
  // 检查是否存在任何项目
  await managers.todoItems.exists();

  // 检查是否存在任何标题为 "Title" 的项目
  await managers.todoItems.filter((f) => f.title("Title")).exists();
}

更新

我们可以使用管理器批量更新行或满足特定条件的单个行。

dart
Future<void> updateTodoItems() async {
  // 更新所有项目
  await managers.todoItems.update((o) => o(content: Value('New Content')));

  // 更新多个项目
  await managers.todoItems
      .filter((f) => f.id.isIn([1, 2, 3]))
      .update((o) => o(content: Value('New Content')));
}

我们还可以用一个新行替换整个行。甚至可以一次替换多个行。

dart
Future<void> replaceTodoItems() async {
  // 替换单个项目
  var obj = await managers.todoItems.filter((o) => o.id(1)).getSingle();
  obj = obj.copyWith(content: 'New Content');
  await managers.todoItems.replace(obj);

  // 替换多个项目
  var objs = await managers.todoItems
      .filter((o) => o.id.isIn([1, 2, 3]))
      .get();
  objs = objs.map((o) => o.copyWith(content: 'New Content')).toList();
  await managers.todoItems.bulkReplace(objs);
}

创建行

管理器包含一个用于快速将行插入表中的方法。我们可以一次插入单行或多行。

dart
Future<void> createTodoItem() async {
  // 创建一个新项目
  await managers.todoItems.create(
    (o) => o(title: 'Title', content: 'Content'),
  );

  // 我们还可以使用 `mode` 和 `onConflict` 参数,就像
  // 表上的 `[InsertStatement.insert]` 方法一样
  await managers.todoItems.create(
    (o) => o(title: 'Title', content: 'New Content'),
    mode: InsertMode.replace,
  );

  // 我们还可以一次创建多个项目
  await managers.todoItems.bulkCreate(
    (o) => [
      o(title: 'Title 1', content: 'Content 1'),
      o(title: 'Title 2', content: 'Content 2'),
    ],
  );
}

删除行

我们也可以使用管理器从表中删除行。任何满足指定条件的行都将被删除。

dart
Future<void> deleteTodoItems() async {
  // 删除所有项目
  await managers.todoItems.delete();

  // 删除单个项目
  await managers.todoItems.filter((f) => f.id(5)).delete();
}

计算字段

当你需要从数据库表中选择整行及其相关数据时,管理器查询非常有用。但是,在某些情况下,你可能希望直接在数据库中执行更复杂的操作以提高效率。

Drift 对编写 SQL 表达式提供了强大的支持。这些表达式可用于过滤数据、对结果进行排序以及在 SQL 查询中直接执行各种计算。这意味着你可以利用 SQL 的全部功能来直接在数据库中处理复杂的逻辑,从而使你的查询更高效,代码更简洁。

如果你想了解有关如何编写这些 SQL 表达式的更多信息,请参阅表达式文档。

dart
// 首先用你想要使用的表达式创建一个计算字段
final titleLengthField = db.managers.todoItems.computedField(
  (o) => o.title.length,
);

/// 创建一个包含你想要使用的计算字段的管理器副本
final manager = db.managers.todoItems.withFields([titleLengthField]);

// 然后在过滤器中使用计算字段
// 这将过滤所有标题恰好有 10 个字符的项目
manager.filter((f) => titleLengthField.filter(10));

// 你也可以在排序中使用计算字段
// 这将按标题长度升序对所有项目进行排序
manager.orderBy((o) => titleLengthField.order.asc());

/// 你也可以读取计算字段的结果
for (final (item, refs) in await manager.get()) {
  final titleLength = titleLengthField.read(refs);
  print('Item ${item.id} has a title length of $titleLength');
}

你可以编写引用同一表中其他列甚至其他表的表达式。连接将由管理器自动创建。

dart
// 这个计算字段将获取此待办事项的用户的名称
final todoUserName = db.managers.todoItems.computedField(
  (o) => o.category.user.name,
);

/// 创建一个包含你想要使用的计算字段的管理器副本
final manager = db.managers.todoItems.withFields([todoUserName]);

/// 你也可以读取计算字段的结果
for (final (item, refs) in await manager.get()) {
  final userName = todoUserName.read(refs);
  print('Item ${item.id} has a user with the name $userName');
}

你也可以使用聚合函数。

dart
// 你可以对相关表中的多行进行聚合
// 以对它们执行计算
final todoCountcomputedField = db.managers.todoCategory.computedField(
  (o) => o.todoItemsRefs((o) => o.id).count(),
);

/// 创建一个包含你想要使用的计算字段的管理器副本
final manager = db.managers.todoCategory.withFields([todoCountcomputedField]);

/// 读取计算字段的结果
for (final (category, refs) in await manager.get()) {
  final todoCount = todoCountcomputedField.read(refs);
  print('Category ${category.id} has $todoCount todos');
}