Quarkus Tutorial

Configure JPA on Quarkus.io

Configure JPA on Quarkus.io
Quarkus Java JPA Hibernate

Quarkus Tutorial
(série de 3 partes)

How to set up a REST API using Quarkus.io
Configure JPA on Quarkus.io
Using Bean Validation on Quarkus.io

Warning This post was updated to use Java 19 with the latest Quarkus version.

JPA is a specification from Jakarta EE that controls the Data Access Layer. The most common implementation for it is Hibernate ORM.

Trade-offs

Choose technology involves a trade-off. I always thought that if I choose JPA, I will lose control of any generated SQL. And that is true!

JPA is an ORM, so most of the SQL is generated from a JPQL. If you do a bad design of your code, you will have SQL been generated at execution time, that is a bad design. In truth, horrible design.

But you will boost your coding speed. You will no more care about writing SQL or modelling the database. You can think in terms of objects, not tables. This does not mean that you will not care about modelling. In the first time is better to create the database model before, than design the objects according to what you want. This will avoid you creating some very commons Anti Patterns from JPA. I, have worked on a project where if you get a user from the database, you can load all objects associated with that user on the memory crashing the application.

It is important to know well the specification to create an optimal service, balancing database performance with development speed.

Advantages Aisadvantages
Fast Coding SQL is generated
Easy to change the model Lack of support for NoSQL ¹
Database agnostic  

¹ Use JNoSQL instead of JPA

Configuring JPA in an existent Quarkus Project

Configure dependencies

So the first step is to configure the dependencies. For adding JPA you will have to add the Hibernate Plugin and the Database JDBC plugin.

Why we have two dependencies? This is one of the code ideas of JPA, it is database agnostic. You can change your database with almost no change on your code.

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

If you want to know which database you can use, just search on mvnrepository.com.

Modeling the database

The next step we need to model the database. You probably have your database model using SQL, but you need to create Java POJOs that will map your data in your Java code.

For this example we will create a backend CRUD for User. That means we will create the Create, Read, Update and Delete endpoints for it.

So, to model we have to add the annotations:

Annotation Description
javax.persistence.Entity Specifies that the class is an entity.
javax.persistence.Table Specifies the primary table for the annotated entity.
javax.persistence.Id Specifies the primary key of an entity.
javax.persistence.Column Specifies the mapped column for a persistent property or field.

Not even all annotations are required, but you should do a good design of your code.

So I’m defining the User adding all required annotations, and some NamedQuery and a Builder. The builder is just a fancy code to improve the readability and the NamedQuery it will be used later to search for users.

package io.vepo.tutorial.quarkus.user;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

@Entity
@Table(name = "tb_users", uniqueConstraints = {
    @UniqueConstraint(name = "uq_users_username", columnNames = {
        "username" }),
    @UniqueConstraint(name = "uq_users_email", columnNames = {
        "email" })
})
@NamedQuery(name = "User.findByUsernameAndHashedPassword", query = "SELECT usr FROM User usr WHERE usr.username = :username AND usr.hashedPassword = :hashedPassword AND usr.enabled = true")
@NamedQuery(name = "User.findByUsername", query = "SELECT usr FROM User usr WHERE usr.username = :username")
public class User {
    public static final class UserBuilder {
        private String email;
        private String username;
        private String firstName;
        private String lastName;
        private boolean admin;
        private String hashedPassword;

        private UserBuilder() {
        }

        public UserBuilder email(String email) {
            this.email = email;
            return this;
        }

        public UserBuilder username(String username) {
            this.username = username;
            return this;
        }

        public UserBuilder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        public UserBuilder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

        public UserBuilder admin(boolean admin) {
            this.admin = admin;
            return this;
        }

        public UserBuilder hashedPassword(String hashedPassword) {
            this.hashedPassword = hashedPassword;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }

