Skip to content

1.存储 3 AVQuery

jwfing edited this page Jun 20, 2018 · 3 revisions

AVQuery 是构建针对 AVObject 查询的基础类。每次查询默认最多返回 100 条符合条件的结果,要更改这一数值,请参考 限定结果返回数量

示例数据结构

熟悉本文所使用的相关数据表结构将有助于更好地理解后面的内容。

Todo(待办事项)

字段 类型 说明
content String 事项的详细内容
images AVFile 与事项相关的图片
location String 处理该事项的地点
priority Number 0 优先级最高,最迫切需要完成。
reminders Array 设置提醒日期和时间
status Number 0 未完成,1 已完成
title String 事项的标题(简短描述)
views Number 该事项被浏览过的次数
whereCreated AVGeoPoint 该事项被创建时的地理定位

TodoFolder(待办事项的分组)

字段 类型 说明
containedTodos Relation 所包含的 Todo,与表 Todo 相关联。
name String 分组的名称,如家庭、会议。
owner Pointer 分组的所有者或创建人,指向表 _User
priority Number 该分组的优先级别,0 优先级最高。
tags Relation 标签,与表 Tag 相关联。

Comment(待办事项分组的评论)

字段 类型 说明
content String 评论的内容
likes Number 点了赞就是 1,点了不喜欢为 -1,没有做任何操作就为 0(默认)。
targetTodoFolder Pointer 相关联的待办事项分组,指向表 TodoFolder 的 objectId

Tag(待办事项分组的标签)

字段 类型 说明
name String 标签的名称,如今日必做、老婆吩咐、十分重要等。
targetTodoFolder Pointer 相关联的待办事项分组,指向表 TodoFolder 的 objectId

创建查询实例

最基础的用法是根据 objectId 来查询对象:

    String objectId = "558e20cbe4b060308e3eb36c";
    AVQuery<AVObject> avQuery = new AVQuery<>("Todo");
    AVObject object = avQuery.get(objectId);
    // object 就是 id 为 558e20cbe4b060308e3eb36c 的 Todo 对象实例

比较查询

逻辑操作 AVQuery 方法
等于 equalTo
不等于 notEqualTo
大于 greaterThan
大于等于 greaterThanOrEqualTo
小于 lessThan
小于等于 lessThanOrEqualTo

利用上述表格介绍的逻辑操作的接口,我们可以很快地构建条件查询。

例如,查询优先级小于 2 的所有 Todo :

    query.whereLessThan("priority", 2);
每次查询默认最多返回 100 条符合条件的结果,要更改这一数值,请参考 [限定结果返回数量](#限定返回数量)。

以上逻辑用 SQL 语句表达为 select * from Todo where priority < 2。LeanStorage 也支持使用这种传统的 SQL 语句查询。具体使用方法请移步至 CQL 查询

查询优先级大于等于 2 的 Todo:

    query.whereGreaterThanOrEqualTo("priority", 2);

多个查询条件

当多个查询条件并存时,它们之间默认为 AND 关系,即查询只返回满足了全部条件的结果。建立 OR 关系则需要使用 组合查询

简单查询中,如果对一个对象的同一属性设置多个条件,那么先前的条件会被覆盖,查询只返回满足最后一个条件的结果。例如要找出优先级为 0 和 1 的所有 Todo,错误写法是:

    AVQuery<AVObject> query = new AVQuery<>("Todo");
    query.whereEqualTo("priority", 0);
    query.whereEqualTo("priority", 1);
    // 如果这样写,第二个条件将覆盖第一个条件,查询只会返回 priority = 1 的结果
    List<AVObject> todos = query.find();

正确作法是使用 组合查询 · OR 关系 来构建这种条件。

字符串查询

前缀查询类似于 SQL 的 LIKE 'keyword%' 条件。因为支持索引,所以该操作对于大数据集也很高效。

    // 找出开头是「早餐」的 Todo
    AVQuery<AVObject> query = new AVQuery<>("Todo");
    query.whereStartsWith("content", "早餐");

包含查询类似于 SQL 的 LIKE '%keyword%' 条件,比如查询标题包含「李总」的 Todo:

    AVQuery<AVObject> query = new AVQuery<>("Todo");
    query.whereContains("title", "李总");

不包含查询可以使用正则匹配查询的方式来实现。例如,查询标题不包含「机票」的 Todo:

    AVQuery query = new AVQuery<>("Todo");
    query.whereMatches("title","^((?!机票).)*&dollar;");
正则匹配查询**只适用于**字符串类型的数据。

但是基于正则的模糊查询有两个缺点:

  • 当数据量逐步增大后,查询效率将越来越低。
  • 没有文本相关性排序

因此我们推荐使用 应用内搜索 功能。它基于搜索引擎技术构建,提供更强大的搜索功能。

数组查询

当一个对象有一个属性是数组的时候,针对数组的元数据查询可以有多种方式。例如,在 数组 一节中我们为 Todo 设置了 reminders 属性,它就是一个日期数组,现在我们需要查询所有在 8:30 会响起闹钟的 Todo 对象:

    Date getDateWithDateString(String dateString) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = dateFormat.parse(dateString);
        return date;
    }

    void queryRemindersContains() {
        Date reminder = getDateWithDateString("2015-11-11 08:30:00");

        AVQuery<AVObject> query = new AVQuery<>("Todo");

        // equalTo: 可以找出数组中包含单个值的对象
        query.whereEqualTo("reminders", reminder);
    }

