Quarkus: Simplified Hibernate ORM with Panache

In this blog post we will see how to persist data using Hibernate ORM with Panache in Quarkus framework.

Hibernate ORM is the de facto JPA implementation and offers 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 and operations on them trivial.

Let’s see how Panache simplifies ORM persistence compared to plain Hibernate

Hibernate ORM comes with

  • Duplicating ID logic. Most entities need an ID but it’s mostly auto generated.
  • Dumb getters and setters
  • Hibernate queries are super powerful, but overly verbose for common operations, requiring you to write queries even when you don’t need all the parts.
  • Entity definition (the model) and the operations that performed on them (DAOs, Repositories) are defined in separate files.

Panache, comes with an opinionated approach to tackle above problems

  • Make your entities extend PanacheEntity: it has an ID field that is auto-generated. If you require a 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 setters that are missing.
  • With the active record pattern: put all your entity logic in static methods in your entity class and don’t create DAOs. 
  • PanacheEntity comes with lots of super useful static methods and developers can add custom methods
  • Don’t write parts of the query that you don’t need

We can achieve persistence using Panache in two ways

  • Active record pattern
  • Repository pattern

Today we are going to see the data persistence using Active record pattern

What is Active Record Pattern

First, let’s understand what is Active Record Pattern

The definition of ActiveRecord Pattern according to Marting Fowler

Active Record Pattern

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.

Entities object that follow AR pattern would include functions such as Insert, Update, and Delete, plus properties that correspond more or less directly to the columns in the underlying database table.

Setting up the project

Head over to code.quarkus.io and create project by selecting following extensions

  • RESTEasy jackson
  • JDBC Driver -h2
  • Hibernate ORM with Panache

Download the project, unzip and import the project into your favourite IDE.

We are going to use MapStruct to map between entities and dto’s so added the following dependencies in pom.xml

    <dependency>
      <groupId>org.mapstruct</groupId>
      <artifactId>mapstruct</artifactId>
      <version>1.3.1.Final</version>
    </dependency>
    <dependency>
      <groupId>org.mapstruct</groupId>
      <artifactId>mapstruct-processor</artifactId>
      <version>1.3.1.Final</version>
    </dependency>Code language: Java (java)

Adding configurations for DataSource

Add the following configurations in application.properties file to configure h2 database

# configure your datasource

quarkus.datasource.db-kind=h2
quarkus.datasource.username=username-default
quarkus.datasource.jdbc.url=jdbc:h2:mem:default

# log hibernate sql queries
quarkus.hibernate-orm.log.sql=true

# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-createCode language: Java (java)

Please visit link for the list of properties you can set in application.properties for  Hibernate ORM configuration

Define Entities

To define a Panache entity, simply extend PanacheEntity, annotate it with @Entity and add your columns as public fields.

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.

@Entity
public class Employee extends PanacheEntity {

    public String first_name;
    public String last_name;
    public String gender;
    public LocalDate birth_date;
    public LocalDate hire_date;

    @ManyToOne(fetch = FetchType.LAZY)
    public Department department;

    // return first_name as uppercase in the model
    public String getFirst_name(){
        return first_name.toUpperCase();
    }

    // store last_name in lowercase in the DB
    public void setLast_name(String name) {
        this.last_name = last_name.toLowerCase();
    }
    
  }Code language: Java (java)

Clearly, there are few differences compared to Hibernate ORM entities

  1. All the properties are public fields instead of private+getter+setter
  2. Entity class extends PanacheEntity class.
  3. There is no ID field defined in the Entity class

Hibernate ORM with Panache will actually generate any missing getter+setter for public fields, and it will replace all field accesses with accesses to the getters and setters.

 Due to field access rewrite feature in Quarkus, when Employee.first_name they will actually call your getFirst_name() 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.

Entities inherit ID property defined in PanacheEntity . So we are not defining the separate ID field.

ID field uses generated values from hibernate_sequence.

