Creating entity with includes nested data structures in Hibernate 6 (and in Spring Boot 3)
Sometimes your entity might need store nested data structures such as Map<String, Map<String, String> or list of list etc.. If you want to …
As developers, we often find ourselves faced with the challenge of managing multiple entities that share common attributes and behavior. By leveraging the power of abstraction and inheritance, we can simplify our codebase, enhance code reusability, and maintain a consistent structure across our project. In this article, I will guide you through the process of designing and implementing a base class(and also I will show you which fields base should have) for entities in your Spring Boot project.
If you only need to see the code, here is the github link
First create a new spring boot project with the following dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
Assume that we have Teacher
and Student
entities:
@Entity
@Table(name = "student")
@Getter
@Setter
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
private String surname;
}
@Entity
@Table(name = "teacher")
@Getter
@Setter
public class Teacher {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
private String surname;
}
with the following repository classes:
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
}
@Repository
public interface TeacherRepository extends JpaRepository<Teacher, Long> {
}
Whether you are using jpa or other solutions, some operations are common such as:
Let’s add these fields by one by
Before adding fields/columns, let’s create class called BaseEntity
and two entities will extend it:
@MappedSuperclass
public class BaseEntity {}
// ...
public class Student extends BaseEntity {}
public class Teacher extends BaseEntity {}
The JPA standard specification defines the @MappedSuperclass annotation to allow an entity to inherit properties from a base class. For more information check out my blog https://mehmetozanguven.com/jpa/jpa-fundamentals-and-hibernate-inheritance-strategies/#mappedsuperclass
To be able create&update (automatically), I will use @EntityListener
If you want to know more about entity listener, please check out https://mehmetozanguven.com/jpa/jpa-fundamentals-and-hibernate-13-entity-lifecycle-events/
This interface will include the setters and getters for our common operations
public interface Auditable {}
I will listen the entity events in this class
public class AuditableListener {
@PrePersist
void preCreate(Auditable auditable) {
// run operations before persisting entity
}
@PreUpdate
void preUpdate(Auditable auditable) {
// run operations before updating entity
}
}
The rest is just to update BaseEntity with annotation and interface
@MappedSuperclass
@EntityListeners(AuditableListener.class)
public class BaseEntity implements Auditable {
}
From now on, I can add common operation using Auditable interface and listener class:
All entities need versionId to avoid DataIntegration error. This error can be happened when entities can be updated more than one user.
For instance, one of the user(user-A) gets the current information from the database but before updating it, someone(user-B) can update before user-A finishes it. At the end user-A will override values set by user-B which leads to DataIntegration problem. To solve that error we simply add versionId to each field and update it every time entity is updated. And then, before one of the user updates the entity we can compare the versionId from the client and the value inside database. If they are not matched, we can throw DataIntegration error.
First let’s define the field (into the base entity) and getter/setter into the interface:
public interface Auditable {
void setEntityVersion(String versionId);
String getEntityVersion();
}
BaseEntity class:
@MappedSuperclass
@EntityListeners(AuditableListener.class)
public class BaseEntity implements Auditable {
private String versionId;
@Override
public void setEntityVersion(String versionId) {
this.versionId = versionId;
}
@Override
public String getEntityVersion() {
return versionId;
}
}
versionId should be updated/created every time before entity is persisted and updated. That’s why I have to update versionId on @PrePersist
and @PreUpdate
For the sake of simplicity, I will use
randomUUID
for versionId value
public class AuditableListener {
@PrePersist
void preCreate(Auditable auditable) {
auditable.setEntityVersion(UUID.randomUUID().toString());
}
@PreUpdate
void preUpdate(Auditable auditable) {
auditable.setEntityVersion(UUID.randomUUID().toString());
}
}
with this setup, now each entity (which extends base entity) will also have versionId field into the database.
I will apply the similar operation as the versionId.
First let’s define the field and getter/setter:
public interface Auditable {
void setEntityVersion(String versionId);
String getEntityVersion();
OffsetDateTime getCreateUTCTime();
void setCreateUTCTime(OffsetDateTime createTime);
}
BaseEntity class:
@MappedSuperclass
@EntityListeners(AuditableListener.class)
public class BaseEntity implements Auditable {
private String versionId;
@CreatedDate
private OffsetDateTime createTime;
// ...
@Override
public OffsetDateTime getCreateUTCTime() {
return createTime;
}
@Override
public void setCreateUTCTime(OffsetDateTime createTime) {
this.createTime = createTime;
}
}
createdTime should only be used/called on @PrePersist
:
public class AuditableListener {
@PrePersist
void preCreate(Auditable auditable) {
auditable.setCreateUTCTime(OffsetDateTime.now(ZoneId.of("UTC")));
auditable.setEntityVersion(UUID.randomUUID().toString());
}
@PreUpdate
void preUpdate(Auditable auditable) {
auditable.setEntityVersion(UUID.randomUUID().toString());
}
}
I will update the lastModifiedTime field on @PrePersist
and @PreUpdate
:
Auditable interface:
public interface Auditable {
void setEntityVersion(String versionId);
String getEntityVersion();
OffsetDateTime getCreateUTCTime();
void setCreateUTCTime(OffsetDateTime createTime);
OffsetDateTime getLastModifiedUTCTime();
void setLastModifiedUTCTime(OffsetDateTime lastModifiedTime);
}
BaseEntity:
@MappedSuperclass
@EntityListeners(AuditableListener.class)
public class BaseEntity implements Auditable {
// ...
@LastModifiedDate
private OffsetDateTime lastModifiedTime;
// ...
@Override
public OffsetDateTime getLastModifiedUTCTime() {
return lastModifiedTime;
}
@Override
public void setLastModifiedUTCTime(OffsetDateTime lastModifiedTime) {
this.lastModifiedTime = lastModifiedTime;
}
}
Finally AuditableListener:
public class AuditableListener {
@PrePersist
void preCreate(Auditable auditable) {
auditable.setCreateUTCTime(OffsetDateTime.now(ZoneId.of("UTC")));
auditable.setLastModifiedUTCTime(OffsetDateTime.now(ZoneId.of("UTC")));
auditable.setEntityVersion(UUID.randomUUID().toString());
}
@PreUpdate
void preUpdate(Auditable auditable) {
auditable.setLastModifiedUTCTime(OffsetDateTime.now(ZoneId.of("UTC")));
auditable.setEntityVersion(UUID.randomUUID().toString());
}
}
Right now, all entities have the common fields: createTime, lastModifiedTime and versionId. And these fields will automatically updated/created by the appropriate entity listener.
Sometimes your entity might need store nested data structures such as Map<String, Map<String, String> or list of list etc.. If you want to …
In this article, we are going to learn lifecycle events in the JPA. As of the most application we will have date_created and last_modified column to …