Quarkus - Simplified Hibernate ORM with Panache

Hibernate ORM is the de facto JPA implementation and offers you the full breadth of an Object Relational Mapper.It makes complex mappings possible, but it does not make simple and common mappings trivial.Hibernate ORM with Panache focuses on making your entities trivial and fun to write in Quarkus.

First: an example

What we’re doing in Panache is allow you to write your Hibernate ORM entities like this:

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

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

the list() method might be surprising at first. It takes fragments of HQL (JP-QL) queries and contextualizes the rest. That makes for very concise but yet readable code.

Setting up and configuring Hibernate ORM with Panache

To get started:

  • add your settings in application.properties

  • annotate your entities with @Entity and make them extend PanacheEntity

  • place your entity logic in static methods in your entities

Follow the Hibernate set-up guide for all configuration.

In your pom.xml, add the following dependencies:

  • the Panache JPA extension

  • your JDBC driver extension (quarkus-jdbc-postgresql, quarkus-jdbc-h2, quarkus-jdbc-mariadb, …​)

  1. <dependencies>
  2. <!-- Hibernate ORM specific dependencies -->
  3. <dependency>
  4. <groupId>io.quarkus</groupId>
  5. <artifactId>quarkus-hibernate-orm-panache</artifactId>
  6. </dependency>
  7. <!-- JDBC driver dependencies -->
  8. <dependency>
  9. <groupId>io.quarkus</groupId>
  10. <artifactId>quarkus-jdbc-postgresql</artifactId>
  11. </dependency>
  12. </dependencies>

Then add the relevant configuration properties in application.properties.

  1. # configure your datasource
  2. quarkus.datasource.url = jdbc:postgresql://localhost:5432/mydatabase
  3. quarkus.datasource.driver = org.postgresql.Driver
  4. quarkus.datasource.username = sarah
  5. quarkus.datasource.password = connor
  6. # drop and create the database at startup (use `update` to only update the schema)
  7. quarkus.hibernate-orm.database.generation = drop-and-create

Defining your entity

To define a Panache entity, simply extend PanacheEntity, annotate it with @Entity and add yourcolumns as public fields:

  1. @Entity
  2. public class Person extends PanacheEntity {
  3. public String name;
  4. public LocalDate birth;
  5. public Status status;
  6. }

You can put all your JPA column annotations on the public fields. If you need a field to not be persisted, use the@Transient annotation on it. If you need to write accessors, you can:

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

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.

Most useful operations

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

  1. // creating a person
  2. Person person = new Person();
  3. person.name = "Stef";
  4. person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
  5. person.status = Status.Alive;
  6. // persist it
  7. person.persist();
  8. // note that once persisted, you don't need to explicitly save your entity: all
  9. // modifications are automatically persisted on transaction commit.
  10. // check if it's persistent
  11. if(person.isPersistent()){
  12. // delete it
  13. person.delete();
  14. }
  15. // getting a list of all Person entities
  16. List<Person> allPersons = Person.listAll();
  17. // finding a specific person by ID
  18. person = Person.findById(personId);
  19. // finding all living persons
  20. List<Person> livingPersons = Person.list("status", Status.Alive);
  21. // counting all persons
  22. long countAll = Person.count();
  23. // counting all living persons
  24. long countAlive = Person.count("status", Status.Alive);
  25. // delete all living persons
  26. Person.delete("status", Status.Alive);
  27. // delete all persons
  28. Person.deleteAll();

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());
The stream methods require a transaction to work.

Paging

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

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

Sorting

All methods accepting a query string also accept the following simplified query form:

  1. List<Person> persons = Person.list("order by name,birth");

But these methods also accept an optional Sort parameter, which allows your 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. @Entity
  2. public class Person extends PanacheEntity {
  3. public String name;
  4. public LocalDate birth;
  5. public Status status;
  6. public static Person findByName(String name){
  7. return find("name", name).firstResult();
  8. }
  9. public static List<Person> findAlive(){
  10. return list("status", Status.Alive);
  11. }
  12. public static void deleteStefs(){
  13. delete("name", "Stef");
  14. }
  15. }

Simplified queries

Normally, HQL queries are of this form: from EntityName [where …​] [order by …​], with optional elementsat the end.

If your query does not start with from, we support the following additional forms:

  • order by …​ which will expand to from EntityName order by …​

  • <singleColumnName> (and single parameter) which will expand to from EntityName where <singleColumnName> = ?

  • <query> will expand to from EntityName where <query>

You can also write your queries in plainHQL:
  1. Order.find("select distinct o from Order o left join fetch o.lineItems");

Query parameters

You can pass query parameters by index (1-based) as shown below:

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

Or by name using a Map:

  1. Map<String, Object> params = new HashMap<>();
  2. params.put("name", "stef");
  3. params.put("status", Status.Alive);
  4. Person.find("name = :name and status = :status", params);

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. Parameters.with("name", "stef").and("status", Status.Alive).map());
  4. // use it as-is
  5. Person.find("name = :name and status = :status",
  6. Parameters.with("name", "stef").and("status", Status.Alive));

Every query operation accepts passing parameters by index (Object…​), or by name (Map<String,Object> or Parameters).

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 PanacheRepository:

  1. @ApplicationScoped
  2. public class PersonRepository implements PanacheRepository<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 deleteStefs(){
  11. delete("name", "Stef");
  12. }
  13. }