public abstract class PanacheEntity extends PanacheEntityBase {
    @Id
    @GeneratedValue
    public Long id;
Code language: Java (java)

Performing CRUD operations

Now lets perform some common operations like Create, Update, Delete operations on Entities.

PanacheEntity defines lot of useful static methods which helps us to perform common operations and also develop our own custom methods.

Retrieving Single Entity

Following methods can be used to retrieve the entities/records

  • findById(Object id) – Finds an entity by ID. returns the entity found, or null if not found.
  • findByIdOptional​(Object id) – Find an entity of this type by ID.if found, an optional containing the entity, else Optional.empty().
@ApplicationScoped
public class EmployeeService {

    @Inject
    EmployeeMapper employeeMapper;

    public EmployeeDto getEmployee(Long id) {
        return employeeMapper.toEmployeeDto(Employee.findById(id));
    }
...
}Code language: Java (java)
@ApplicationScoped
public class DepartmentService {

    @Inject
    DepartmentMapper departmentMapper;

    public DepartmentDto getDepartment(Long id) {
        Optional<Department> optionalDepartment = Department.findByIdOptional(id);
        Department department = optionalDepartment.orElseThrow(NotFoundException::new);
        return departmentMapper.toDepartmentDto(department);
    }
}Code language: Java (java)

Retrieving List of Entities

Following methods can be used to retrieve the entity

  •  findAll().list() – Find all entities of this type
  • List listAll() – Find all entities of this type. This method shortcut for above method
public List<EmployeeDto> getAllEmployees() {
        return employeeMapper.toEmployeeList(Employee.listAll());
    }Code language: Java (java)


public List<EmployeeDto> getAllEmployees() {
        return employeeMapper.toEmployeeList(Employee.findAll().list());
    }

Code language: Java (java)

Create/Save Entity

Following methods can be used to save an entity.

  • persist() – Persist this entity in the database, if not already persisted. This will set your ID field if it is not already set.
  • persistAndFlush() – Persist this entity in the database, if not already persisted. This will set your ID field if it is not already set. Then flushes all pending changes to the database.
  • persist​(Object firstEntity, Object… entities)
  • persist​(Stream entities)
  • persist​(Iterable entities)

First 2 methods are called on Entity object and next 3 methods are static methods called on Entity class

    @Transactional
    public EmployeeDto createEmployee(EmployeeDto employee) {

        Employee entity = employeeMapper.toEmployeeEntity(employee);
        Employee.persist(entity);
      
        if(entity.isPersistent()) {
         Optional<Employee> optionalEmp = Employee.findByIdOptional(entity.id);
         entity = optionalEmp.orElseThrow(NotFoundException::new);
         return employeeMapper.toEmployeeDto(entity);
        } else {
            throw new PersistenceException();
        }

    }
Code language: Java (java)

Entity can also be persisted in following ways

Using persistAndFlush method

 Employee entity = employeeMapper.toEmployeeEntity(employee);
 entity.persistAndFlush();Code language: Java (java)

Using persist method

Employee entity = employeeMapper.toEmployeeEntity(employee);
entity.persist();Code language: Java (java)

Update Entity

There is no direct merge method available in Panache Entity, If you update values of persisted entity, the changes will be flushed automatically

@Transactional
    public Employee updateEmployee(Long id, EmployeeDto employee) {
        Employee entity  = Employee.findById(id);
        if(entity == null) {
            throw new WebApplicationException("Employee with id of " + id + " does not exist.", 404);
        }

       
        entity.last_name = employee.getLast_name() ;
        entity.first_name = employee.getFirst_name();
        entity.birth_date = employee.getBirth_date();
        entity.hire_date = employee.getHire_date();

        return entity;
    }Code language: Java (java)

If you are using MapStruct, it can be written in following way

