mongodb 没有官方的游标滚动实现深度分页功能,建议的都是选择出一个字段,如_id,然后每次查询时限制该字段,而不进行分页处理。
也没有看到更优的实现方式,本文做一个大胆的假设,自行实现滚动分页功能。供大家思路参考。
但是猜想可以自行实现一个,简单思路就是,第一次查询时不带limit进行查询全量数据,然后自己通过cursor迭代出需要的行数后返回调用端,下次再调用时,直接取出上一次的cursor,再迭代limit的数量返回。
(资料图片)
优势是只需计算一次,后续就直接复用结果即可。该功能需要有mongodb的clientSession功能支持。
但是需要复杂的自己维护cursor实例,打开、关闭、过期等。稍微管理不好,可能就客户端内存泄漏或者mongo server内存泄漏。
实践步骤:
1. 引入mongo 驱动:
org.mongodb mongodb-driver-sync 4.4.2 org.mongodb mongodb-driver-core 4.4.2 org.mongodb bson 4.4.2
注意版本不匹配问题,所以要引入多个包。
2. 创建测试类:
验证接入mongo无误,且造入适量的数据。
import static com.mongodb.client.model.Filters.eq;import com.mongodb.ConnectionString;import com.mongodb.MongoClientSettings;import com.mongodb.WriteConcern;import com.mongodb.client.*;import com.mongodb.client.result.InsertOneResult;import org.bson.Document;import org.junit.Before;import org.junit.Test;import org.openjdk.jmh.annotations.Setup;public class MongoQuickStartTest { private MongoClient mongoClient; @Before public void setup() { // Replace the placeholder with your MongoDB deployment"s connection string String uri = "mongodb://localhost:27017"; MongoClientSettings options = MongoClientSettings.builder() .applyConnectionString(new ConnectionString(uri)) .writeConcern(WriteConcern.W1).build(); mongoClient = MongoClients.create(options); } @Test public void testFind() {// ConnectionString connectionString = new ConnectionString("mongodb://localhost:27017");// MongoClient mongoClient = MongoClients.create(connectionString); // Replace the placeholder with your MongoDB deployment"s connection string MongoDatabase database = mongoClient.getDatabase("local"); MongoCollectioncollection = database.getCollection("test01"); Document doc = collection.find(eq("name", "zhangsan1")).first(); if (doc != null) { System.out.println(doc.toJson()); } else { System.out.println("No matching documents found."); } } @Test public void testInsert() { Document body = new Document(); long startId = 60011122212L; MongoDatabase database = mongoClient.getDatabase("local"); MongoCollection collection = database.getCollection("test01"); int i; for (i = 0; i < 500000; i++) { String id = (startId + i) + ""; body.put("_id", id); body.put("name", "name_" + id); body.put("title", "title_" + id); InsertOneResult result = collection.insertOne(body); } System.out.println("insert " + i + " rows"); }}
3. 创建cursor的查询实现类并调用
基于springboot创建 controller进行会话测试,使用一个固定的查询语句进行分页测试。
import com.mongodb.ConnectionString;import com.mongodb.MongoClientSettings;import com.mongodb.WriteConcern;import com.mongodb.client.*;import org.bson.Document;import org.springframework.stereotype.Service;import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;@Servicepublic class MongoDbService { private MongoClient mongoClient; // 所有游标容器,简单测试,真正的管理很复杂 private Map> cursorHolder = new ConcurrentHashMap<>(); public void ensureMongo() { // Replace the placeholder with your MongoDB deployment"s connection string String uri = "mongodb://localhost:27017"; MongoClientSettings options = MongoClientSettings.builder() .applyConnectionString(new ConnectionString(uri)) .writeConcern(WriteConcern.W1).build(); mongoClient = MongoClients.create(options); } // 特殊实现的 cursor 滚动查询 public List findDataWithCursor(String searchAfter, int limit) { ensureMongo(); MongoDatabase database = mongoClient.getDatabase("local"); MongoCollection collection = database.getCollection("test01"); List resultList = new ArrayList<>(); MongoCursor cursor = cursorHolder.get(searchAfter); if(cursor == null) { // 第一次取用需要查询,后续直接复用cursor即可 cursor = collection.find().sort(new Document("name", 1)).iterator(); cursorHolder.put(searchAfter, cursor); } int i = 0; // 自行计数,到达后即返回前端 while (cursor.hasNext()) { resultList.add(cursor.next()); if(++i >= limit) { break; } } if(!cursor.hasNext()) { cursor.close(); cursorHolder.remove(searchAfter); } return resultList; }}
应用调用controller:
@Resource private MongoDbService mongoDbService; @GetMapping("/mongoPageScroll") @ResponseBody public Object mongoPageScroll(@RequestParam(required = false) String params, @RequestParam String scrollId) { return mongoDbService.findDataWithCursor(scrollId, 9); }
测试方式,访问接口:http://localhost:8080/hello/mongoPageScroll?scrollId=c,然后反复调用(下一页)。
如此,只要前端第一次查询时,不存在cursor就创建,后续就直接使用原来的结果。第一次可能慢,第二次就很快了。
结论,是可以简单实现的,但是生产不一定能用。因为,如何管理cursor,绝对是个超级复杂的事,何时打开,何时关闭,超时处理,机器宕机等,很难解决。