Quarkus - Simplified MongoDB with Panache

    MongoDB with Panache provides active record style entities (and repositories) like you have in Hibernate ORM with Panache and focuses on making your entities trivial and fun to write in Quarkus.

    It is built on top of the extension.

    Panache allows you to write your MongoDB entities like this:

    You have noticed how much more compact and readable the code is compared to using the MongoDB API?Does this look interesting? Read on!

    the list() method might be surprising at first. It takes fragments of PanacheQL queries (subset of JPQL) and contextualizes the rest.That makes for very concise but yet readable code.MongoDB native queries are also supported.

    Solution

    We recommend that you follow the instructions in the next sections and create the application step by step.However, you can go right to the completed example.

    Clone the Git repository: git clone , or download an archive.

    The solution is located in the mongodb-panache-quickstart .

    Creating the Maven project

    First, we need a new project. Create a new project with the following command:

    1. mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create \
    2. -DprojectGroupId=org.acme \
    3. -DprojectArtifactId=mongodb-panache-quickstart \
    4. -DclassName="org.acme.mongodb.panache.FruitResource" \
    5. -Dpath="/fruits" \
    6. -Dextensions="resteasy-jsonb,mongodb-panache"
    7. cd mongodb-panache-quickstart

    This command generates a Maven structure importing the RESTEasy/JAX-RS, JSON-B and MongoDB with Panache extensions.After this, the quarkus-mongodb-panache extension has been added to your pom.xml.

    If you don’t want to generate a new project you can add the dependency in your pom.xml:

    1. <dependencies>
    2. <dependency>
    3. <groupId>io.quarkus</groupId>
    4. <artifactId>quarkus-mongodb-panache</artifactId>
    5. </dependency>
    6. </dependencies>

    Setting up and configuring MongoDB with Panache

    To get started:

    • add your settings in application.properties

    • Make your entities extend PanacheMongoEntity, you can use the @MongoEntity annotation to specify the name of the database and the name of the collection (it will default to the name of your entity).

    • place your entity logic in static methods in your entities

    Then add the relevant configuration properties in application.properties.

    1. # configure the MongoDB client for a replica set of two nodes
    2. quarkus.mongodb.connection-string = mongodb://mongo1:27017,mongo2:27017
    3. # mandatory if you don't specify the name of the database using @MongoEntity
    4. quarkus.mongodb.database = person

    The quarkus.mongodb.database property will be used by MongoDB with Panache to determine the name of the database where your entities will be persisted.

    For advanced configuration of the MongoDB client, you can follow the Configuring the MongoDB database guide.

    Defining your entity

    To define a Panache entity, simply extend PanacheMongoEntity and add your columns as public fields.You can add the @MongoEntity annotation to your entity if you need to customize the name of the collection and/or the database.

    1. @MongoEntity(collection="ThePerson")
    2. public class Person extends PanacheMongoEntity {
    3. public String name;
    4. // will be persisted as a 'birth' field in MongoDB
    5. @BsonProperty("birth")
    6. public LocalDate birthDate;
    7. public Status status;
    8. }

    MongoDB with Panache uses the PojoCodecProvider to map your entities to a MongoDB Document.

    You will be allowed to use the following annotations to customize this mapping:

    • @BsonId: allows you to customize the ID field, see .

    • @BsonProperty: customize the serialized name of the field.

    • @BsonIgnore: ignore a field during the serialization.

    1. public class Person extends PanacheMongoEntity {
    2. public String name;
    3. public LocalDate birth;
    4. public Status status;
    5. // return name as uppercase in the model
    6. public String getName(){
    7. return name.toUpperCase();
    8. }
    9. // store all names in lowercase in the DB
    10. public void setName(String name){
    11. }
    12. }

    And thanks to our field access rewrite, when your users read person.name they will actually call your getName() accessor, and similarly for field writes and the setter.This allows for proper encapsulation at runtime as all fields calls will be replaced by the corresponding getter/setter calls.

    Once you have written your entity, here are the most common operations you will be able to do:

    All list methods have equivalent stream versions.

    1. Stream<Person> persons = Person.streamAll();
    2. List<String> namesButEmmanuels = persons
    3. .map(p -> p.name.toLowerCase() )
    4. .filter( n -> ! "emmanuel".equals(n) )
    5. .collect(Collectors.toList());
    A persistOrUpdate() method exist that persist or update an entity in the database, it uses the upsert capability of MongoDB to do it in a single query.

    Paging

    You should only use list and stream methods if your collection contains small enough data sets. For larger datasets you can use the find method equivalents, which return a PanacheQuery on which you can do paging:

    1. PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);
    2. // make it use pages of 25 entries at a time
    3. livingPersons.page(Page.ofSize(25));
    4. // get the first page
    5. List<Person> firstPage = livingPersons.list();
    6. // get the second page
    7. List<Person> secondPage = livingPersons.nextPage().list();
    8. // get page 7
    9. List<Person> page7 = livingPersons.page(Page.of(7, 25)).list();
    10. // get the number of pages
    11. int numberOfPages = livingPersons.pageCount();
    12. // get the total number of entities returned by this query without paging
    13. int count = livingPersons.count();
    14. // and you can chain methods of course
    15. return Person.find("status", Status.Alive)
    16. .page(Page.ofSize(25))
    17. .nextPage()
    18. .stream()

    The PanacheQuery type has many other methods to deal with paging and returning streams.

    Sorting

    All methods accepting a query string also accept an optional Sort parameter, which allows you to abstract your sorting:

    1. List<Person> persons = Person.list(Sort.by("name").and("birth"));
    2. // and with more restrictions
    3. List<Person> persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive);

    The Sort class has plenty of methods for adding columns and specifying sort direction.

    Adding entity methods

    In general, we recommend not adding custom queries for your entities outside of the entities themselves,to keep all model queries close to the models they operate on. So we recommend adding them as static methodsin your entity class:

    1. public class Person extends PanacheMongoEntity {
    2. public String name;
    3. public LocalDate birth;
    4. public Status status;
    5. public static Person findByName(String name){
    6. return find("name", name).firstResult();
    7. }
    8. public static List<Person> findAlive(){
    9. return list("status", Status.Alive);
    10. }
    11. public static void deleteLoics(){
    12. delete("name", "Loïc");
    13. }
    14. }

    Simplified queries

    Normally, MongoDB queries are of this form: {'firstname': 'John', 'lastname':'Doe'}, this is what we call MongoDB native queries.

    You can use them if you want, but we also support what we call PanacheQL that can be seen as a subset of JPQL (or ) and allows you to easily express a query.MongoDB with Panache will then map it to a MongoDB native query.

    If your query does not start with {, we will consider it a PanacheQL query:

    • <singleColumnName> (and single parameter) which will expand to {'singleColumnName': '?'}

    • <query> will expand to {<query>} where we will map the PanacheQL query to MongoDB native query form. We support the following operators that will be mapped to the corresponding MongoDB operators: 'and', 'or' ( mixing 'and' and 'or' is not currently supported), '=', '>', '>=', '<', '⇐', '!=', 'is null', 'is not null', and 'like' that is mapped to the MongoDB $regex operator.

    Here are some query examples:

    • firstname = ?1 and status = ?2 will be mapped to {'firstname': ?1, 'status': ?2}

    • amount > ?1 and firstname != ?2 will be mapped to {'amount': {'$gt': ?1}, 'firstname': {'$ne': ?2}}

    • lastname like ?1 will be mapped to {'lastname': {'$regex': ?1}}. Be careful that this will be MongoDB regex support and not SQL like pattern.

    • lastname is not null will be mapped to {'lastname':{'$exists': true}}

    We also handle some basic date type transformations: all fields of type Date, LocalDate or LocalDateTime will be mapped to the using the ISODate type (UTC datetime).

    MongoDB with Panache also supports extended MongoDB queries by providing a Document query, this is supported by the find/list/stream/count/delete methods.

    You can pass query parameters, for both native and PanacheQL queries, by index (1-based) as shown below:

    1. Person.find("name = ?1 and status = ?2", "Loïc", Status.Alive);

    Or by name using a Map:

    Or using the convenience class Parameters either as is or to build a Map:

    1. // generate a Map
    2. Person.find("name = :name and status = :status",
    3. // use it as-is
    4. Person.find("{'name': :name, 'status': :status}",
    5. Parameters.with("name", "Loïc").and("status", Status.Alive));

    When you use query parameters, be careful that PanacheQL queries will refer to the Object parameters name but native queries will refer to MongoDB field names.

    Imagine the following entity:

    1. public class Person extends PanacheMongoEntity {
    2. @BsonProperty("lastname")
    3. public String name;
    4. public LocalDate birth;
    5. public Status status;
    6. public static Person findByNameWithPanacheQLQuery(String name){
    7. return find("name", name).firstResult();
    8. }
    9. public static Person findByNameWithNativeQuery(String name){
    10. return find("{'lastname': ?1}", name).firstResult();
    11. }
    12. }

    Both findByNameWithPanacheQLQuery() and findByNameWithNativeQuery() methods will return the same result but query written in PanacheQLwill use the entity field name: name, and native query will use the MongoDB field name: lastname.

    The DAO/Repository option

    Repository is a very popular pattern and can be very accurate for some use case, depending onthe complexity of your needs.

    Whether you want to use the Entity based approach presented above or a more traditional Repository approach, it is up to you,Panache and Quarkus have you covered either way.

    If you lean towards using Repositories, you can get the exact same convenient methods injected in your Repository by making itimplement PanacheMongoRepository:

    1. @ApplicationScoped
    2. public class PersonRepository implements PanacheMongoRepository<Person> {
    3. // put your custom logic here as instance methods
    4. public Person findByName(String name){
    5. return find("name", name).firstResult();
    6. }
    7. public List<Person> findAlive(){
    8. return list("status", Status.Alive);
    9. }
    10. public void deleteLoics(){
    11. delete("name", "Loïc");
    12. }
    13. }

    Absolutely all the operations that are defined on PanacheMongoEntityBase are available on your DAO, so using itis exactly the same except you need to inject it:

    1. @Inject
    2. PersonRepository personRepository;
    3. @GET
    4. public long count(){
    5. return personRepository.count();
    6. }

    So if Repositories are your thing, you can keep doing them. Even with repositories, you can keep your entities assubclasses of PanacheMongoEntity in order to get the ID and public fields working, but you can even skip that andgo back to specifying your ID and using getters and setters if that’s your thing. Use what works for you.

    Transactions

    Custom IDs

    IDs are often a touchy subject. In MongoDB, they are usually auto-generated by the database with an ObjectId type.In MongoDB with Panache the ID are defined by a field named id of the org.bson.types.ObjectId type,but if you want ot customize them, once again we have you covered.

    You can specify your own ID strategy by extending PanacheMongoEntityBase instead of PanacheMongoEntity. Thenyou just declare whatever ID you want as a public field by annotating it by @BsonId:

    1. @MongoEntity
    2. public class Person extends PanacheMongoEntityBase {
    3. @BsonId
    4. public Integer myId;
    5. //...
    6. }

    If you’re using repositories, then you will want to extend PanacheMongoRepositoryBase instead of PanacheMongoRepositoryand specify your ID type as an extra type parameter:

    When using ObjectId, MongoDB will automatically provide a value for you, but if you use a custom field type,you need to provide the value by yourself.

    ObjectId can be difficult to use if you want to expose its value in your REST service.So we created JSON-B and Jackson providers to serialize/deserialize them as a String which are are automatically registered if your project depends on one of the RESTEasy with JSON-B or RESTEasy with Jackson extensions.

    How and why we simplify MongoDB API

    When it comes to writing MongoDB entities, there are a number of annoying things that users have grown used toreluctantly deal with, such as:

    • Duplicating ID logic: most entities need an ID, most people don’t care how it’s set, because it’s not reallyrelevant to your model.

    • Dumb getters and setters: since Java lacks support for properties in the language, we have to create fields,then generate getters and setters for those fields, even if they don’t actually do anything more than read/writethe fields.

    • Traditional EE patterns advise to split entity definition (the model) from the operations you can do on them(DAOs, Repositories), but really that requires an unnatural split between the state and its operations even thoughwe would never do something like that for regular objects in the Object Oriented architecture, where state and methods are in the same class. Moreover, this requires two classes per entity, and requires injection of the DAO or Repository where you need to do entity operations, which breaks your edit flow and requires you to get out of the code you’re writing to set up an injection point before coming back to use it.

    • MongoDB queries are super powerful, but overly verbose for common operations, requiring you to write queries evenwhen you don’t need all the parts.

    • MongoDB queries are JSON based, so you will need some String manipulation or using the Document type and it will need a lot of boilerplate code.

    With Panache, we took an opinionated approach to tackle all these problems:

    • Make your entities extend PanacheMongoEntity: it has an ID field that is auto-generated. If you requirea custom ID strategy, you can extend PanacheMongoEntityBase instead and handle the ID yourself.

    • Use public fields. Get rid of dumb getter and setters. Under the hood, we will generate all getters and settersthat are missing, and rewrite every access to these fields to use the accessor methods. This way you can stillwrite useful accessors when you need them, which will be used even though your entity users still use field accesses.

    • Don’t use DAOs or Repositories: put all your entity logic in static methods in your entity class. Your entity superclass comes with lots of super useful static methods and you can add your own in your entity class. Users can just start using your entity Person by typing Person. and getting completion for all the operations in a single place.

    MongoDB with Panache relies on compile-time bytecode enhancements to your entities.If you define your entities in the same project where you build your Quarkus application, everything will work fine.If the entities come from external projects or jars, you can make sure that your jar is treated like a Quarkus application libraryby indexing it via Jandex, see How to Generate a Jandex Index in the CDI guide.This will allow Quarkus to index and enhance your entities as if they were inside the current project.