随着社交媒体微信和微博的应用,数据呈现高动态和时间序列的分布形式,往往我们在查询用户发帖的瞬间,用户的帖子或评论就已经发生了“增删改查”等操作,那么如何及时准确地提取用户的帖子信息成了一个需要重视的问题。
结果集分页处理
在较大的数据集中,仅从数据库返回一部分行可能是有意义的。 例如,在微博和微信中,用户可能拥有多年来所发大量内容。 常规而言我们只提取最近的内容,或者说只提取有必要的早期内容。
Dart在Query<T>中,有两种机制用于构建可以获取特定范围内行的子集的查询。 初始分页可以使用Query<T>的 fetchLimit 和offset属性来完成。 例如,如果一个表包含100行,并且您希望一次抓取10个,则每个查询的fetchLimit值将为10。 第一个查询的偏移量为0,然后是10,然后是20,依此类推。 特别是在使用 sortBy时,这种类型的分页可能是有效的。 这种分页方式有一个缺点,如果我们在查询数据的这一刹那,用户发布或者删除了微博或微信,那么结果集会有重复或缺失数据。
例如,上图所示按时间排序的七个对象。 我们从第一个item(offset=0)开始分页一次抽取两个item(fetchLimit = 2),我们的第一个结果集是前两个对象。 下一页是offset加上每页的条目数 -(我们抽取接下来的两行)。 但是在下一页被提取之前,数据库插入了一个新item并将其放入我们已经获取的索引。 那么我们抽取的下一页数据将再次返回3:00pm条目。 如果以这种方式进行分页时删除了一行,也会出现类似的问题。
客户端应用程序必须检查并合并重复项确实很让人头疼。 有一种不受此问题影响的分页技术依赖于客户端从前一页中的最后一个条目发送值,而不是偏移量offset。 因此,在上面的示例中,不是在第二个查询中要求offset 2,而是把尾部值1:30pm
传递出来。 接下来的查询过滤出的值小于发送的值,排序剩余的行,然后从顶部获取最新的行。
Query.pageBy 使用这种技术。 它的用法类似于 sortBy:
var firstQuery = new Query<Post>()
..pageBy((p) => p.dateCreated, QuerySortOrder.descending)
..fetchLimit = 10;
var firstQueryResults = await firstQuery.fetch();
var oldestPostWeGot = firstQueryResults.last.dateCreated;
var nextQuery = new Query<Post>()
..pageBy((p) => p.dateCreated, QuerySortOrder.descending, boundingValue: oldestPostWeGot)
..fetchLimit = 10;
此查询将获取最新的10个帖子,并按时间由近到远顺序排列。 把第10条的创建时间提取为参数,这样第二次查询就只返回比第10条帖子更早的10条帖子。
pageBy方法使用闭包来标识哪个字段用于对行进行排序。 上例中的闭包传递一个Post
实例,并返回它的一个属性。pageBy的第二个参数定义了行将被排序的顺序。
当第一次开始分页时,因为还没有任何结果,所以不能从最后的结果集发送一个值。 在这种情况下,pageBy的boundingValue
为null
- 意味着从头开始。 一旦获取第一个集合,boundingValue就是返回的最后一个条目中的分页属性的值。
分页通常通过向接受边界值的端点添加查询参数来完成。 (请参阅ManagedObjectController<T> 为例。)
当没有更多值时,pageBy查询将返回一个空的条目列表。 如果最后一页剩余的对象数少于fetchLimit,则只返回那些对象。 例如,如果剩下四个条目并且fetchLimit是10,则返回的条目数将是四个。
我们应该对被分页的属性进行索引:
@ManagedColumnAttributes(indexed: true)
int pageableProperty;
过滤结果集
获取一张表里的所有数据通常是没有意义的。 相反,我们需要查询到一个特定的结果或一组匹配某些条件的结果集。
Query.where 是构建查询的安全和优雅的方式。 where属性允许您将匹配器matchers 分配给ManagedObject<T>的属性。 匹配器将条件(如等于或小于)应用于分配给它的属性。 (这与Dart测试框架使用的Hamcrest匹配器样式相同。)
Query.where与正在提取的对象的类型相同。 对于分配了匹配器的每个属性,表达式将被添加到SQL where子句中。 以下是查找ID为1的用户的示例:
var query = new Query<User>()
..where.id = whereEqualTo(1);
(以上生成 SQL 语句为 'SELECT _user.id, _user.name, ... FROM _user WHERE _user.id = 1'.)
所有匹配器都以where开头。 其他的例子是 whereGreaterThan, whereBetween, 和 whereIn 。 每个匹配器都使用and
进行组合。 换句话说,以下查询将查找
姓名为“Bob”且电子邮件不为空的所有用户:
var query = new Query<User>()
..where.name = whereEqualTo("Bob")
..where.email = whereNotNull;
关系属性也可以有匹配器。 例如,以下查询将获取所有有10岁以下儿童的父母:
var query = new Query<Parent>()
..where.children.haveAtLeastOneWhere.age = whereLessThan(10);
有一些重要的事情要理解,当用where 进行关联属性relationship properties查询时,结果集中并不返回任何关系属性的值。 在上一个查询中,这意味着将返回一个Parent
列表 - 但他们的children
项属性值不会被返回。 (要返回关联属性的值,请参考 join。)
我们在做关联查询时都会采用SQL join方式,这比常规查询要耗费更多的资源。 关系匹配器不会产生SQL join的唯一时间是匹配外键列的值。 也就是说,属于关系属性,我们只检查相关对象的主键。 有两种方法可以做到这一点:
var preferredQuery = new Query<Child>()
..where.parent = whereRelatedByValue(23);
var sameQuery = new Query<Child>()
..where.parent.id = whereEqualTo(23);
whereRelatedByValue方法是首选, 查询可以通过它的关系属性是否有值来过滤。 例如,以下查询返回有子女和无子女的人员:
var peopleWithoutChildren = new Query<Person>()
..where.children = whereNull;
var peopleWithChildren = new Query<Person>()
..where.children = whereNotNull;
唯一可以直接应用于关系属性的匹配器是以下示例中显示的三个匹配器: whereRelatedByValue, whereNull 和 whereNotNull.
。 关系属性的属性,例如: where.parent.age = whereGreaterThan(40)
,没有这些限制。
我们也可以得到关系的关系属性。 以下查询将返回父母是医生的孩子。
var childrenWithDoctorParents = new Query<Child>()
..where.parent.job.title = whereEqualTo("Doctor");
将匹配器处理一对多关系属性时,可以使用haveAtLeastOneWhere 属性。 例如,以下情况会返回所有至少有一个10岁以下儿童的父母 - 但他们可能有其他孩子大于10岁:
var query = new Query<Parent>()
..where.children.haveAtLeastOneWhere.age = whereLessThan(10);
过滤器filter应用于返回的Parents
项 - 如果Parents
项没有小于10岁的child
项,它将从结果集中剔除。 如果父母的一个孩子中只有一个小于10,那么也将被包括在结果集内。
在提取中包含关系(Joins)
Query<T>也可以获取关系属性。 这允许查询获取整个模型图model graphs
并减少和数据库的交互round-trips
次数。 (这种类型的读取操作将执行SQL LEFT OUTER JOIN
语句)
默认情况下,关系属性不会在查询中提取,因此不包含在对象的asMap()方法内。 例如,请考虑以下两个ManagedObject<T>s, 一个 User
拥有 Task
的查询:
class User extends ManagedObject<_User> implements _User {}
class _User {
@managedPrimaryKey int id;
String name;
ManagedSet<Task> tasks;
}
class Task extends ManagedObject<_Task> implements _Task {}
class _Task {
@managedPrimaryKey int id;
@ManagedColumnAttributes(#tasks)
User user;
String contents;
}
Query<User>
将获取每个用户的name 和 id 。 用户的任务不会被提取,所以返回的数据如下所示:
var q = new Query<User>();
var users = await q.fetch();
users.first.asMap() == {
"id": 1,
"name": "Bob"
}; // yup
join() 方法会告诉Query<T> 还包含一个特定的has-many关系,这里是一个用户的tasks
:
var q = new Query<User>()
..join(set: (u) => u.tasks);
var users = await q.fetch();
users.first.asMap() == {
"id": 1,
"name": "Bob",
"tasks": [
{"id": 1, "contents": "Take out trash", "user" : {"id": 1}},
...
]
}; // yup
请注意,这个tasks
实际上包含在这个查询中。 当加入一个has-many 关系时,set:
参数被赋予一个闭包,该闭包返回被查询类型的ManagedSet<T>属性。
join()方法实际返回一个新的Query<T>,其中T
是关系属性中的对象类型。 也就是说,上面的代码也可以这样写:
var q = new Query<User>();
// type annotation added for clarity
Query<Task> taskSubQuery = q.join(set: (u) => u.tasks);
就像其他任何Query<T>一样,返回属性的集合可以通过returningProperties进行修改:
var q = new Query<User>()
..returningProperties((u) => [u.id, u.name]);
q.join(set: (u) => u.tasks)
..returningProperties((t) => [t.id, t.contents]);
加入has-one或belongs-to关系时,请使用join(object:)
而不是join(set:)
:
var q = new Query<Task>()
..join(object: (t) => t.user);
var results = await q.fetch();
results.first.asMap() == {
"id": 1,
"contents": "Take out trash",
"user": {
"id": 1,
"name": "Bob"
}
}; // yup
请注意,此查询的结果包含Task.user
的所有详细信息 - 而不仅仅是其id。
通过 join 创建的子查询也可以通过其where属性进行过滤。 例如,以下查询将仅返回用户名为'Bob'及其逾期任务:
var q = new Query<User>()
..where.name = whereEquals("Bob");
q.join(set: (u) => u.tasks)
..where.overdue = whereEqualTo(true);
请注意,子查询上的where属性是Task
的一个实例,而User
查询中的位置是User
。
可以将多个join 应用于一个查询,并且子查询可以嵌套。 所以,这是全部有效的,假设关系属性存在:
var q = new Query<User>()
..join(object: (u) => u.address);
q.join(set: (u) => u.tasks)
..join(object: (u) => u.location);
这将返回所有用户,他们的地址,他们的所有任务以及每个任务的位置。
了解在使用where和子查询时如何过滤对象很重要。 适用于顶级查询的匹配器将过滤掉这些类型的对象。 子查询上的哪个位置对顶层返回的对象数量没有影响。
假设总共有10个用户,每个用户总共有10个任务。 以下查询返回所有10个用户对象,但每个用户的任务tasks
只包含那些过期的。 因此,用户可能会返回0,1或10个任务 。
var q = new Query<User>();
q.join(set: (u) => u.tasks)
..where.overdue = whereEqualTo(true);
但是,以下查询将返回少于10个用户,但对于每个返回的用户,他们将拥有全部10个任务:
var q = new Query<User>()
..where.name = whereEqualTo("Bob")
..join(set: (u) => u.tasks);
请注意,查询将始终获取所有对象的主键,即使它在returningProperties中被省略。
Reduce 函数 (也称, Aggregate 函数)
查询也可以用来执行诸如count, sum, average, min
and max
之类的函数。 这是一个例子:
var query = new Query<User>();
var numberOfUsers = await query.reduce.count();
reduce 函数使用某些属性值,通过属性选择器来标识该属性。
var averageSalary = await query.reduce.sum((u) => u.salary);
在Query<T> 中配置的任何值也会影响reduce 函数。 例如,应用Query.where然后执行sum函数将仅对符合where子句的条件的行进行求和:
var query = new Query<User>()
..where.name = "Bob";
var averageSalaryOfPeopleNamedBob = await query.reduce.sum((u) => u.salary);
回退机制
您可能总是使用PersistentStore.execute执行任意SQL。 请注意,返回的对象将是一个List<List<dynamic>>
你也可以用QueryPredicate提供原始raw的WHERE子句。 QueryPredicate是一个设置为查询的where子句的字符串。 QueryPredicate有两个属性,一个格式化字符串和一个Map<String, dynamic>
参数值。 格式化字符串format string可以(也应该)参数化任何输入值。 参数在格式字符串中使用@
标记指示:
// Creates a predicate that would only include instances where some column "id" is less than 2
var predicate = new QueryPredicate("id < @idVariable", {"idVariable" : 2});
@标记后面的文本可以包含[A-Za-z0-9_]。 所得到的where子句将通过用参数映射中的匹配键替换每个token而形成。 值不会以任何方式转换,因此它必须是列的适当类型。 如果Map中不存在某个键,则会抛出异常。 额外的键将被忽略。
网友评论