查询包含 8:30 和 9:30 这两个时间点响起闹钟的 Todo:

    Date getDateWithDateString(String dateString) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = dateFormat.parse(dateString);
        return date;
    }

    void queryRemindersWhereContainsAll() {
        Date reminder1 = getDateWithDateString("2015-11-11 08:30:00");
        Date reminder2 = getDateWithDateString("2015-11-11 09:30:00");

        AVQuery<AVObject> query = new AVQuery<>("Todo");
        query.whereContainsAll("reminders", Arrays.asList(reminder1, reminder2));

        List<AVObject> todos = query.find();
    }

注意这里是包含关系,假如有一个 Todo 会在 8:30、9:30 和 10:30 响起闹钟,它仍然是会被查询出来的。

查询「全不包含」的情况:

    query.whereNotContainedIn("reminders", Arrays.asList(reminder1, reminder2));

空值查询

假设用户可以有选择地为 Todo 上传图片来做标注,要想找出那些已有图片的 Todo:

关系查询

关联数据查询也可以通俗地理解为关系查询,关系查询在传统型数据库的使用中是很常见的需求,因此我们也提供了相关的接口来满足开发者针对关联数据的查询。

首先,我们需要明确关系的存储方式,再来确定对应的查询方式。

Pointer 查询

基于在 Pointer 小节介绍的存储方式:每一个 Comment 都会有一个 TodoFolder 与之对应,用以表示 Comment 属于哪个 TodoFolder。现在我已知一个 TodoFolder,想查询所有的 Comnent 对象,可以使用如下代码:

    AVQuery<AVObject> query = new AVQuery<>("Comment");
    query.whereEqualTo("targetTodoFolder", AVObject.createWithoutData("TodoFolder", "5590cdfde4b00f7adb5860c8"));

AVRelation 查询

假如用户可以给 TodoFolder 增加一个 Tag 选项,用以表示它的标签,而为了以后拓展 Tag 的属性,就新建了一个 Tag 对象,如下代码是创建 Tag 对象:

    AVObject tag = new AVObject("Tag");// 构建对象
    tag.put("name", "今日必做");// 设置名称
    tag.save();

而 Tag 的意义在于一个 TodoFolder 可以拥有多个 Tag,比如「家庭」(TodoFolder) 拥有的 Tag 可以是:今日必做、老婆吩咐、十分重要。实现创建「家庭」这个 TodoFolder 的代码如下:

    AVObject tag1 = new AVObject("Tag");// 构建对象
    tag1.put("name", "今日必做");// 设置 Tag 名称

    AVObject tag2 = new AVObject("Tag");// 构建对象
    tag2.put("name", "老婆吩咐");// 设置 Tag 名称

    AVObject tag3 = new AVObject("Tag");// 构建对象
    tag3.put("name", "十分重要");// 设置 Tag 名称

    AVObject todoFolder = new AVObject("TodoFolder");// 构建对象
    todoFolder.put("name", "家庭");// 设置 Todo 名称
    todoFolder.put("priority", 1);// 设置优先级

    AVRelation<AVObject> relation = todoFolder.getRelation("tags");
    relation.add(tag1);
    relation.add(tag2);
    relation.add(tag3);

    todoFolder.save();// 保存到云端

查询一个 TodoFolder 的所有 Tag 的方式如下:

    AVObject todoFolder = AVObject.createWithoutData("TodoFolder", "5661047dddb299ad5f460166");
    AVRelation<AVObject> relation = todoFolder.getRelation("tags");
    AVQuery<AVObject> query = relation.getQuery();
    List<AVObject> list = query.find(); // list 是一个 AVObject 的 List,它包含所有当前 todoFolder 的 tags

