Posted in SpringBoot
Home » Optimizing entities with JPA lifecycle events
In this article, we will explore how to leverage JPA entity lifecycle events, with a particular focus on @PrePersist
and @PreUpdate
, to enhance code readability and maintainability. By the end of this article, you will gain a clear understanding of how to utilize these lifecycle events to optimize your entities and improve overall performance.
Imagine we’re working on a project with multiple entities. In this scenario, we have two entities that are BusinessEntity
, a table that stores the registered businesses and the ServiceEntity
, a table that stores the services these businesses offer. Their implementation would look as follows.
@Getter
@Setter
@Entity
@Table(name = "business_tab")
@SQLRestriction("deleted_at is null")
@NoArgsConstructor
public class BusinessEntity {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", insertable = false)
private LocalDateTime updatedAt;
@Column(name = "deleted_at", nullable = true)
private LocalDateTime deletedAt;
@Column(nullable = false)
private String name;
@Column(nullable = true)
private String description;
@Column(name = "owner_id", nullable = false)
private String ownerId;
@Column(name = "path", nullable = false, unique = true)
private String path;
}
@Getter
@Setter
@Entity
@Table(name = "service_tab")
@SQLRestriction("deleted_at is null")
@NoArgsConstructor
public class ServiceEntity {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private int time;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", insertable = false)
private LocalDateTime updatedAt;
@Column(name = "deleted_at", nullable = true)
private LocalDateTime deletedAt;
}
As you can notice, entities typically share common information, such as the id, the creation date, the last updated date, and, if using soft deletion, the deletion date.
We need to refine our code to eliminate duplicate lines. We’ll achieve this by leveraging lifecycle entities, applying the object-oriented principle of inheritance, and utilizing JPA’s @MappedSuperclass
annotation.
First, we need to create a base class called BaseEntity
, which will contain the common information shared among our other entities.
Our other entities, BusinessEntity
and ServiceEntity
in this case, will extend the base class, inheriting its properties.
@Data
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 4871858767721157958L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", insertable = false)
private LocalDateTime updatedAt;
@Column(name = "deleted_at", nullable = true)
private LocalDateTime deletedAt;
But our work with the BaseEntity
is not over yet since we must use the @MappedSuperclass
annotation of JPA.
The @MappedSuperclass
annotation in Java is used to define a base class whose properties should be mapped to database columns in subclasses but doesn’t represent a database table itself.
When a class is annotated with @MappedSuperclass
, its fields are inherited by subclasses, and these fields are mapped to the corresponding columns in the database tables of the subclasses. You cannot perform queries directly on a @MappedSuperclass
. It’s intended only to be used as a common base class to avoid code duplication.
By using @MappedSuperclass
our BaseEntity
class would look as follows.
@Data
@MappedSuperclass
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 4871858767721157958L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", insertable = false)
private LocalDateTime updatedAt;
@Column(name = "deleted_at", nullable = true)
private LocalDateTime deletedAt;
}
Great! Now, our JPA entities look way cleaner.
@Getter
@Setter
@Entity
@Table(name = "business_tab")
@SQLRestriction("deleted_at is null")
@NoArgsConstructor
public class BusinessEntity extends BaseEntity {
private static final long serialVersionUID = 1L;
@Column(nullable = false)
private String name;
@Column(nullable = true)
private String description;
@Column(name = "owner_id", nullable = false)
private String ownerId;
@Column(name = "path", nullable = false, unique = true)
private String path;
}
Normally, we want to store the creation date every time a new entity is created but instead of adding it manually for each of our entities, we are going to leverage the JPA lifecycle events, specifically the @PrePersist
.
But, what are the JPA lifecycle events?
JPA lifecycle events are callback methods that allow us to hook into the lifecycle of an entity. These events occur at specific points in an entity’s life, such as when it is persisted, updated, removed, or loaded from the database. We can use annotations to mark methods in your entity classes that should be called when these events occur.
@PrePersist
is triggered before an entity is inserted into the database (before the persist
operation). It is very useful for initializing default values or setting timestamps. On the other hand, @PostPersist
is triggered after an entity is inserted into the database.
Moreover, @PreUpdate
is triggered before an entity is updated in the database (before the merge
operation). Similarly, @PostUpdate
is triggered after the entity is updated in the database.
@PreRemove
is triggered before an entity is removed from the database. It is often used for cleanup tasks or for setting a “soft delete” flag. On the contrary, @PostRemove
is triggered after the entity is removed from the database.
Finally, there is also @PostLoad
, which is triggered after an entity has been loaded from the database.
How can we leverage JPA lifecycle events?
We can utilize the @PrePersist
annotation to automatically set the creation date of an entity. The @PreUpdate
annotation will help us maintain the most recent update timestamp.
Finally, the BaseEntity
looks as follows and our other entities are clean and free of duplication.
@Data
@MappedSuperclass
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 4871858767721157958L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", insertable = false)
private LocalDateTime updatedAt;
@Column(name = "deleted_at", nullable = true)
private LocalDateTime deletedAt;
@PrePersist
protected void setCreatedAt() {
this.createdAt = LocalDateTime.now();
}
@PreUpdate
protected void setUpdatedAt() {
this.updatedAt = LocalDateTime.now();
}
}
Whenever a new entity is inserted or an existing one is updated, the corresponding dates will be automatically refreshed.
Using JPA lifecycle events such as @PrePersist
and @PreUpdate
improves our entity management by automating the handling of important timestamps and soft delete flags. We ensure that our entities always reflect accurate creation, update, and deletion information.
If you are interested in learning more about Spring Boot, check the Spring Boot section now.