Getting started with MongoDB and Quarkus

This tutorial covers the all the steps required for creating a REST application with MongoDB NoSQL Database and Quarkus.

MongoDB is a document-oriented NoSQL database which became popular in the last decade. It can be used for high volume data storage as a replacement for relational databases. Instead of using tables and rows, MongoDB makes use of collections and documents. A Document consists of key-value pairs which are the basic unit of data in MongoDB. A Collection, on the other hand, contain sets of documents and function which is the equivalent of relational database tables.

In order to get started with MongoDB, you can download the latest Community version from: https://www.mongodb.com/try/download/community

To simplify things, we will start MongoDB using Docker with just one line:

docker run -ti --rm -p 27017:27017 mongo:4.0

Done with MongoDB, there are basically two approaches for developing MongoDB applications and Quarkus:

  • Using the MongoDB Java Client which focuses on the API provided by the MongoDB Client
  • Using Hibernate Panache which focuses on a special kind of Entity objects which are named PanacheMongoEntity

Using the MongoDB Java Client API

By using this approach, you manipulate plain Java Bean classes with MongoDB Java Client. Let’s build a sample application. We will start from the simple Model class:

public class Customer {
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Then, we need a Service class to manage the standard CRUD operations on the Model using the MongoDB Client:

@ApplicationScoped
public class CustomerService {

    @Inject MongoClient mongoClient;

    public List<Customer> list(){
        List<Customer> list = new ArrayList<>();
        MongoCursor<Document> cursor = getCollection().find().iterator();

        try {
            while (cursor.hasNext()) {
                Document document = cursor.next();
                Customer customer = new Customer();
                customer.setName(document.getString("name"));
                customer.setId(document.getLong("id"));
                list.add(customer);
            }
        } finally {
            cursor.close();
        }
        return list;
    }

    public void add(Customer customer){
        Document document = new Document()
                .append("name", customer.getName())
                .append("id", customer.getId());
        getCollection().insertOne(document);
    }

    public void update(Customer customer){
        Bson filter = eq("id", customer.getId());
        Bson updateOperation = set("name", customer.getName());
        getCollection().updateOne(filter, updateOperation);
    }

    public void delete(Customer customer){
        Bson filter = eq("id", customer.getId());
        getCollection().deleteOne(filter);
    }
    private MongoCollection getCollection(){
        return mongoClient.getDatabase("customer").getCollection("customer");
    }
}

Finally, a REST Endpoint is added to expose the Service:

@Path("/customer")
@Produces("application/json")
@Consumes("application/json")
public class CustomerEndpoint {

    @Inject CustomerService service;

    @GET
    public List<Customer> list() {
        return service.list();
    }

    @POST
    public List<Customer> add(Customer customer) {
        service.add(customer);
        return list();
    }

    @PUT
    public List<Customer> put(Customer customer) {
        service.update(customer);
        return list();
    }

    @DELETE
    public List<Customer> delete(Customer customer) {
        service.delete(customer);
        return list();
    }
}

The last configuration tweak required is in application.properties where we will configure the MongoDB client for a single instance on localhost:

quarkus.mongodb.connection-string = mongodb://localhost:27017

To compile the application, besides the resteasy libraries and the testing API, we need to include the Quarkus MongoDB Client API:

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-jsonb</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-mongodb-client</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>

Run the application:

mvn install quarkus:dev

Then, you can test adding a new Customer:

curl -d '{"id":"1", "name":"Frank"}' -H "Content-Type: application/json" -X POST http://localhost:8080/customer

Checking the list of customers:

curl http://localhost:8080/customer
[{"id":1,"name":"Frank"}]

Updating a Customer:

curl -d '{"id":"1", "name":"John"}' -H "Content-Type: application/json" -X PUT http://localhost:8080/customer

And finally deleting it:

curl -d '{"id":"1"}' -H "Content-Type: application/json" -X DELETE http://localhost:8080/customer

Using MongoDB with Hibernate Panache

The Hibernate Panache API greatly simplifies the management of CRUD operations on Hibernate Entity by extending them with Panache Entity Objects and their subclasses such as PanacheMongoEntity.

The following here is the Customer class, which extends PanacheMongoEntity and therefore inherits all the basic CRUD operations:

@MongoEntity(collection = "customers")
public class Customer extends PanacheMongoEntity {
    
    public Long id;

    @BsonProperty("customer_name")
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    public static Customer findByName(String name) {
        return find("name", name).firstResult();
    }

}

The @MongoEntity annotation can be used to define the MongoDB collection name to be used for the Entity class. Also, the @BsonProperty annotation is used to change the name used in MongoDB to persist the field. With the PanacheMongoEntity, we already have all the methods available to perform the standard CRUD operations. The REST Endpoint is therefore just a wrapper to access each CRUD operation:

@Path("/customer")
@Produces("application/json")
@Consumes("application/json")
public class CustomerEndpoint {

    @GET
    public List<Customer> list() {
        return Customer.listAll();
    }

    @POST
    public Response create(Customer customer) {
        customer.persist();
        return Response.status(201).build();
    }

    @PUT
    public void update(Customer customer) {
        customer.update();
    }

    @DELETE
    public void delete(Customer c) {
        Customer customer = Customer.findById(c.id);
        customer.delete();
    }
}

The last configuration tweak required is in application.properties where we will configure the MongoDB client for a single instance on localhost and the database name:

quarkus.mongodb.connection-string = mongodb://localhost:27017
quarkus.mongodb.database = customers

To build this example, you will need to use the following dependency in your pom.xml (instead of of the MongoDB client API):

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-mongodb-panache</artifactId>
    </dependency>

Using PanacheQuery to fetch your data

The PanacheQuery interface can be used to map an Entity query, adding the use of paging, getting the number of results, and operating both on List and Java Stream API.

Here is an example:

    // Get first page of 20 entries
    @Path("/page1")
    @GET
    public List<Customer> pageList() {
        PanacheQuery<Customer> customers = Customer.findAll();
        customers.page(Page.ofSize(20));
        return customers.list();
    }

The above example shows how you can fetch just the first page of 20 Customer Entity objects. To fetch the next page:

List<Customer> nextPage = customers.nextPage().list();

Besides the findAll() method you can also restrict your Query using the find(key,value) method. For example:

    @Path("/page1")
    @GET
    public List<Customer> pageList() {
        PanacheQuery<Customer> customers = Customer.find("name","John");
        customers.page(Page.ofSize(20));
        return customers.list();
    }

Finally, the PanacheQuery can be used as well to fetch the total number of entities, without paging:

int count = customers.count();

If you want an example of Hibernate Panache with a Relational Database check the following article: Managing Data Persistence with Quarkus and Hibernate Panache

You can find the source code for both examples on Github at: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/mongodb