Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
Java
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
Javachevron-right

Introduction to Data Pagination With Quarkus and MongoDB: A Comprehensive Tutorial

Otavio Santana7 min read • Published Apr 25, 2024 • Updated Apr 25, 2024
QuarkusMongoDBJava
FULL APPLICATION
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty

Introduction

In modern web development, managing large datasets efficiently through APIs is crucial for enhancing application performance and user experience. This tutorial explores pagination techniques using Quarkus and MongoDB, a robust combination for scalable data delivery. Through a live coding session, we'll delve into different pagination methods and demonstrate how to implement these in a Quarkus-connected MongoDB environment. This guide empowers developers to optimize REST APIs for effective data handling.
You can find all the code presented in this tutorial in the GitHub repository:
1git clone git@github.com:mongodb-developer/quarkus-pagination-sample.git

Prerequisites

For this tutorial, you'll need:
  • Java 21.
  • Maven.
  • A MongoDB cluster.
    • MongoDB Atlas (Option 1)
    • Docker (Option 2)
You can use the following Docker command to start a standalone MongoDB instance:
1docker run --rm -d --name mongodb-instance -p 27017:27017 mongo
Or you can use MongoDB Atlas and try the M0 free tier to deploy your cluster.

Create a Quarkus project

  • Configure your project by selecting the desired options, such as the group and artifact ID.
  • Add the necessary dependencies to your project. For this tutorial, we will add:
    • JNoSQL Document MongoDB [quarkus-jnosql-document-mongodb].
    • RESTEasy Reactive [quarkus-resteasy-reactive].
    • RESTEasy Reactive Jackson [quarkus-resteasy-reactive-jackson].
    • OpenAPI [quarkus-smallrye-openapi].
Note: If you cannot find some dependencies, you can add them manually in the pom.xml. See the file below.
  • Generate the project, download the ZIP file, and extract it to your preferred location. Remember that the file structure may vary with different Quarkus versions, but this should be fine for the tutorial. The core focus will be modifying the pom.xml file and source code, which remains relatively consistent across versions. Any minor structural differences should be good for your progress, and you can refer to version-specific documentation if needed for a seamless learning experience.
At this point, your pom.xml file should look like this:
1<dependencies>
2 <dependency>
3 <groupId>io.quarkus</groupId>
4 <artifactId>quarkus-smallrye-openapi</artifactId>
5 </dependency>
6 <dependency>
7 <groupId>io.quarkiverse.jnosql</groupId>
8 <artifactId>quarkus-jnosql-document-mongodb</artifactId>
9 <version>3.3.0</version>
10 </dependency>
11 <dependency>
12 <groupId>io.quarkus</groupId>
13 <artifactId>quarkus-resteasy</artifactId>
14 </dependency>
15 <dependency>
16 <groupId>io.quarkus</groupId>
17 <artifactId>quarkus-resteasy-jackson</artifactId>
18 </dependency>
19 <dependency>
20 <groupId>io.quarkus</groupId>
21 <artifactId>quarkus-arc</artifactId>
22 </dependency>
23 <dependency>
24 <groupId>io.quarkus</groupId>
25 <artifactId>quarkus-junit5</artifactId>
26 <scope>test</scope>
27 </dependency>
28 <dependency>
29 <groupId>io.rest-assured</groupId>
30 <artifactId>rest-assured</artifactId>
31 <scope>test</scope>
32 </dependency>
33</dependencies>
We will work with the latest version of Quarkus alongside Eclipse JNoSQL Lite, a streamlined integration that notably does not rely on reflection. This approach enhances performance and simplifies the configuration process, making it an optimal choice for developers looking to maximize efficiency in their applications.

Database configuration

Before you dive into the implementation, it's essential to configure your MongoDB database properly. In MongoDB, you must often set up credentials and specific configurations to connect to your database instance. Eclipse JNoSQL provides a flexible configuration mechanism that allows you to manage these settings efficiently.
You can find detailed configurations and setups for various databases, including MongoDB, in the Eclipse JNoSQL GitHub repository.
To run your application locally, you can configure the database name and properties in your application's application.properties file. Open this file and add the following line to set the database name:
1quarkus.mongodb.connection-string = mongodb://localhost
2jnosql.document.database = fruits
This configuration will enable your application to:
  • Use the "fruits" database.
  • Connect to the MongoDB cluster available at the provided connection string.
In production, make sure to enable access control and enforce authentication. See the security checklist for more details.
It's worth mentioning that Eclipse JNoSQL leverages Eclipse MicroProfile Configuration, which is designed to facilitate the implementation of twelve-factor applications, especially in configuration management. It means you can override properties through environment variables, allowing you to switch between different configurations for development, testing, and production without modifying your code. This flexibility is a valuable aspect of building robust and easily deployable applications.
Now that your database is configured, you can proceed with the tutorial and create your RESTful API with Quarkus and Eclipse JNoSQL for MongoDB.

Create a fruit entity

