How to Build a CRUD Application With MongoDB, Quarkus, and GraalVM
Maxime Beugnet7 min read • Published Aug 29, 2024 • Updated Aug 29, 2024
FULL APPLICATION
Rate this quickstart
When we write a traditional Java application, our Java source code is compiled and transformed into Java bytecode.
This bytecode can then be executed by a Java virtual machine (JVM) specific to the operating system you are
running. This is why we can say that Java is a portable language. You compile once, and you can run it everywhere,
as long as you have the right JVM on the right machine.
This is a great mechanism, but it comes at a cost. Starting a program is slow because the JVM and the entire context
need to be loaded first before running anything. It's not memory-efficient because we need to load hundreds of classes that might not be used at all in the end as the classpath scanning only occurs after.
This was perfectly fine in the old monolithic realm, but this is totally unacceptable in the new world made of lambda
functions, cloud, containers, and Kubernetes. In this context, a low memory footprint and a lightning-fast startup time
are absolutely mandatory.
With Quarkus, you can build native binaries that can boot and send their first response in 0.042 seconds versus 9.5
seconds for a traditional Java application.
In this tutorial, we are going to build a Quarkus application that can manage a
persons
collection in MongoDB. The
goal is to perform four simple CRUD operations with a REST API using a native application.For this tutorial, you'll need:
- cURL.
If you don't want to code along and prefer to check out directly the final code:
1 git clone git@github.com:mongodb-developer/quarkus-mongodb-crud.git
The easiest way to get your project up and running with Quarkus and all the dependencies you need is to
use https://code.quarkus.io/.
Similar to Spring initializr, the Quarkus project starter website will help you
select your dependencies and build your Maven or Gradle configuration file. Some dependencies will also include a
starter code to assist you in your first steps.
For our project, we are going to need:
- MongoDB client [quarkus-mongodb-client].
- SmallRye OpenAPI [quarkus-smallrye-openapi].
- REST [quarkus-rest].
- REST Jackson [quarkus-rest-jackson].
Feel free to use the
group
and artifact
of your choice. Make sure the Java version matches the version of your
GraalVM version, and we are ready to go.Download the zip file and unzip it in your favorite project folder. Once it's done, take some time to read the README.md
file provided.
Finally, we need a MongoDB cluster. Two solutions:
- Create an ephemeral single-node replica set with Docker.
1 docker run --rm -d -p 27017:27017 -h $(hostname) --name mongo mongo:latest --replSet=RS && sleep 5 && docker exec mongo mongosh --quiet --eval "rs.initiate();"
Either way, the next step is to set up your connection string in the
application.properties
file.1 quarkus.mongodb.connection-string=mongodb://localhost:27017
Now that our Quarkus project is ready, we can start developing.
First, we can start the developer mode which includes live coding (automatic refresh) without the need to restart the
program.
1 ./mvnw compile quarkus:dev
The developer mode comes with two handy features:
Feel free to take some time to explore both these UIs and see the capabilities they offer.
Also, as your service is now running, you should be able to receive your first HTTP communication. Open a new terminal and execute the following query:
1 curl http://localhost:8080/hello
Note: If you cloned the repo, then it’s
/api/hello
. We are changing this below in a minute.Result:
1 Hello from Quarkus REST
This works because your project currently contains a single class
GreetingResource.java
with the following code.1 package com.mongodb; 2 3 import jakarta.ws.rs.GET; 4 import jakarta.ws.rs.Path; 5 import jakarta.ws.rs.Produces; 6 import jakarta.ws.rs.core.MediaType; 7 8 9 public class GreetingResource { 10 11 12 13 public String hello() { 14 return "Hello from Quarkus REST"; 15 } 16 }
"Hello from Quarkus REST" is nice, but it's not our goal! We want to manipulate data from a
persons
collection in
MongoDB.Let's create a classic
PersonEntity.java
POJO class. I created
it in the default com.mongodb
package which is my group
from earlier. Feel free to change it.1 package com.mongodb; 2 3 import com.fasterxml.jackson.databind.annotation.JsonSerialize; 4 import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; 5 import org.bson.types.ObjectId; 6 7 import java.util.Objects; 8 9 public class PersonEntity { 10 11 12 public ObjectId id; 13 public String name; 14 public Integer age; 15 16 public PersonEntity() { 17 } 18 19 public PersonEntity(ObjectId id, String name, Integer age) { 20 this.id = id; 21 this.name = name; 22 this.age = age; 23 } 24 25 26 public int hashCode() { 27 int result = id != null ? id.hashCode() : 0; 28 result = 31 * result + (name != null ? name.hashCode() : 0); 29 result = 31 * result + (age != null ? age.hashCode() : 0); 30 return result; 31 } 32 33 34 public boolean equals(Object o) { 35 if (this == o) return true; 36 if (o == null || getClass() != o.getClass()) return false; 37 38 PersonEntity that = (PersonEntity) o; 39 40 if (!Objects.equals(id, that.id)) return false; 41 if (!Objects.equals(name, that.name)) return false; 42 return Objects.equals(age, that.age); 43 } 44 45 public ObjectId getId() { 46 return id; 47 } 48 49 public void setId(ObjectId id) { 50 this.id = id; 51 } 52 53 public String getName() { 54 return name; 55 } 56 57 public void setName(String name) { 58 this.name = name; 59 } 60 61 public Integer getAge() { 62 return age; 63 } 64 65 public void setAge(Integer age) { 66 this.age = age; 67 } 68 }
Now that we have a
PersonEntity
, we can create a PersonRepository
template, ready to welcome our CRUD queries.Create a
PersonRepository.java
class next to the PersonEntity.java
one.1 package com.mongodb; 2 3 import com.mongodb.client.MongoClient; 4 import com.mongodb.client.MongoCollection; 5 import jakarta.enterprise.context.ApplicationScoped; 6 7 8 public class PersonRepository { 9 10 private final MongoClient mongoClient; 11 private final MongoCollection<PersonEntity> coll; 12 13 public PersonRepository(MongoClient mongoClient) { 14 this.mongoClient = mongoClient; 15 this.coll = mongoClient.getDatabase("test").getCollection("persons", PersonEntity.class); 16 } 17 18 // CRUD methods will go here 19 20 }
We are now almost ready to create our first CRUD method. Let's update the default
GreetingResource.java
class to match
our goal.- Rename the file
GreetingResource.java
toPersonResource.java
. - In the
test
folder, also rename the default test files toPersonResourceIT.java
andPersonResourceTest.java
. - Update
PersonResource.java
like this:
1 package com.mongodb; 2 3 import jakarta.inject.Inject; 4 import jakarta.ws.rs.*; 5 import jakarta.ws.rs.core.MediaType; 6 7 8 9 10 public class PersonResource { 11 12 13 PersonRepository personRepository; 14 15 16 17 public String hello() { 18 return "Hello from Quarkus REST"; 19 } 20 21 // CRUD routes will go here 22 23 }
Note that with the
@Path("/api")
annotation, the URL of our /hello
service is now /api/hello
.As a consequence, update
PersonResourceTest.java
so our test keeps working.1 package com.mongodb; 2 3 import io.quarkus.test.junit.QuarkusTest; 4 import org.junit.jupiter.api.Test; 5 6 import static io.restassured.RestAssured.given; 7 import static org.hamcrest.CoreMatchers.is; 8 9 10 class PersonResourceTest { 11 12 void testHelloEndpoint() { 13 given().when().get("/api/hello").then().statusCode(200).body(is("Hello from Quarkus REST")); 14 } 15 }
All the code blocks are now in place. We can create our first route to be able to create a new person.
In
the repository,
add the following method that inserts a
PersonEntity
and returns the inserted document's ObjectId
in String
format.1 public String add(PersonEntity person) { 2 return coll.insertOne(person).getInsertedId().asObjectId().getValue().toHexString(); 3 }
1 2 3 public String createPerson(PersonEntity person) { 4 return personRepository.add(person); 5 }
Without restarting the project (remember the dev mode?), you should be able to test this route.
1 curl -X POST http://localhost:8080/api/person \ 2 -H 'Content-Type: application/json' \ 3 -d '{"name": "John Doe", "age": 30}'
This should return the
ObjectId
of the new person
document.1 661dccf785cd323349ca42f7
1 RS [direct: primary] test> db.persons.find() 2 [ 3 { 4 _id: ObjectId('661dccf785cd323349ca42f7'), 5 age: 30, 6 name: 'John Doe' 7 } 8 ]
Now, we can read all the persons in the database, for example.
1 public List<PersonEntity> getPersons() { 2 return coll.find().into(new ArrayList<>()); 3 }
1 2 3 public List<PersonEntity> getPersons() { 4 return personRepository.getPersons(); 5 }
Now, we can retrieve all the persons in our database:
1 curl http://localhost:8080/api/persons
This returns a list of persons:
1 [ 2 { 3 "id": "661dccf785cd323349ca42f7", 4 "name": "John Doe", 5 "age": 30 6 } 7 ]
It's John Doe's anniversary! Let's increment his age by one.
1 public long anniversaryPerson(String id) { 2 Bson filter = eq("_id", new ObjectId(id)); 3 Bson update = inc("age", 1); 4 return coll.updateOne(filter, update).getModifiedCount(); 5 }
1 2 3 public long anniversaryPerson( String id) { 4 return personRepository.anniversaryPerson(id); 5 }
Time to test this party:
1 curl -X PUT http://localhost:8080/api/person/661dccf785cd323349ca42f7
This returns
1
which is the number of modified document(s). If the provided ObjectId
doesn't match a person's id,
then it returns 0
and MongoDB doesn't perform any update.Finally, it's time to delete John Doe...
1 public long deletePerson(String id) { 2 Bson filter = eq("_id", new ObjectId(id)); 3 return coll.deleteOne(filter).getDeletedCount(); 4 }
1 2 3 public long deletePerson( String id) { 4 return personRepository.deletePerson(id); 5 }
Let's test:
1 curl -X DELETE http://localhost:8080/api/person/661dccf785cd323349ca42f7
Again, it returns
1
which is the number of deleted document(s).Now that we have a working Quarkus application with a MongoDB CRUD service, it's time to experience the full
power of Quarkus.
Quit the developer mode by simply hitting the
q
key in the relevant terminal.It's time to build
the native executable
that we can use in production with GraalVM and experience the insanely fast start-up time.
Use this command line to build directly with your local GraalVM and other dependencies.
1 ./mvnw package -Dnative
Or use the Docker image that contains everything you need:
1 ./mvnw package -Dnative -Dquarkus.native.container-build=true
The final result is a native application, ready to be launched, in your
target
folder.1 ./target/quarkus-mongodb-crud-1.0.0-SNAPSHOT-runner
On my laptop, it starts in just 0.019s! Remember how much time Spring Boot needs to start an application and respond
to queries for the first time?!
You can read more about how Quarkus makes this miracle a reality in
the container first documentation.
In this tutorial, we've explored how Quarkus and MongoDB can team up to create a lightning-fast RESTful API with CRUD
capabilities.
Now equipped with these insights, you're ready to build blazing-fast APIs with Quarkus, GraalVM, and MongoDB. Dive into
the
provided GitHub repository for more details.
If you have questions, please head to our Developer Community website where the
MongoDB engineers and the MongoDB community will help you build your next big idea with MongoDB.
Top Comments in Forums
There are no comments on this article yet.