反过来,现在已知一个 Tag,要查询有多少个 TodoFolder 是拥有这个 Tag 的,可以使用如下代码查询:

    AVObject tag = AVObject.createWithoutData("Tag", "5661031a60b204d55d3b7b89");
    AVQuery<AVObject> query = new AVQuery<>("TodoFolder");
    query.whereEqualTo("tags", tag);
    List<AVObject> list = query.find(); // list 指的就是所有包含当前 tag 的 TodoFolder

关于关联数据的建模是一个复杂的过程,很多开发者因为在存储方式上的选择失误导致最后构建查询的时候难以下手,不但客户端代码冗余复杂,而且查询效率低,为了解决这个问题,我们专门针对关联数据的建模推出了一个详细的文档予以介绍,详情请阅读《数据模型设计指南》。

关联属性查询

正如在 Pointer 中保存 Comment 的 targetTodoFolder 属性一样,假如查询到了一些 Comment 对象,想要一并查询出每一条 Comment 对应的 TodoFolder 对象的时候,可以加上 include 关键字查询条件。同理,假如 TodoFolder 表里还有 pointer 型字段 targetAVUser 时,再加上一个递进的查询条件,形如 include(b.c),即可一并查询出每一条 TodoFolder 对应的 AVUser 对象。代码如下:

    AVQuery<AVObject> commentQuery = new AVQuery<>("Comment");
    commentQuery.orderByDescending("createdAt");
    commentQuery.limit(10);
    commentQuery.include("targetTodoFolder");// 关键代码,用 includeKey 告知服务端需要返回的关联属性对应的对象的详细信息,而不仅仅是 objectId
    List<AVObject> list = commentQuery.find(); // list 是最近的十条评论, 其 targetTodoFolder 字段也有相应数据
    for (AVObject comment : list) {
        // 并不需要网络访问
        AVObject todoFolder = comment.getAVObject("targetTodoFolder");
    }

此外需要格外注意的是,假设对象有一个 Array 类型的字段 todoArray 内部是 Pointer 类型:

[pointer1, pointer2, pointer3]

可以用 include 方法获取数组中的 pointer 数据,例如:

[query includeKey:@"todoArray"];
query.include("todoArray");
query.include('todoArray');

但是 Array 类型的 include 操作只支持到第一层,不支持 include(b.c) 这种递进关联查询。

select 也具备使用 dot 符号 . 来进行级联操作:

[query selectKeys:@"targetTodoFolder.targetAVUser.username"];
query.selectKeys(Arrays.asList("targetTodoFolder.targetAVUser.username"));
query.select(['targetTodoFolder.targetAVUser.username']);

内嵌查询

查询点赞超过 20 次的 TodoFolder 的 Comment 评论(注意查询针对的是 Comment),使用内嵌查询接口就可以通过一次查询来达到目的。

    // 构建内嵌查询
    AVQuery<AVObject> innerQuery = new AVQuery<>("TodoFolder");
    innerQuery.whereGreaterThan("likes", 20);
    // 将内嵌查询赋予目标查询
    AVQuery<AVObject> query = new AVQuery<>("Comment");
    // 执行内嵌操作
    query.whereMatchesQuery("targetTodoFolder", innerQuery);
    List<AVObject> list = query.find(); // list 就是符合超过 20 个赞的 TodoFolder 这一条件的 Comment 对象集合

    // 注意如果要做相反的查询可以使用
    query.whereDoesNotMatchQuery("targetTodoFolder", innerQuery);
    // 如此做将查询出 likes 小于或者等于 20 的 TodoFolder 的 Comment 对象

与普通查询一样,内嵌查询默认也最多返回 100 条记录,想修改这一默认请参考 限定结果返回数量

如果所有返回的记录没有匹配到外层的查询条件,那么整个查询也查不到结果。例如:

-- 找出积分高于 80、region 为 cn 的玩家记录
SELECT * 
FROM   player 
WHERE  NAME IN (SELECT NAME 
                FROM   gamescore 
                WHERE  score > 80) 
       AND region = 'cn' 

LeanCloud 云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌/子查询(和普通查询一样,limit 默认为 100,最大 1000),然后将子查询的结果填入主查询的对应位置,再执行主查询。

如果子查询匹配到了 100 条以上的记录(性别等区分度低的字段重复值往往较多),且主查询有其他查询条件(region = 'cn'),那么可能会出现没有结果或结果不全的情况,其本质上是子查询查出的 100 条记录没有满足主查询的其他条件。

