管理器(Manager)
摘要
为常用查询使用更简单的绑定。
Drift 提供了两种在 Dart 中编写查询的方式:一种是与 Dart 中的 SQL 非常相似的查询构建器,另一种是本页介绍的新的生成管理器界面。管理器界面的设计旨在使最常见的查询更容易编写。特别是,如果你是从另一个持久性库转向 drift,或者没有太多 SQL 经验,它们应该会很有帮助。
本页的示例使用了与设置说明中类似的数据库模式:
Select
当管理器生成被启用时(默认情况下是启用的),drift 将为数据库中的每个表生成一个管理器。这些管理器的集合通过数据库类上的 managers getter 来访问。除非表使用自定义行类,否则每个表都会为其生成一个管理器。
管理器简化了从表中检索行的过程。使用它来从表中读取行或监视更改。
Future<void> selectTodoItems() async {
// 获取所有项目
managers.todoItems.get();
// 所有待办事项的流,实时更新
managers.todoItems.watch();
// 要获取单个项目,请应用过滤器并调用 `getSingle`
await managers.todoItems.filter((f) => f.id(1)).getSingle();
}管理器提供了一个非常易于使用的 API,用于从表中选择行。这些可以与 | 和 & 以及括号组合以构建更复杂的查询。使用 .not 来否定一个条件。
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());
}每个列都有用于相等、不相等和可空性检查的过滤器。开箱即用地包含了 int、double、Int64、DateTime 和 String 的类型特定过滤器。
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 对象的记录。
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 字段中可用。
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();
}
}跨表过滤
你可以通过使用生成的引用过滤器来跨表引用进行过滤。你可以根据需要将它们嵌套得尽可能深,管理器将在幕后负责添加别名连接。
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();
}你还可以跨反向引用进行过滤。当存在一对多关系并且你希望根据子表过滤父表时,这很有用。
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(...) 注解在外键上为过滤器集指定一个自定义名称。如果你对同一个表有多个引用,这可能是必要的,请看以下示例:
我们现在可以在查询中像这样使用它们:
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 方法。使用 & 组合多个排序。排序按其添加顺序应用。你还可以像使用过滤器一样跨多个表使用排序。
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
管理器可以轻松检查行是否存在或计算满足特定条件的行数。
Future<void> count() async {
// 计算所有项目
await managers.todoItems.count();
// 计算所有标题为 "Title" 的项目
await managers.todoItems.filter((f) => f.title("Title")).count();
}Future<void> exists() async {
// 检查是否存在任何项目
await managers.todoItems.exists();
// 检查是否存在任何标题为 "Title" 的项目
await managers.todoItems.filter((f) => f.title("Title")).exists();
}更新
我们可以使用管理器批量更新行或满足特定条件的单个行。
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')));
}我们还可以用一个新行替换整个行。甚至可以一次替换多个行。
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);
}创建行
管理器包含一个用于快速将行插入表中的方法。我们可以一次插入单行或多行。
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'),
],
);
}删除行
我们也可以使用管理器从表中删除行。任何满足指定条件的行都将被删除。
Future<void> deleteTodoItems() async {
// 删除所有项目
await managers.todoItems.delete();
// 删除单个项目
await managers.todoItems.filter((f) => f.id(5)).delete();
}计算字段
当你需要从数据库表中选择整行及其相关数据时,管理器查询非常有用。但是,在某些情况下,你可能希望直接在数据库中执行更复杂的操作以提高效率。
Drift 对编写 SQL 表达式提供了强大的支持。这些表达式可用于过滤数据、对结果进行排序以及在 SQL 查询中直接执行各种计算。这意味着你可以利用 SQL 的全部功能来直接在数据库中处理复杂的逻辑,从而使你的查询更高效,代码更简洁。
如果你想了解有关如何编写这些 SQL 表达式的更多信息,请参阅表达式文档。
// 首先用你想要使用的表达式创建一个计算字段
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');
}你可以编写引用同一表中其他列甚至其他表的表达式。连接将由管理器自动创建。
// 这个计算字段将获取此待办事项的用户的名称
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');
}你也可以使用聚合函数。
// 你可以对相关表中的多行进行聚合
// 以对它们执行计算
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');
}