In this step, we will create a simple Fruit entity using Java records. Create a new class in the src/main/java directory named Fruit.
1import jakarta.nosql.Column;
2import jakarta.nosql.Convert;
3import jakarta.nosql.Entity;
4import jakarta.nosql.Id;
5import org.eclipse.jnosql.databases.mongodb.mapping.ObjectIdConverter;
6
7@Entity
8public class Fruit {
9
10 @Id
11 @Convert(ObjectIdConverter.class)
12 private String id;
13
14 @Column
15 private String name;
16
17 public String getId() {
18 return id;
19 }
20
21 public void setId(String id) {
22 this.id = id;
23 }
24
25 public String getName() {
26 return name;
27 }
28
29 public void setName(String name) {
30 this.name = name;
31 }
32
33 @Override
34 public String toString() {
35 return "Fruit{" +
36 "id='" + id + '\'' +
37 ", name='" + name + '\'' +
38 '}';
39 }
40
41 public static Fruit of(String name) {
42 Fruit fruit = new Fruit();
43 fruit.setName(name);
44 return fruit;
45 }
46
47}

Create a fruit repository

We will simplify the integration between Java and MongoDB using the Jakarta Data repository by creating an interface that extends NoSQLRepository. The framework automatically implements this interface, enabling us to define methods for data retrieval that integrate seamlessly with MongoDB. We will focus on implementing two types of pagination: offset pagination represented by Page and keyset (cursor) pagination represented by CursoredPage.
Here's how we define the FruitRepository interface to include methods for both pagination strategies:
1import jakarta.data.Sort;
2import jakarta.data.page.CursoredPage;
3import jakarta.data.page.Page;
4import jakarta.data.page.PageRequest;
5import jakarta.data.repository.BasicRepository;
6import jakarta.data.repository.Find;
7import jakarta.data.repository.OrderBy;
8import jakarta.data.repository.Repository;
9
10@Repository
11public interface FruitRepository extends BasicRepository<Fruit, String> {
12
13 @Find
14 CursoredPage<Fruit> cursor(PageRequest pageRequest, Sort<Fruit> order);
15
16 @Find
17 @OrderBy("name")
18 Page<Fruit> offSet(PageRequest pageRequest);
19
20 long countBy();
21
22}

Create setup

We'll demonstrate how to populate and manage the MongoDB database with a collection of fruit entries at the start of the application using Quarkus. We'll ensure our database is initialized with predefined data, and we'll also handle cleanup on application shutdown. Here's how we can structure the SetupDatabase class:
1import jakarta.enterprise.context.ApplicationScoped;
2
3import jakarta.enterprise.event.Observes;
4
5import io.quarkus.runtime.ShutdownEvent;
6import io.quarkus.runtime.StartupEvent;
7import org.jboss.logging.Logger;
8
9import java.util.List;
10
11@ApplicationScoped
12public class SetupDatabase {
13
14 private static final Logger LOGGER = Logger.getLogger(SetupDatabase.class.getName());
15
16 private final FruitRepository fruitRepository;
17
18 public SetupDatabase(FruitRepository fruitRepository) {
19 this.fruitRepository = fruitRepository;
20 }
21
22
23 void onStart(@Observes StartupEvent ev) {
24 LOGGER.info("The application is starting...");
25 long count = fruitRepository.countBy();
26 if (count > 0) {
27 LOGGER.info("Database already populated");
28 return;
29 }
30 List<Fruit> fruits = List.of(
31 Fruit.of("apple"),
32 Fruit.of("banana"),
33 Fruit.of("cherry"),
34 Fruit.of("date"),
35 Fruit.of("elderberry"),
36 Fruit.of("fig"),
37 Fruit.of("grape"),
38 Fruit.of("honeydew"),
39 Fruit.of("kiwi"),
40 Fruit.of("lemon")
41 );
42 fruitRepository.saveAll(fruits);
43 }
44
45 void onStop(@Observes ShutdownEvent ev) {
46 LOGGER.info("The application is stopping...");
47 fruitRepository.deleteAll(fruitRepository.findAll().toList());
48 }
49
50}

Create a REST API

