Cursor based pagination with Spring Boot and MongoDB

Say goodbye to offset pagination with few lines of code

Image for post
Image for post
Photo by Pixabay on Pexels
@Data
@Document(collection = "tweets")
public class Tweet {
private ObjectId id;
private String author;
private String content;
private Long createdAt;
}
public interface TweetRepository extends MongoRepository<Tweet, ObjectId> {
}
@GetMapping(value = "/paged")
public Page<Tweet> getPaged(Pageable pageable) {
return tweetRepository.findAll(pageable);
}

A better approach: cursor based pagination (a.k.a. range queries)

Choosing the “bookmark”

Image from https://giphy.com/
db.tweets.find({
$or: [
{ author: { $gt: "def" } },
{ author: "def", _id: { $gt: "abc" } }
]})
.sort({ author: 1, _id: 1 })
.limit(11);

Trade off

[
{
"_id": "1",
"birthday": "2000/1/1",
"name": "abc"
},
{
"_id": "4",
"birthday": "2001/1/1",
"name": "def"
},
{
"_id": "2",
"birthday": "2020/1/1",
"name": "ghi"
},
{
"_id": "3",
"name": "jkl"
}
]
db.persons.find({})
.sort({ birthday: -1, _id: -1 })
.limit(4);
db.persons.find({
$or: [
{ birthday: { $lt: "2000/1/1" } },
{ birthday: "2000/1/1", _id: { $lt: "1" } }
]})
.sort({ birthday: -1, _id: -1 })
.limit(4);

Putting things together

<dependency>
<groupId>it.davidepedone</groupId>
<artifactId>spring-cursor-pagination-mongodb</artifactId>
<version>1.1.1</version>
</dependency>
public class TweetSearchFilter {

private String author;
// getter/setter
}
public class TweetPaginationService extends CursorPaginationService<Tweet, TweetSearchFilter> {

public TweetPaginationService(MongoOperations mongoOperations) {
super(mongoOperations, List.of("createdAt", "author"), Tweet.class);
}

@Override
public void configSearchQuery(Query query, TweetSearchFilter filter, Principal principal) {
Optional.ofNullable(filter.getAuthor()).ifPresent(a -> {
query.addCriteria(where("author").is(a));
});
}

}
@GetMapping
public ContinuationTokenSlice<Tweet> getAll(TweetSearchFilter tweetSearchFilter, CursorPageable cursorPageable) throws Exception {
return tweetPaginationService.executeQuery(tweetSearchFilter, cursorPageable);
}
{
"content": [
[....]
{
"id": "5ed6a45d0038622837ba60da",
"author": "xyz",
"content": "......",
"createdAt": 1590585400521
}
],
"continuationToken": "4LFv0w30RzTOTFRy2RzRz15Wu2in73AfwM00KFhqVKSxqF9-yDMAZG8Jx7NpDoUz6oo97Vjssum5k6h7nw9o0g==",
"numberOfElements": 10,
"size": 10
}
db.tweets.find({
$or: [
{ createdAt: { $lt: 1590585400521} },
{ createdAt: 1590585400521,
_id: { $lt: "5ed6a45d0038622837ba60da" }
}
]})
.sort({ createdAt: -1, _id: -1 })
.limit(10);

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store