java - 游标翻页

本文探讨了普通翻页在大数据量下存在的性能问题,介绍了深翻页的概念,随后引入游标翻页作为解决方案。作者详细解释了如何利用游标和索引来提高查询效率,并给出了实现游标翻页的代码示例和一个工具类的设计,以简化未来使用过程。
摘要由CSDN通过智能技术生成

普通翻页

相信大家平时接触使用最多的翻页方式就是普通翻页,包括我自己在内,在没接触游标翻页之前,也都是使用的普通翻页。普通翻页的sql语句如下:

select * from table where limit 10,10;

熟悉mysql的应该知道,limit语句的作用是:第一个参数,表示跳过的查询数,第二个表示要查询的数量,上述语句表示从table中查询11到20的十条数据。
大家可以想象,如果在项目中数据量比较大,那么,查询效率会非常低,比如,如果现在要查询10001到10010的十条数据,那么sql语句就为:

select * from table where limit 10000, 10;

上述语句表示跳过前一万条数据,只取10001到10010的十条数据,那么前一万条数据就是无用数据,那么扫描这一万条数据的时间就是白白浪费了。

深翻页问题

其实,上面的描述已经差不多介绍了深翻页问题,这里再提出来说一下,加深大家印象。
当翻页进行到一定程度,比如翻到1000页时,此时,对应的sql语句:

select * from table where id > 1 limit 10000, 10;

出现了上述普通翻页出现的问题,此时mysql会丢弃掉前面的10000条数据,只取后面的10条数据,降低性能。

游标翻页

然普通翻页会出现深翻页问题,那么有没有解决方案呢?答案是肯定的。游标翻页就可以解决这一问题。
细想一下,出现上述问题的主要原因是什么?是因为要白白浪费时间去查很多无关数据。那么我们直接跳过前面的无关数据是不是就可以提高查询效率了。
比如,我们现在希望查询10001到100010的数据,那么我们希望直接跳过前面的10000条数据,然后返回10001到10010十条数据,怎么能让mysql快速跳过呢?看下面的sql语句:

select * from table where id > 10000 limit 0, 10;

上述sql语句中,快速地跳过了前面10000条数据(id <= 10000的数据),然后limit表示跳过0条数据,直接取10001开始的10条数据,这样就省去了很多查询无关数据的时间,提升了数据库的查询性。这里的10000就是游标。
注意:一般来说,对于游标都要加索引,才能快速定位到相应的位置,不然,使用游标翻页依旧是一个非常耗时的操作。
由于,mysql中对于表的主键id默认是索引,所以上面的sql语句会很快地定位到id为10000的数据。

实现游标翻页

说了这么多,怎么实现游标翻页呢?我们需要定义游标的基本请求类和基本响应类。
基本请求类:

@Data
@ApiModel("游标翻页请求")
@AllArgsConstructor
@NoArgsConstructor
public class CursorPageBaseReq<T> {

    @ApiModelProperty("页面大小")
    @Min(0)
    @Max(100)
    private Integer pageSize = 10;

    @ApiModelProperty("游标(初始为null,后续请求附带上次翻页的游标)")
    private String cursor;

    /**
    * 直接根据当前类中的pageSize信息生成一个page类,方便后面的查询
    */
    public Page plusPage() {
        return new Page(1, this.pageSize, false);
    }
}

前端每次只需要发送游标和每页的大小即可。
基本响应类:

@Data
@ApiModel("游标翻页返回")
@AllArgsConstructor
@NoArgsConstructor
public class CursorPageBaseResp<T> {

    @ApiModelProperty("游标(下次翻页带上这参数)")
    private String cursor;

    @ApiModelProperty("是否最后一页")
    private Boolean isLast = Boolean.FALSE;

    @ApiModelProperty("数据列表")
    private List<T> list;
}

需要每次给前端返回查询完之后的新的游标,以便前端下次请求时携带游标,还需要返回给前端此次返回的数据是不是最后一页。当然最重要的是要返回数据。

看看具体使用的代码,清晰一下流程:

    	LambdaQueryChainWrapper<UserFriend> wrapper = lambdaQuery();
        //游标字段(id)(快速定位索引位置)
        wrapper.lt(UserFriend::getId,cursorPageBaseReq.getCursor());
        //游标方向,降序查询
        wrapper.orderByDesc(UserFriend::getId);
        //额外查询条件
        wrapper.eq(UserFriend::getUid,uid);
        //游标翻页结果
        Page<UserFriend> page = page(cursorPageBaseReq.plusPage(), wrapper);
        //计算游标位置
        String cursor = Optional.ofNullable(CollectionUtil.getLast(page.getRecords()))
                .map(UserFriend::getId)
                .map(String::valueOf)
                .orElse(null);
        //是否最后一页判断
        Boolean isLast = page.getRecords().size() != cursorPageBaseReq.getPageSize();
        return new CursorPageBaseResp<>(cursor, isLast, page.getRecords());

相信从上述代码可以清晰地知道游标翻页的流程了,

  1. 首先完善查询器LambdaQueryChainWrapper,其中包括了游标信息,查询条件,
  2. 然后通过mybatisplus中的方法直接分页请求,拿到分页的结果
  3. 然后更新游标位置,返回给前端,以便前端下次请求时携带
  4. 判断当前数据是否是最后一页

这是一个完整的游标翻页的流程,非常清晰。
但是,如果每当有一个游标翻页请求时,我们都需要写一遍上述代码,会不会太臃肿了。答案也是肯定的。
所以,这促使我们去编写一个工具类,将每次使用游标翻页时重复使用到的代码进行抽象,对于不同场景的翻页需要的参数,从业务传参即可。那么我们现在就来将上述完整的游标翻页的代码进行抽象,封装为一个工具类。

