Serengeti logo BLACK white bg w slogan
Menu

Handling Persistence Layer with Quarkus

Irem Aktas, Senior Software Developer
28.05.2024.

In our previous blog, we introduced Quarkus and mentioned how easy a project can be created with it.

Before starting to describe panache, we need to remember why Java needs ORM (Object Relational Mapping) since Java has an object-oriented structure and is used in the application layer. It manipulates classes, interfaces, abstract classes, attributes, methods, collections, etc. to be used. On the other hand, data needs to be stored in somewhere, for example in relational databases, NoSQL databases, cloud-DBs or file system. ORM sits between the application layer and the relational database layer, and provides data exchange between these layers.

Relational Databases are still heavily used in Java Enterprise Applications. In the evolving enterprise world, persistence and efficient data integration play a crucial role in building robust and scalable system solutions.  Quarkus Panache framework is a great alternative to simplify data integration and works directly with Jakarta EE, Jakarta Persistence and Hibernate ORM. It has a solution for Relational Database Mapping and MongoDB, and allows developers to focus on the core business logic rather than implementing the persistence layer.

ORM Architecture

quarks 1

A simplified ORM top-to-bottom layered architecture is as shown above. Additionally, Entity Manager is the key component of orchestrating entities, managing entity state and life cycle. Panache abstracts the Entity Manager and provides two patterns; one is an active record pattern, and the other one is a panache repository pattern.

Panache Repository Pattern:

In the panache repository pattern, you define a repository interface for each entity you want to manage. This interface extends PanacheRepositoryBase, which is a base class provided by Quarkus.

Quarkus automatically implements CRUD operations and queries related to the entity based on conventions, reducing boilerplate code. You can also add additional queries as per your business use case needs.

Let’s see the usage in the code snippet below:

Lead.class

import jakarta.persistence.Column;
import jakarta.persistence.Entity; 
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor

@AllArgsConstructor
public class Lead {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "first_name", length = 50, nullable = false)
    private String firstName;

    @Column(name = "last_name", length = 50, nullable = false)
    private String lastName;

LeadRepository.class

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import org.eaetirk.efd.lead.constant.LeadAPIConstant;
import org.eaetirk.efd.lead.model.Lead;

import java.util.List;
@ApplicationScoped

public class LeadRepository implements PanacheRepository<Lead> {
    public List<Lead> findLeadByFirstName(String firstName){
       return 
find(LeadAPIConstant.QUERY_LIST_BY_FIRSTNAME_ORDER_BY_LAST_NAME, firstName.toLowerCase()).stream().toList();
   }
}

The query used above the find function is:

"LOWER(firstName) = ?1 ORDER BY lastName";

Let’s create a test class and see the usage:

import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import org.eaetirk.efd.lead.model.Lead;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@QuarkusTest
public class LeadRepositoryTest {
@Inject
LeadRepository leadRepository;
@Test
@TestTransaction
public void testLeadPersists(){
    Lead lead = new Lead("Severus", "Snape");
    leadRepository.persist(lead);
    assertNotNull(lead.getId());
 }
}
Lead.class

import jakarta.persistence.Column;
import jakarta.persistence.Entity; 
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@Entity

@NoArgsConstructor

@AllArgsConstructor
public class Lead {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "first_name", length = 50, nullable = false)
    private String firstName;

    @Column(name = "last_name", length = 50, nullable = false)
    private String lastName;

LeadRepository.class

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import org.eaetirk.efd.lead.constant.LeadAPIConstant;
import org.eaetirk.efd.lead.model.Lead;

import java.util.List;

@ApplicationScoped

public class LeadRepository implements PanacheRepository<Lead> {
    public List<Lead> findLeadByFirstName(String firstName){
       return 
find(LeadAPIConstant.QUERY_LIST_BY_FIRSTNAME_ORDER_BY_LAST_NAME, firstName.toLowerCase()).stream().toList();
   }
}

The query used above the find function is:

"LOWER(firstName) = ?1 ORDER BY lastName";

Let’s create a test class and see the usage:

import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import org.eaetirk.efd.lead.model.Lead;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@QuarkusTest
public class LeadRepositoryTest {

@Inject
LeadRepository leadRepository;

@Test
@TestTransaction
public void testLeadPersists(){
    Lead lead = new Lead("Severus", "Snape");
    leadRepository.persist(lead);
    assertNotNull(lead.getId());
 }
}

