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 @GeneratedValue
are 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.
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:
- List all Users:
GET /user
- Create an User:
POST /user
- Find User by Username:
GET /user/{username}
You can find all code in the repository bellow.
vepo / quarkus-tutorial
This is a series of blog posts where I will create a tutorial of Quakus.io.
Quarkus Tutorial
Steps
1. Create a REST API
In the first example, we create a minimal REST API using Quarkus and JAX-RS.
2. Configure JPA Jakarta Persistence
In the second example, we add the persistence layer for our REST API.
3. Configure Jakarta Bean Validation
In the third example, we add the validation to all layers of our REST API.
And you can test it using 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.
Top comments (1)
Why don't you have a try github.com/braisdom/ObjectiveSql
Some comments have been hidden by the post's author - find out more