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 extendPanacheEntityBase
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-create
Code 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
- All the properties are public fields instead of private+getter+setter
- Entity class extends
PanacheEntity
class. - 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 tofrom EntityName order by ...
<singleColumnName>
(and single parameter) which will expand tofrom EntityName where <singleColumnName> = ?
<query>
will expand tofrom EntityName where <query>
If update query does not start with update from
, Panache support the following additional forms:
from EntityName ...
which will expand toupdate from EntityName ...
set? <singleColumnName>
(and single parameter) which will expand toupdate from EntityName set <singleColumnName> = ?
set? <update-query>
will expand toupdate 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 todelete from EntityName ...
<singleColumnName>
(and single parameter) which will expand todelete from EntityName where <singleColumnName> = ?
<query>
will expand todelete 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