    public static final UserBuilder builder() {
        return new UserBuilder();
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column
    private String email;

    @Column
    private String username;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column
    private boolean admin;

    @Column(name = "hashed_password")
    private String hashedPassword;

    @Column(name = "enabled")
    private boolean enabled;

    public User() {
    }

    private User(UserBuilder builder) {
        email = builder.email;
        username = builder.username;
        firstName = builder.firstName;
        lastName = builder.lastName;
        admin = builder.admin;
        hashedPassword = builder.hashedPassword;
        enabled = true;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public boolean isAdmin() {
        return admin;
    }

    public void setAdmin(boolean admin) {
        this.admin = admin;
    }

    public String getHashedPassword() {
        return hashedPassword;
    }

    public void setHashedPassword(String hashedPassword) {
        this.hashedPassword = hashedPassword;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        User other = (User) obj;
        if (id == null) {
            if (other.id != null) {
                return false;
            }
        } else if (!id.equals(other.id)) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", email=" + email + ", username=" + username + ", firstName=" + firstName
                + ", lastName=" + lastName + ", admin=" + admin + ", hashedPassword=" + hashedPassword + ", enabled="
                + enabled + "]";
    }

}

I want to point out some important things on this code. The @Column annotation is not required, if you omit does not mean that JPA will ignore, it will only read the column with the same name from the field. If you want a not persisted column, you should use create a transient field. If you have a compound name for the field, for example, lastName, if you do not specify the name in @Column, JPA will search for lastName, not last_name. I don’t know if you have understood, but naming is important! All applications should follow naming conventions and the SQL Naming Convetions is different from Java Naming Convetions.

With the @Column you have more options, like required and unique. You can see all options on Jakarta Documentation.

For the id column, I have added two important annotations. @Id and @GeneratedValueare required to create a good design, but if you choose a different database probably it will have different values!

I have added the Unique Constraints, this is not required, but as I’m creating the database automatically, I have to do it. In a future tutorial I will teach how to use Flyway to create automatically the database using SQL.

Define the parameters

Hibernate requires few parameters to configure, you can put all in src/main/resources/application.properties. But you can also replace it using environment variables. This will take place later in a tutorial from Microprofile Config.

So we need to define the Database type and credentials and what Hibernate will do with the Schema. For production always use quarkus.hibernate-orm.database.generation=validate, but in our example, we will use drop-and-create recreating the database schema every deploy.

If you need to change more configurations properties, the Quarkus documentation has a list of all available configurations.

quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = postgres
quarkus.datasource.password = password
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/tutorial

quarkus.hibernate-orm.database.generation=drop-and-create

Creating the database

To create the database, we will use Docker. So just execute the line bellow.

docker run --rm --name postgres-db -e POSTGRES_PASSWORD=password -e POSTGRES_DB=tutorial -p 5432:5432 -d postgres:13-alpine

If you want to use Postgres directly, just download and install it. It will work fine.

Acessing the database

There is a lot of ways to access the database, but you should always use javax.persistence.EntityManager. I have created a service class called Users and added some methods to show the best way to access it.

package io.vepo.tutorial.quarkus.user;

import java.util.List;
import java.util.Objects;
import java.util.Optional;

import javax.enterprise.context.ApplicationScoped;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.transaction.Transactional;

@Transactional
@ApplicationScoped
public class Users {
    @PersistenceContext
    EntityManager em;

    public List<User> list() {
        CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
        CriteriaQuery<User> query = criteriaBuilder.createQuery(User.class);
        query.from(User.class);
        return em.createQuery(query).getResultList();
    }

    public User create(User user) {
        if (Objects.nonNull(user.getId())) {
            throw new IllegalStateException("Id should be null!");
        }
        em.persist(user);
        return user;
    }

    public Optional<User> findByUsername(String username) {
        TypedQuery<User> query = em.createNamedQuery("User.findByUsername", User.class);
        query.setParameter("username", username);
        return query.getResultStream().findFirst();
    }

    public User get(int userId) {
        return em.find(User.class, userId);
    }
}

Using Criteria Builder

Using Criteria Builder is a good way when you need to generate dynamically the query. You will create the query programmatically, the API is not so easy to understand because it is very powerful.

Using Named Query

For me, Named Queries is the best way to execute commons queries. You have to write the query on JPQL, not SQL. JPQL is very similar to SQL, but you can use the same query in different databases, Hibernate will translate it for you. It is better to use it than custom JPQL queries because it will compile it once.

Using Query

You can create the query directly. But this is not a good approach! Every time you call createQuery it will compile the query.

public Optional<User> findByUsername(String username) {
    TypedQuery<User> query = em.createQuery("SELECT usr FROM User usr WHERE usr.username = :username", User.class);
    query.setParameter("username", username);
    return query.getResultStream().findFirst();
}

Access the data directly

If you have a Class and an Id, you can access the object directly.

public User get(int userId) {
    return em.find(User.class, userId);
}

Transactional Objects

As we are using a transactional object, all changes we do in the JPA object it will be updated on the database. Each object read from the database is attached to the current session.

Integrating Endpoing and Database

The last step we need to add is injecting the pass Users inside our UserEndpoint as we show on the previous posts. So we need to use the Java CDI, inject it using the annotation @Inject.

@Path("/user")
@ApplicationScoped
public class UserEndpoing {
    @Inject
    Users users;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<User> list() {
        return users.list();
    }
}

Executing and Testing

With the database running you only need to start the Quarkus using maven.

mvn quarkus:dev

In our example, we have provided 3 endpoints:

You can find all code in the repository bellow.

And you can test it using Postman.

Run in Postman

Conclusion

If you want to develop fast and use databases, you should use JPA. You will not be worried about queries and knowing the JPA features and implementation, you can optimize the access from your database.

Future Readings

Quarkus Tutorial
(série de 3 partes)

How to set up a REST API using Quarkus.io
Configure JPA on Quarkus.io
Using Bean Validation on Quarkus.io
Licença Creative Commons
Este obra está licenciado com uma Licença Creative Commons Atribuição-NãoComercial-CompartilhaIgual 4.0 Internacional .
Escrito em 25/novembro/2020