LeadRepository implements PanacheRepository and inherits all CRUD and query operations from PanacheRepository for Lead entity.

Panache Active Repository Pattern:

In this pattern, you define entity classes by extending from PanacheEntityBase. These entity classes act as active records which contain both data and behavior. In this pattern, you don’t need to create repository interface.

In this way, you have direct access to domain objects when calling a CRUD operation.

Let’s see the entity implementation:

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;

@Entity

@NoArgsConstructor

@AllArgsConstructor

public class Lead extends PanacheEntity {

    @Column(name = "first_name", length = 50, nullable = false)
    public String firstName;

    @Column(name = "last_name", length = 50, nullable = false)
    public String lastName;

}

Let’s create a test class and see the active record usage.

import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import org.eaetirk.efd.lead.model.Lead;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@QuarkusTest
public class LeadRepositoryTest {
@Test
@TestTransaction

public void testLeadPersists(){
    Lead lead = new Lead("Severus", "Snape");
    Lead.persist(lead);
    assertNotNull(lead.id);
 }
}

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;

@Entity

@NoArgsConstructor

@AllArgsConstructor

public class Lead extends PanacheEntity {

    @Column(name = "first_name", length = 50, nullable = false)
    public String firstName;

    @Column(name = "last_name", length = 50, nullable = false)
    public String lastName;

}

Let’s create a test class and see the active record usage.

import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import org.eaetirk.efd.lead.model.Lead;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@QuarkusTest
public class LeadRepositoryTest {

@Test
@TestTransaction

public void testLeadPersists(){
    Lead lead = new Lead("Severus", "Snape");
    Lead.persist(lead);
    assertNotNull(lead.id);
 }
}

The lead entity extends from PanacheEntity and inherits the id attribute from the parent class. The PanacheEntity class inherits all CRUD and query operations from PanacheEntityBase.

Both patterns aim to reduce boilerplate code inheriting default CRUD and query implementations from their Base class. The Panache repository pattern is more suitable for applications where there is a clear separation between data access and domain model. The panache active repository pattern, on the other hand, is preferred by developers who want to work with active records and want to encapsulate domain and behavior within an entity class. The tight coupling between the entity and the persistence layer can create a violation of the single responsibility principle, which definitely needs to be considered when deciding on a panache entity.

Configuration:

When you add the necessary extensions to your application, Quarkus handles database configuration and starts the database in a docker container when you start the application in development mode.

Here are the mvn wrapper commands for the related extensions to be run in the project directory:

./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-hibernate-orm-panache"

./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-jdbc-postgresql"

In our example, we used postgresql, but Quarkus offers many jdbc extensions:

image 11

Quarkus supports default property profiles for dev, test and prod. To make your unit tests faster, you can switch your application with an in-memory DB which is h2.  

After adding the h2 extension for test, you should go to pom.xml and change dependency scope as test as such:

./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-jdbc-h2"

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jdbc-h2</artifactId>
  <scope>test</scope>
</dependency>

Here is the application.properties configuration for the test profile

#Test Profile
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:lead_DB;DB_CLOSE_DELAY=-1
%test.quarkus.hibernate-orm.log.sql=true

Here is the application.properties configuration for the dev profile

%dev.quarkus.datasource.devservices.port=5432
%dev.quarkus.datasource.devservices.image-name=postgres:14

With the above configuration, we stated that the postgresql container running on docker will always expose the connection url with port number 5432.

The second configuration states that we are going to work with postressql version 14.

Here is the postgressql container created by Quarkus after running the application in development mode:

image 12

Conclusion

In this chapter, we tried to describe the ORM architecture and how Quarkus manages the persistence layer.

We mentioned how easy it is to create DB configurations with Quarkus after adding the necessary extensions.

We have covered the Active Record and Repository patterns used for the persistence layer in Java applications.

Understanding the power and the limitations of these persistence layer patterns is crucial for us as developers to make efficient architectural decisions.

Thank you for reading 😊

References:

https://quarkus.io/guides/getting-started-dev-services

Using Hibernate ORM and Jakarta Persistence - Quarkus

Let's do business

The project was co-financed by the European Union from the European Regional Development Fund. The content of the site is the sole responsibility of Serengeti ltd.
cross