Tutorial: Paginate Data with Java and Quarkus
For the complete MongoDB documentation index, see www.mongodb.com/docs/llms.txt
Tutorial: Paginate Data with Java and Quarkus
Overview
This tutorial shows you how to implement pagination techniques in a Quarkus application connected to MongoDB. You learn how to use the Jakarta Data repository to create REST API endpoints that support both offset-based and cursor-based pagination methods.
Pagination
Pagination is a technique used to divide large datasets into smaller, more manageable chunks. This tutorial implements offset-based and cursor-based pagination methods. Offset-based pagination uses page numbers to retrieve specific subsets of data, while cursor-based pagination uses a reference point, or a cursor, to navigate through the dataset.
Tutorial
This tutorial shows how to perform the following actions:
-
Verify the prerequisites
-
Create a Quarkus project with the required dependencies
-
Configure the MongoDB connection
-
Define a data entity and repository
-
Implement REST API endpoints for pagination
-
Test the pagination endpoints
-
Verify the prerequisites.
Before you begin, complete the following prerequisite tasks:
-
[Install Java 21](https://www.oracle.com/java/technologies/downloads/#java21
-
Configure a MongoDB cluster, either on MongoDB Atlas or a local Docker instance
To start a local MongoDB instance with Docker, run the following command:
bash
docker run --rm -d --name mongodb-instance -p 27017:27017 mongo
Alternatively, you can use MongoDB Atlas and deploy a free M0 cluster. To learn how to create an Atlas account and cluster, see the [MongoDB Get Started](https://www.mongodb.com/docs/get-started.md#std-label-unified-get-started guide.
- Create a Quarkus project.
Navigate to the Quarkus Code Generator and configure your project with the following settings:
Select your preferred group and artifact ID.
Add the following dependencies:
-
JNoSQL Document MongoDB (
quarkus-jnosql-document-mongodb) -
RESTEasy Reactive (
quarkus-resteasy-reactive) -
RESTEasy Reactive Jackson (
quarkus-resteasy-reactive-jackson) -
OpenAPI (
quarkus-smallrye-openapi)
Generate the project, download the ZIP file, and extract it.
Note:
If you cannot find a dependency in the generator, add it manually to the pom.xml file.
After you complete the setup, verify that your pom.xml file includes the following dependencies:
xml
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.jnosql</groupId>
<artifactId>quarkus-jnosql-document-mongodb</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- Configure the MongoDB database.
Open the application.properties file and add the following configuration properties to connect to your MongoDB instance:
none
quarkus.mongodb.connection-string = <your connection string>
jnosql.document.database = fruits
This configuration enables your application to connect to the MongoDB cluster at the specified connection string and use the fruits database.
Important:
In production environments, enable access control and enforce authentication. For more information, see the Security Checklist.
You can override these properties by using environment variables, which allow you to specify different configurations for development, testing, and production without modifying your code.
- Create a data entity.
Create a Fruit entity class in the src/main/java directory. The following code defines the entity with id and name fields:
```java import jakarta.nosql.Column; import jakarta.nosql.Convert; import jakarta.nosql.Entity; import jakarta.nosql.Id; import org.eclipse.jnosql.databases.mongodb.mapping.ObjectIdConverter;
@Entity public class Fruit {
@Id
@Convert(ObjectIdConverter.class)
private String id;
@Column
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Fruit{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
public static Fruit of(String name) {
Fruit fruit = new Fruit();
fruit.setName(name);
return fruit;
}
}
```
- Create a repository interface.
Create a FruitRepository interface that extends the BasicRepository class. The following code defines methods for both offset and cursor-based pagination:
```java import jakarta.data.Sort; import jakarta.data.page.CursoredPage; import jakarta.data.page.Page; import jakarta.data.page.PageRequest; import jakarta.data.repository.BasicRepository; import jakarta.data.repository.Find; import jakarta.data.repository.OrderBy; import jakarta.data.repository.Repository;
@Repository
public interface FruitRepository extends BasicRepository
@Find
CursoredPage<Fruit> cursor(PageRequest pageRequest, Sort<Fruit> order);
@Find
@OrderBy("name")
Page<Fruit> offSet(PageRequest pageRequest);
long countBy();
}
```
The framework automatically implements this interface, allowing you to perform database operations without writing boilerplate code.
- Create a database setup class.
Create a SetupDatabase class in the src/main/java directory. The following code populates the database with sample data on startup and deletes the data on shutdown:
```java import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import io.quarkus.runtime.ShutdownEvent; import io.quarkus.runtime.StartupEvent; import org.jboss.logging.Logger; import java.util.List;
@ApplicationScoped public class SetupDatabase {
private static final Logger LOGGER = Logger.getLogger(SetupDatabase.class.getName());
private final FruitRepository fruitRepository;
public SetupDatabase(FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
void onStart(@Observes StartupEvent ev) {
LOGGER.info("The application is starting...");
long count = fruitRepository.countBy();
if (count > 0) {
LOGGER.info("Database already populated");
return;
}
List<Fruit> fruits = List.of(
Fruit.of("apple"),
Fruit.of("banana"),
Fruit.of("cherry"),
Fruit.of("date"),
Fruit.of("elderberry"),
Fruit.of("fig"),
Fruit.of("grape"),
Fruit.of("honeydew"),
Fruit.of("kiwi"),
Fruit.of("lemon")
);
fruitRepository.saveAll(fruits);
}
void onStop(@Observes ShutdownEvent ev) {
LOGGER.info("The application is stopping...");
fruitRepository.deleteAll(fruitRepository.findAll().toList());
}
}
```
- Create REST API endpoints.
Create a FruitResource class in the src/main/java directory. Then, paste the following code into the class file:
```java import jakarta.data.Sort; import jakarta.data.page.PageRequest; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType;
@Path("/fruits") public class FruitResource {
private final FruitRepository fruitRepository;
private static final Sort<Fruit> ASC = Sort.asc("name");
private static final Sort<Fruit> DESC = Sort.desc("name");
public FruitResource(FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
@Path("/offset")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Iterable<Fruit> offset(@QueryParam("page") @DefaultValue("1") long page,
@QueryParam("size") @DefaultValue("2") int size) {
var pageRequest = PageRequest.ofPage(page).size(size);
return fruitRepository.offSet(pageRequest).content();
}
@Path("/cursor")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Iterable<Fruit> cursor(@QueryParam("after") @DefaultValue("") String after,
@QueryParam("before") @DefaultValue("") String before,
@QueryParam("size") @DefaultValue("2") int size) {
if (!after.isBlank()) {
var pageRequest = PageRequest.ofSize(size).afterCursor(PageRequest.Cursor.forKey(after));
return fruitRepository.cursor(pageRequest, ASC).content();
} else if (!before.isBlank()) {
var pageRequest = PageRequest.ofSize(size).beforeCursor(PageRequest.Cursor.forKey(before));
return fruitRepository.cursor(pageRequest, DESC).stream().toList();
}
var pageRequest = PageRequest.ofSize(size);
return fruitRepository.cursor(pageRequest, ASC).content();
}
}
```
This class defines the following endpoints:
-
/fruits/offset: Supports offset-based pagination by using thepagequery parameter. -
/fruits/cursor: Supports cursor-based pagination by using theafterandbeforequery parameters.
Both endpoints also accept the size query parameter to specify the number of items per page.
- Start the application.
Run the following command from your project directory to start your Quarkus application in development mode:
bash
./mvnw compile quarkus:dev
- Test offset-based pagination.
In a separate terminal window, use the following curl commands to test the offset pagination endpoint. These commands request different pages of fruit data.
To fetch the first page, run the following command:
bash
curl --location http://localhost:8080/fruits/offset?page=1
To fetch the second page, run the following command:
bash
curl --location http://localhost:8080/fruits/offset?page=2
To fetch the fifth page, run the following command:
bash
curl --location http://localhost:8080/fruits/offset?page=5
-
Test cursor-based pagination.
Use the following
curlcommands to test the cursor pagination endpoint. These commands use theafterandbeforeparameters to navigate through the dataset.To fetch the initial set of fruits, run the following command:
bash curl --location http://localhost:8080/fruits/cursorTo fetch fruits with
namefield values that come after“banana”, run the following command:bash curl --location http://localhost:8080/fruits/cursor?after=bananaTo fetch fruits with
namefield values that come before“date”, run the following command:bash curl --location http://localhost:8080/fruits/cursor?before=date
Additional Resources
To learn more about pagination in MongoDB, see the Paginate Results guide in the MongoDB Atlas documentation.
To learn more about Quarkus, see the Quarkus documentation.
