Real Life Scenarios I: Avoiding Extra Scripts in Database Operations with JPA/Hibernate
When performing database operations with JPA/Hibernate, we all know how important an element database operations are facing. We must not ignore the importance of the ID field that determines whether the object should be updated with a new record or an existing record.
This article will address the correct use of ID field, which is an important part of avoiding errors that may be encountered in database operations. I will explain the errors you may encounter if the ID field is not null and how you can avoid them with real use cases.
Why Is ID Field Important?
When saving objects to the database, JPA/Hibernate understands whether this object is new or existing by looking at whether the id field is full and whether a record with that id exists:
When ID Field is Full: JPA/Hibernate considers this object as an existing record and performs an update.
When the ID Field is Empty: JPA/Hibernate treats this object as a new record and inserts it.
Misuse Example: Setting The ID Field In The Application Layer
import javax.persistence.*;
@Entity
@Table(name = "T_USER")
public class UserEntity {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME")
private String name;
@ManyToMany
@JoinTable(
name = "T_USER_ROLE_RELATION",
joinColumns = @JoinColumn(name = "USER_ID"),
inverseJoinColumns = @JoinColumn(name = "ROLE_ID")
)
private List<RoleEntity> roles;
@Column(name = "CREATED_AT")
private LocalDateTime createdAt;
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
...
}
...
List<RoleEntity> roleEntities = roleRepository.findAll();
UserEntity userEntity = new UserEntity();
userEntity.setId(UUID.randomUUID().toString());
userEntity.setName("Test User");
userEntity.setRoles(roleEntities);
userRepository.save(userEntity);
...
In this piece of code, the id field of the userEntity object is assigned a value in the application layer.
This value assignment may not normally cause any errors on the application side.
However, problems can occur when an n-to-n relationship is established between the UserEntity object and the RoleEntity object and a value assignment is desired with the @jakarta.persistence.PreUpdate
anotation.
When creating a user and also want to identify a role to the corresponding user, the JPA/Hibernate id field is full, so it will think that this object is already present in the database and will do both save and update.
This can cause a different situation than expected behavior.
SQL queries running while saving users as a result of misuse:
16:29:08.941 DEBUG org.hibernate.SQL -
select
asre1_0.ID,
asre1_0.NAME,
asre1_0.CREATED_AT,
asre1_0.UPDATED_AT
from
T_USER asre1_0
where
asre1_0.ID=?
16:29:29.746 DEBUG org.hibernate.SQL -
insert
into
USER
(ID, NAME, CREATED_AT, UPDATED_AT)
values
(?, ?, ?, ?)
16:29:29.797 DEBUG org.hibernate.SQL -
update
T_USER
set
NAME=?,
CREATED_AT=?,
UPDATED_AT=?
where
ID=?
16:29:29.826 DEBUG org.hibernate.SQL -
insert
into
T_USER_ROLE_RELATION
(USER_ID, ROLE_ID)
values
(?, ?)
Correct Use Case: Using @GeneratedValue Annotation
import java.util.UUID;
import javax.persistence.*;
@Entity
@Table(name = "T_USER")
public class UserEntity {
@Id
@Column(name = "ID")
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(name = "NAME")
private String name;
@ManyToMany
@JoinTable(
name = "T_USER_ROLE_RELATION",
joinColumns = @JoinColumn(name = "USER_ID"),
inverseJoinColumns = @JoinColumn(name = "ROLE_ID")
)
private List<RoleEntity> roles;
@Column(name = "CREATED_AT")
private LocalDateTime createdAt;
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
...
}
In this piece of code, the id field of the userEntity object is not assigned a value.
So how do we fix this error?
Instead of manually assigning the id
value in the application layer, we should use the @jakarta.persistence.GeneratedValue
anotion.
If the id
field is set to be automatically assigned with UUID, you do not need to manually set the id
value. In this case, JPA/Hibernate automatically creates a new UUID and adds the record.
In the example, JPA/Hibernate will create a new UUID and add the record, as the id
area of the UserEntity
class is automatically set with UUID. This is the most preferred and correct approach when adding new records.
Scripts that work while saving users with the correct use;
16:22:09.070 DEBUG org.hibernate.SQL -
insert
into
T_USER
(ID, NAME, CREATED_AT, UPDATED_AT)
values
(?, ?, ?, ?, ?, ?, ?)
16:22:09.111 DEBUG org.hibernate.SQL -
insert
into
T_USER_ROLE_RELATION
(USER_ID, ROLE_ID)
values
(?, ?)
Summary
Incorrect: Assigning the value of the
id
field in the Entity in the application layer. Not using the@jakarta.persistence.GeneratedValue
anotation.Correct: Using the
@jakarta.persistence.GeneratedValue
anotation and have the value assignment for theid
field done by JPA/Hibernate.
By using and following these principles correctly, you can accurately add or update existing records to the database when using JPA/Hibernate. This will help your application manage database operations correctly and efficiently, preventing unwanted queries from running.
You can access the actual use cases of the code pieces included in the article from the following project;