 @Transactional
    public EmployeeDto updateEmployee(Long id, EmployeeDto employee) {
        Employee entity  = Employee.findById(id);
        if(entity == null) {
            throw new WebApplicationException("Employee with id of " + id + " does not exist.", 404);
        }
        employeeMapper.updateEmployeeEntityFromDto(employee,entity);

        return employeeMapper.toEmployeeDto(entity);
    }Code language: Java (java)

We can also get hold of EntityManager object using ‘getEntityManager( )` static method on entity class and call merge method to update the entity.

 @Transactional
    public EmployeeDto updateEmployee(EmployeeDto employee) {
        Employee entity  = Employee.findById(employee.getId());
        if(entity == null) {
            throw new WebApplicationException("Employee with id " + employee.getId() + " does not exist.", 404);
        }
        employeeMapper.updateEmployeeEntityFromDto(employee,entity);
        entity =  Employee.getEntityManager().merge(entity);
        return employeeMapper.toEmployeeDto(entity);
    }
Code language: Java (java)

Delete Entity

Following methods can be used to delete an entity from database.

  • delete() – Delete this entity from the database, if it is already persisted.
  • deleteById​(Object id) -Delete an entity of this type by ID.

Using deleteById method

@Transactional
    public Response deleteEmployee(Long id) {

       boolean isEntityDeleted = Employee.deleteById(id);
        if(!isEntityDeleted) {
            throw new WebApplicationException("Employee with id of " + id + " does not exist.", 404);
        }
                
        return Response.status(204).build();

    }Code language: Java (java)

Using delete method

@Transactional
    public Response deleteEmployee(Long id) {

        Employee emp  = Employee.findById(id);
        if(emp == null) {
            throw new WebApplicationException("Employee with id of " + id + " does not exist.", 404);
        }
        emp.delete();
        return Response.status(204).build();

    }Code language: Java (java)

Writing custom queries

Panache entity provides many static methods which takes HQL or JPQL query as parameter for writing custom queries.You can following methods or their corresponding overloaded methods to perform custom query operations.

  • find​(String query, Object… params) – Find entities using a query, with optional indexed parameters.
  • list​(String query, Object… params) – Find entities matching a query, with optional indexed parameters.
  • update​(String query, Object… params) – Update all entities of this type matching the given query, with optional indexed parameters.
  • update​(String query, Object… params) – Update all entities of this type matching the given query, with optional indexed parameters.

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

But Panache does not require verbose queries, it can form the query based on the context.

Following are rules how panache forms the query

If select query does not start with from, Panache supports 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>

If update query does not start with update from, Panache support the following additional forms:

  • from EntityName ... which will expand to update from EntityName ...
  • set? <singleColumnName> (and single parameter) which will expand to update from EntityName set <singleColumnName> = ?
  • set? <update-query> will expand to update from EntityName set <update-query> = ?

If delete query does not start with delete from, Panache support the following additional forms:

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

For example, we can write custom queries in Employee entity like below

@Entity
public class Employee extends PanacheEntity {

    ........  
// find all employees belonging to given department
    public static List<Employee> findEmployeesByDepartmentId(Long departmentId) {
        return find("department.id", departmentId).list();
    }
     
// search for employees by name
    public static List<Employee> searchEmpsByName(String name) {
      
        return find("first_name like CONCAT('%',?1, '%') ", name).list();
    }


}Code language: Java (java)

Named queries

You can reference a named query instead of a (simplified) HQL query by prefixing its name with the ‘#’ character.

@Entity
@NamedQuery(name = "Employee.getByName", query = "from Employee where name = ?1")
public class Employee extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Employee findByName(String name){
        return find("#Employee.getByName", name).firstResult();
    }
}Code language: Java (java)

Custom IDs

In our example , we did not specify Id attribute for our entities as we are using inherited Id attribute from PanacheEntity. ID field uses generated values from hibernate_sequence. But there may be cases where you want to specify your own ID strategy , in those cases you should extend the entities from PanacheEntityBase instead of PanacheEntity. Then you just declare whatever ID you want as a public field.

@Entity
public class Employee extends PanacheEntityBase {

    @Id
    @SequenceGenerator(
            name = "employeeSequence",
            sequenceName = "employee_id_seq",
            allocationSize = 1,
            initialValue = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "employeeSequence")
    public Integer id;

    //...
}Code language: Java (java)

You can download source code for this blog post from GitHub

Similar Posts