Creating base class (abstract entity class) for all entities in your Spring Boot project

  • |
  • 16 May 2023
Post image

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

Create Spring Boot Project

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>

Create entity classes

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> {
}

Common fields in entities

Whether you are using jpa or other solutions, some operations are common such as:

  • Setting createTime column
  • Setting lastModifiedTime column
  • Setting versionId

Let’s add these fields by one by

Create base class

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

EntityListener

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/

Create Auditable interface

This interface will include the setters and getters for our common operations

public interface Auditable {}

Create AuditableListener class

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
    }
}

Update BaseEntity with @EntityListener annotation and Auditable interface

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:

  • I will define getter and setter into Auditable interface
  • I will update/create the common field into the listener.

Add versionId field

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.

Add getter and setter

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;
    }
}

Add versionId logic into AuditableListener class

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.

Add createTime field

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());
    }
}

Add lastModifiedTime field

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.

You May Also Like