Absolutely all the operations that are defined on PanacheEntityBase 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 PanacheEntity 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

Make sure to wrap methods modifying your database (e.g. entity.persist()) within a transaction. Marking aCDI bean method @Transactional will do that for you and make that method a transaction boundary. We recommend doingso at your application entry point boundaries like your REST endpoint controllers.

JPA batches changes you make to your entities and sends changes (it’s called flush) at the end of the transaction or before a query.This is usually a good thing as it’s more efficient.But if you want to check optimistic locking failures, do object validation right away or generally want to get immediate feedback, you can force the flush operation by calling entity.flush() or even use entity.persistAndFlush() to make it a single method call. This will allow you to catch any PersistenceException that could occur when JPA send those changes to the database.Remember, this is less efficient so don’t abuse it.And your transaction still has to be committed.

Here is an example of the usage of the flush method to allow making a specific action in case of PersistenceException:

  1. @Transactional
  2. public void create(Parameter parameter){
  3. try {
  4. //Here I use the persistAndFlush() shorthand method on a Panache repository to persist to database then flush the changes.
  5. return parameterRepository.persistAndFlush(parameter);
  6. }
  7. catch(PersistenceException pe){
  8. LOG.error("Unable to create the parameter", pe);
  9. //in case of error, I save it to disk
  10. diskPersister.save(parameter);
  11. }
  12. }

Lock management

Panache does not provide direct support for database locking, but you can do it by injecting the EntityManager (see first example below) in your entity (or PanacheRepository) and creating a specific method that will use the entity manager to lock the entity after retrieval. The entity manager can also be retrieved via Panache.getEntityManager() see second example below.

The following examples contain a findByIdForUpdate method that finds the entity by primary key then locks it. The lock will generate a SELECT …​ FOR UPDATE query (the same principle can be used for other kinds of find* methods):

First: Locking in a PanacheRepository example

  1. @ApplicationScoped
  2. public class PersonRepository implements PanacheRepository<Person> {
  3. // inject the EntityManager inside the entity
  4. @Inject
  5. EntityManager entityManager;
  6. public Person findByIdForUpdate(Long id){
  7. Person person = findById(id);
  8. //lock with the PESSIMISTIC_WRITE mode type : this will generate a SELECT ... FOR UPDATE query
  9. entityManager.lock(person, LockModeType.PESSIMISTIC_WRITE);
  10. return person;
  11. }
  12. }

Second: Locking in a PanacheEntity example

  1. @Entity
  2. public class Person extends PanacheEntity {
  3. public String name;
  4. public LocalDate birth;
  5. public Status status;
  6. public static Person findByIdForUpdate(Long id){
  7. // get the EntityManager inside the entity
  8. EntityManager entityManager = Panache.getEntityManager();
  9. Person person = findById(id);
  10. //lock with the PESSIMISTIC_WRITE mode type : this will generate a SELECT ... FOR UPDATE query
  11. entityManager.lock(person, LockModeType.PESSIMISTIC_WRITE);
  12. return person;
  13. }
  14. }

This will generate two select queries: one to retrieve the entity and the second to lock it. Be careful that locks are released when the transaction ends, so the method that invokes the lock query must be annotated with the @Transactional annotation.

We are currently evaluating adding support for lock management inside Panache. If you are interested, please visit our github issue #2744 and contribute to the discussion.

Custom IDs

IDs are often a touchy subject, and not everyone’s up for letting them handled by the framework, once again wehave you covered.

You can specify your own ID strategy by extending PanacheEntityBase instead of PanacheEntity. Thenyou just declare whatever ID you want as a public field:

  1. @Entity
  2. public class Person extends PanacheEntityBase {
  3. @Id
  4. @SequenceGenerator(
  5. name = "personSequence",
  6. sequenceName = "person_id_seq",
  7. allocationSize = 1,
  8. initialValue = 4)
  9. @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "personSequence")
  10. public Integer id;
  11. //...
  12. }

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

  1. @ApplicationScoped
  2. public class PersonRepository implements PanacheRepositoryBase<Person,Integer> {
  3. //...
  4. }

How and why we simplify Hibernate ORM mappings

When it comes to writing Hibernate ORM 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 methodsare in the same class. Moreover, this requires two classes per entity, and requires injection of the DAO or Repositorywhere you need to do entity operations, which breaks your edit flow and requires you to get out of the code you’rewriting to set up an injection point before coming back to use it.

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

  • Hibernate is very general-purpose, but does not make it trivial to do trivial operations that make up 90% of ourmodel usage.

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

  • Make your entities extend PanacheEntity: it has an ID field that is auto-generated. If you requirea custom ID strategy, you can extend PanacheEntityBase 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 superclasscomes with lots of super useful static methods and you can add your own in your entity class. Users can just start usingyour entity Person by typing Person. and getting completion for all the operations in a single place.

  • Don’t write parts of the query that you don’t need: write Person.find("order by name") orPerson.find("name = ?1 and status = ?2", "stef", Status.Alive) or even betterPerson.find("name", "stef").

That’s all there is to it: with Panache, Hibernate ORM has never looked so trim and neat.

Defining entities in external projects or jars

Hibernate ORM 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.