游标翻页工具类

要将上述代码抽象成一个工具类,需要找出代码中不需要业务场景传参的地方
在这里插入图片描述
如图,红色框中是各种业务场景中公共的部分,黄色框中的额外查询条件是需要从业务场景中传参过来的,另外,不同场景的游标也是不一样的,所以额外的查询条件和游标是需要从业务场景中传过来的。另外,要使用mybatisplus中的查询方法,就要传入Iservice接口。当然必不可少的游标翻页当然需要游标相关信息,即游标的基本请求类,这样一分析,工具类的参数就已经确定了:

  1. Isevice接口
  2. 游标翻页基本请求类
  3. 额外的查询条件
  4. 游标

除了第三个参数,其余三个参数都很好处理。我们怎么传入额外的查询条件呢?这里可以借助函数式接口,consumer接口就很适合,consumer接口只有入参,没有返回值。可以通过consumer的参数来设置额外的查询条件。我也是从其他地方学习到的这样一种思路,感觉非常的奇妙。
至此,游标翻页工具类的四个参数就已经确定好了,这里给出游标翻页的工具类的代码:

  public static <T> CursorPageBaseResp<T> getCursorPageByMysql(IService<T> mapper, CursorPageBaseReq request, Consumer<LambdaQueryWrapper<T>> initWrapper, SFunction<T, ?> cursorColumn) {
        //游标字段类型
        Class<?> cursorType = LambdaUtils.getReturnType(cursorColumn);
        LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
        //给查询器初始化额外条件
        initWrapper.accept(wrapper);
        //游标条件
        if (StrUtil.isNotBlank(request.getCursor())) {
            wrapper.lt(cursorColumn, parseCursor(request.getCursor(), cursorType));
        }
        //游标方向
        wrapper.orderByDesc(cursorColumn);

        Page<T> page = mapper.page(request.plusPage(), wrapper);
        //取出游标
        String cursor = Optional.ofNullable(CollectionUtil.getLast(page.getRecords()))
                .map(cursorColumn)
                .map(CursorUtils::toCursor)
                .orElse(null);
        //判断是否最后一页
        Boolean isLast = page.getRecords().size() != request.getPageSize();
        return new CursorPageBaseResp<>(cursor, isLast, page.getRecords());
    }

怎么样?代码跟没抽象时的完整代码是不是非常相似,封装成代码后,将来要想实现游标翻页就很轻松了。只需要调用方法,然后传入参数即可。
使用工具类的示例代码:

@Service
public class UserFriendDao extends ServiceImpl<UserFriendMapper, UserFriend> {


    public CursorPageBaseResp<UserFriend> getFriendPage(long uid, CursorPageBaseReq request) {
        return CursorUtils.getCursorPageByMysql(this, request, wrapper -> wrapper.eq(UserFriend::getUid, uid), UserFriend::getId);
    }
}

参数解释:

  1. this:传入工具类中定义的Iservice类型的接口,因为ServiceImpl是实现了Iservice接口的,所以可以直接将本类作为第一个参数传入
  2. 就是游标翻页的基本请求
  3. 这第三个参数就是一个consumer接口,wrapper -> wrapper.eq(UserFriend::getUid, uid)这行代码就是实现了consumer接口,具体功能就是在查询器中初始化额外查询条件。

工具类中的这两行代码就是将wrapper传入consumer接口,然后将其初始化为wrapper.eq(UserFriend::getUid, uid)即额外条件,如果还有其他的查询条件,只需要给wrapper设置查询条件即可。

//给查询器初始化额外条件
LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
initWrapper.accept(wrapper);
  1. 就是给游标工具类设置游标字段。

这样,以后如果需要使用游标翻页,只需要调用工具类中的方法即可。

  • 9
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Java与MongoDB集成的方式有很多,其中一种常用的方法是使用MongoDB的Java驱动程序来进行游标查询。 游标查询是一种在MongoDB中用于获取大量数据的一种方式。与传统的查询方式不同,游标查询并不是一次性获取全部数据,而是按需获取数据,可以减轻对内存的开销。 在Java中使用游标查询,首先需要导入MongoDB的Java驱动程序包。然后,我们可以使用以下代码示例来进行游标查询: ``` // 创建MongoDB链接 MongoClient mongoClient = new MongoClient("localhost", 27017); // 获取数据库 MongoDatabase database = mongoClient.getDatabase("myDatabase"); // 获取集合 MongoCollection<Document> collection = database.getCollection("myCollection"); // 创建查询 Document query = new Document("name", "John"); // 执行查询 FindIterable<Document> cursor = collection.find(query); // 遍历游标结果 for (Document document : cursor) { System.out.println(document); } // 关闭游标和连接 cursor.close(); mongoClient.close(); ``` 在上面的代码中,我们首先创建了一个MongoDB的链接,然后获取了要查询的数据库和集合。接着,我们创建了一个查询文档,并通过`collection.find(query)`方法执行查询,得到一个游标。然后,我们可以使用`for`循环遍历游标的结果,并对每个结果进行相应的处理。最后,我们需要手动关闭游标和关闭MongoDB的链接。 游标查询是一种非常灵活和高效的查询方式,可以处理大量的数据。但是需要注意的是,在使用游标查询时,要避免返回大量的数据,以免对内存造成过大的压力。对于需要返回大量数据的情况,可以考虑使用分页查询的方式来进行处理。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值