我们建议采用以下方案进行改进:

  • 确保子查询的结果在 100 条以下,如果在 100 - 1000 条的话请在子查询末尾添加 limit 1000。
  • 将需要查询的字段冗余到主查询所在的表上;例如将 score 冗余到 Player 表上,或者将 region 添加到 GameScore 上然后只查 GameScore 表。
  • 进行多次查询,每次在子查询上添加 skip 来遍历所有记录(注意 skip 的值较大时可能会引发性能问题,因此不是很推荐)。

地理位置查询

地理位置查询是较为特殊的查询,一般来说,常用的业务场景是查询距离 xx 米之内的某个位置或者是某个建筑物,甚至是以手机为圆心,查找方圆多少范围内餐厅等等。LeanStorage 提供了一系列的方法来实现针对地理位置的查询。

查询位置附近的对象

Todo 的 whereCreated(创建 Todo 时的位置)是一个 AVGeoPoint 对象,现在已知了一个地理位置,现在要查询 whereCreated 靠近这个位置的 Todo 对象可以使用如下代码:

    AVQuery<AVObject> query = new AVQuery<>("Todo");
    AVGeoPoint point = new AVGeoPoint(39.9, 116.4);
    query.limit(10);
    query.whereNear("whereCreated", point);
    List<AVObject> listquery.find(); // 离这个位置最近的 10 个 Todo 对象

在上面的代码中,list 返回的是与 point 这一点按距离排序(由近到远)的对象数组。注意:如果在此之后又使用了 orderByAscendingorderByDescending 方法,则按距离排序会被新排序覆盖。

查询指定范围内的对象

要查找指定距离范围内的数据,可使用 whereWithinKilometerswhereWithinMileswhereWithinRadians 方法。 例如,我要查询距离指定位置,2 千米范围内的 Todo:

    query.whereWithinKilometers("whereCreated", point, 2.0);

注意事项

使用地理位置需要注意以下方面:

  • 每个 AVObject 数据对象中只能有一个 AVGeoPoint 对象的属性。
  • 地理位置的点不能超过规定的范围。纬度的范围应该是在 -90.090.0 之间,经度的范围应该是在 -180.0180.0 之间。如果添加的经纬度超出了以上范围,将导致程序错误。

组合查询

组合查询就是把诸多查询条件合并成一个查询,再交给 SDK 去云端查询。方式有两种:OR 和 AND。

OR 查询

OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级是大于等于 3 或者已经完成了的 Todo:

    final AVQuery<AVObject> priorityQuery = new AVQuery<>("Todo");
    priorityQuery.whereGreaterThanOrEqualTo("priority", 3);

    final AVQuery<AVObject> statusQuery = new AVQuery<>("Todo");
    statusQuery.whereEqualTo("status", 1);

    AVQuery<AVObject> query = AVQuery.or(Arrays.asList(priorityQuery, statusQuery));
    List<AVObject> todos = query.find(); // 返回 priority 大于等于3 或 status 等于 1 的 Todo

注意:OR 查询中,子查询中不能包含地理位置相关的查询。

AND 查询

AND 操作将满足了所有查询条件的对象返回给客户端。例如,找到创建于 2016-11-132016-12-02 之间的 Todo:

    Date getDateWithDateString(String dateString) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date date = dateFormat.parse(dateString);
        return date;
    }

    final AVQuery<AVObject> startDateQuery = new AVQuery<>("Todo");
    startDateQuery.whereGreaterThanOrEqualTo("createdAt", getDateWithDateString("2016-11-13"));

    final AVQuery<AVObject> endDateQuery = new AVQuery<>("Todo");
    endDateQuery.whereLessThan("createdAt", getDateWithDateString("2016-12-03"));

    AVQuery<AVObject> query = AVQuery.and(Arrays.asList(startDateQuery, endDateQuery));
    List<AVObject> list = query.find();

可以对新创建的 AVQuery 添加额外的约束,多个约束将以 AND 运算符来联接。

查询结果数量和排序

获取第一条结果

例如很多应用场景下,只要获取满足条件的一个结果即可,例如获取满足条件的第一条 Todo:

    AVQuery<AVObject> query = new AVQuery<>("Todo");
    query.whereEqualTo("priority",0);
    AVObject avObject = query.getFirst(); // object 就是符合条件的第一个 AVObject

限定返回数量

为了防止查询出来的结果过大,云端默认针对查询结果有一个数量限制,即 limit,它的默认值是 100。比如一个查询会得到 10000 个对象,那么一次查询只会返回符合条件的 100 个结果。limit 允许取值范围是 1 ~ 1000。例如设置返回 10 条结果:

    AVQuery<AVObject> query = new AVQuery<>("Todo");
    Date now = new Date();
    query.whereLessThanOrEqualTo("createdAt", now);//查询今天之前创建的 Todo
    query.limit(10);// 最多返回 10 条结果