Now, let's create a RESTful API to manage developer records. Create a new class in src/main/java named FruitResource.
1import jakarta.data.Sort;
2import jakarta.data.page.PageRequest;
3import jakarta.ws.rs.DefaultValue;
4import jakarta.ws.rs.GET;
5import jakarta.ws.rs.Path;
6import jakarta.ws.rs.Produces;
7import jakarta.ws.rs.QueryParam;
8import jakarta.ws.rs.core.MediaType;
9
10@Path("/fruits")
11public class FruitResource {
12
13 private final FruitRepository fruitRepository;
14
15 private static final Sort<Fruit> ASC = Sort.asc("name");
16 private static final Sort<Fruit> DESC = Sort.asc("name");
17
18 public FruitResource(FruitRepository fruitRepository) {
19 this.fruitRepository = fruitRepository;
20 }
21
22 @Path("/offset")
23 @GET
24 @Produces(MediaType.APPLICATION_JSON)
25 public Iterable<Fruit> hello(@QueryParam("page") @DefaultValue("1") long page,
26 @QueryParam("size") @DefaultValue("2") int size) {
27 var pageRequest = PageRequest.ofPage(page).size(size);
28 return fruitRepository.offSet(pageRequest).content();
29 }
30
31 @Path("/cursor")
32 @GET
33 @Produces(MediaType.APPLICATION_JSON)
34 public Iterable<Fruit> cursor(@QueryParam("after") @DefaultValue("") String after,
35 @QueryParam("before") @DefaultValue("") String before,
36 @QueryParam("size") @DefaultValue("2") int size) {
37 if (!after.isBlank()) {
38 var pageRequest = PageRequest.ofSize(size).afterCursor(PageRequest.Cursor.forKey(after));
39 return fruitRepository.cursor(pageRequest, ASC).content();
40 } else if (!before.isBlank()) {
41 var pageRequest = PageRequest.ofSize(size).beforeCursor(PageRequest.Cursor.forKey(before));
42 return fruitRepository.cursor(pageRequest, DESC).stream().toList();
43 }
44 var pageRequest = PageRequest.ofSize(size);
45 return fruitRepository.cursor(pageRequest, ASC).content();
46 }
47
48}

Test the REST API

Now that we've created our RESTful API for managing developer records, it's time to put it to the test. We'll demonstrate how to interact with the API using various HTTP requests and command-line tools.

Start the project

1./mvnw compile quarkus:dev

Exploring pagination with offset

We will use curl to learn more about pagination using the URLs provided. It is a command-line tool that is often used to send HTTP requests. The URLs you have been given are used to access a REST API endpoint fetching fruit pages using offset pagination. Each URL requests a different page, enabling us to observe how pagination functions via the API. Below is how you can interact with these endpoints using the curl tool.

Fetching the first page

This command requests the first page of fruits from the server.
1curl --location http://localhost:8080/fruits/offset?page=1

Fetching the second page

This command gets the next set of fruits, which is the second page.
1curl --location http://localhost:8080/fruits/offset?page=2

Fetching the fifth page

By requesting the fifth page, you can see how the API responds when you request a page that might be beyond the range of existing data.
1curl --location http://localhost:8080/fruits/offset?page=5

Exploring pagination with a cursor

To continue exploring cursor-based pagination with your API, using both after and before parameters provides a way to navigate through your dataset forward and backward respectively. This method allows for flexible data retrieval, which can be particularly useful for interfaces that allow users to move to the next or previous set of results. Here's how you can structure your curl commands to use these parameters effectively:

Fetching the initial set of fruits

This command gets the first batch of fruits without specifying a cursor, starting from the beginning.
1curl --location http://localhost:8080/fruits/cursor

Fetching fruits after "banana"

This command fetches the list of fruits that appear after "banana" in your dataset. This is useful for moving forward in the list.
1curl --location http://localhost:8080/fruits/cursor?after=banana

Fetching fruits before "date"

This command is used to go back to the set of fruits that precede "date" in the dataset. This is particularly useful for implementing "Previous" page functionality.
1curl --location http://localhost:8080/fruits/cursor?before=date

Conclusion

This tutorial explored the fundamentals and implementation of pagination using Quarkus and MongoDB, demonstrating how to manage large datasets in web applications effectively. By integrating the Jakarta Data repository with Quarkus, we designed interfaces that streamline the interaction between Java and MongoDB, supporting offset and cursor-based pagination techniques. We started by setting up a basic Quarkus application and configuring MongoDB connections. Then, we demonstrated how to populate the database with initial data and ensure clean shutdown behavior.
Throughout this tutorial, we've engaged in live coding sessions, implementing and testing various pagination methods. We've used the curl command to interact with the API, fetching data with no parameters, and using after and before parameters to navigate through the dataset forward and backward. The use of cursor-based pagination, in particular, has showcased its benefits in scenarios where datasets are frequently updated or when precise data retrieval control is needed. This approach not only boosts performance by avoiding the common issues of offset pagination but also provides a user-friendly way to navigate through data.
Ready to explore the benefits of MongoDB Atlas? Get started now by trying MongoDB Atlas.
Access the source code used in this tutorial.
Any questions? Come chat with us in the MongoDB Community Forum.
Top Comments in Forums
There are no comments on this article yet.
Start the Conversation

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Article

How to Optimize Java Performance With Virtual Threads, Reactive Programming, and MongoDB


Aug 29, 2024 | 5 min read
Tutorial

Schema Performance Evaluation in MongoDB Using PerformanceBench


Apr 02, 2024 | 20 min read
Quickstart

Getting Started with MongoDB and Java - CRUD Operations Tutorial


Mar 01, 2024 | 24 min read
Article

Java 21: Unlocking the Power of the MongoDB Java Driver With Virtual Threads


Jan 31, 2024 | 2 min read
Table of Contents