跳过数量

设置 skip 这个参数可以告知云端本次查询要跳过多少个结果。将 skip 与 limit 搭配使用可以实现翻页效果,这在客户端做列表展现时,特别是在数据量庞大的情况下适合使用。例如,在翻页中每页显示数量为 10,要获取第 3 页的对象:

    AVQuery<AVObject> query = new AVQuery<>("Todo");
    Date now = new Date();
    query.whereLessThanOrEqualTo("createdAt", now);//查询今天之前创建的 Todo
    query.limit(10);// 最多返回 10 条结果
    query.skip(20);// 跳过 20 条结果

上述方法的执行效率比较低,因此不建议广泛使用。建议选用 createdAt 或者 updatedAt 这类的时间戳进行分段查询示例)。

返回指定属性/字段

通常列表展现的时候并不是需要展现某一个对象的所有属性,例如,Todo 这个对象列表一般展现的是 title 以及 content,在设置查询时可以告知云端需要返回的属性或字段有哪些,这样既满足需求又节省流量,还可以提高一部分的性能:

    AVQuery<AVObject> query = new AVQuery<>("Todo");
    query.selectKeys(Arrays.asList("title", "content"));
    List<AVObject> list = query.find();
    for (AVObject avObject : list) {
        String title = avObject.getString("title");
        String content = avObject.getString("content");

        // 如果访问没有指定返回的属性(key),则会报错,在当前这段代码中访问 location 属性就会报错
        String location = avObject.getString("location");
    }

所指定的属性或字段也支持 Pointer 类型。例如,获取 Todo 这个对象的所有者信息(owner 属性,Pointer 类型),仅展示这个所有者的 username:

统计总数量

通常用户在执行完搜索后,结果页面总会显示出诸如「搜索到符合条件的结果有 1020 条」这样的信息。例如,查询一下今天一共完成了多少条 Todo:

    AVQuery<AVObject> query = new AVQuery<>("Todo");
    query.whereEqualTo("status", 0);
    int i = query.count();
    // 查询成功,输出计数

排序

对于数字、字符串、日期类型的数据,可对其进行升序或降序排列。

    // 按时间,升序排列
    query.orderByAscending("createdAt");

    // 按时间,降序排列
    query.orderByDescending("createdAt");

一个查询可以附加多个排序条件,如按 priority 升序、createdAt 降序排列:

    query.addAscendingOrder("priority");
    query.addDescendingOrder("createdAt");

CQL 查询

CQL 是 LeanStorage 独创的使用类似 SQL 语法来实现云端查询功能的语言,具有 SQL 开发经验的开发者可以方便地使用此接口实现查询。

分别找出 status = 1 的全部 Todo 结果,以及 priority = 0 的 Todo 的总数:

    String cql = "select * from Todo where status = 1";
    AVCloudQueryResult avCloudQueryResult = AVQuery.doCloudQuery(cql);
    avCloudQueryResult.getResults();

    cql = "select count(*) from Todo where priority = 0";
    AVCloudQueryResult avCloudQueryResult = AVQuery.doCloudQuery(cql);
    avCloudQueryResult.getCount();

通常查询语句会使用变量参数,为此我们提供了与 Java JDBC 所使用的 PreparedStatement 占位符查询相类似的语法结构。

查询 status = 0、priority = 1 的 Todo:

    String cql = " select * from Todo where status = ? and priority = ?";
    AVCloudQueryResult avCloudQueryResult = AVQuery.doCloudQuery(cql, Arrays.asList(0, 1));

目前 CQL 已经支持数据的更新 update、插入 insert、删除 delete 等 SQL 语法,更多内容请参考 CQL 详细指南

缓存查询

缓存一些查询的结果到磁盘上,这可以让你在离线的时候,或者应用刚启动,网络请求还没有足够时间完成的时候可以展现一些数据给用户。当缓存占用了太多空间的时候,LeanStorage 会自动清空缓存。

默认情况下的查询不会使用缓存,除非你调用接口明确设置启用。例如,尝试从网络请求,如果网络不可用则从缓存数据中获取,可以这样设置:

缓存策略

为了满足多变的需求,SDK 默认提供了以下几种缓存策略:

Java SDK 不支持缓存策略。

缓存相关的操作

查询性能优化

影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。

  • 不等于和不包含查询(无法使用索引)
  • 通配符在前面的字符串查询(无法使用索引)
  • 有条件的 count(需要扫描所有数据)
  • skip 跳过较多的行数(相当于需要先查出被跳过的那些行)
  • 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引)
  • 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据)