How to audit entity modifications using the JPA @EntityListeners, @Embedded, and @Embeddable annotations
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
In this article, we are going to see how we can use the @EntityListeners
, @Embedded
, and @Embeddable
annotations with JPA and Hibernate to audit entity modifications.
After I wrote the article about inheriting properties from a base class entity using @MappedSuperclass
, I got an avalanche of opinions, but this one from Lukas deserves a blog post:
Composition over inheritance. It seems to me that an embedded type would be a better fit
— Lukas Eder (@lukaseder) November 8, 2017
While @MappedSuperclass
has its benefit, allowing you to reuse even the @Id
mapping, as well as being more lenient towards Hibernate-specific auto-generated properties like @GeneratedValue
, using Embeddable types is the other JPA alternative for reusing a bunch of properties among multiple entities.
In this article, we are going to see how we can reuse several audit-related properties using @Embeddable
and another awesome JPA feature, @EntityListeners
.
Domain Model
Assuming we have the following tables in our relational database:
As you can see from the diagram above, all tables share the same four audit-based columns:
created_by
created_on
updated_by
updated_on
Therefore, we want to encapsulate these four entity properties in a reusable @Embedabble
type:
@Embeddable public class Audit { @Column(name = "created_on") private LocalDateTime createdOn; @Column(name = "created_by") private String createdBy; @Column(name = "updated_on") private LocalDateTime updatedOn; @Column(name = "updated_by") private String updatedBy; //Getters and setters omitted for brevity }
Now, to populate these properties automatically, we are going to use the following JPA entity event listener:
public class AuditListener { @PrePersist public void setCreatedOn(Auditable auditable) { Audit audit = auditable.getAudit(); if(audit == null) { audit = new Audit(); auditable.setAudit(audit); } audit.setCreatedOn(LocalDateTime.now()); audit.setCreatedBy(LoggedUser.get()); } @PreUpdate public void setUpdatedOn(Auditable auditable) { Audit audit = auditable.getAudit(); audit.setUpdatedOn(LocalDateTime.now()); audit.setUpdatedBy(LoggedUser.get()); } }
The LoggedUser
utility is described in this article, so I won’t repeat its definition here.
The Auditable
type is an interface that looks as follows:
public interface Auditable { Audit getAudit(); void setAudit(Audit audit); }
Our entities are going to implement the Auditable
interface so that the JPA event listener can locate the Audit
embeddable type and set the appropriate audit-based properties.
Now, to make the AuditListener
available to our entities, we are going to use the @EntityListeners
JPA annotation.
Therefore, our four JPA entities are going to look as follows.
Post entity
@Entity(name = "Post") @Table(name = "post") @EntityListeners(AuditListener.class) public class Post implements Auditable { @Id private Long id; @Embedded private Audit audit; private String title; @OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) private List<PostComment> comments = new ArrayList<>(); @OneToOne( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY ) private PostDetails details; @ManyToMany @JoinTable( name = "post_tag", joinColumns = @JoinColumn( name = "post_id" ), inverseJoinColumns = @JoinColumn( name = "tag_id" ) ) private List<Tag> tags = new ArrayList<>(); //Getters and setters omitted for brevity }
PostDetails entity
@Entity(name = "PostDetails") @Table(name = "post_details") @EntityListeners(AuditListener.class) public class PostDetails implements Auditable { @Id private Long id; @Embedded private Audit audit; @OneToOne(fetch = FetchType.LAZY) @MapsId private Post post; @Lob private byte[] image; //Getters and setters omitted for brevity }
We are using @MapsId
for the @OneToOne
mapping because it’s the best way to map a one-to-one table relationship.
PostComment entity
@Entity(name = "PostComment") @Table(name = "post_comment") @EntityListeners(AuditListener.class) public class PostComment implements Auditable { @Id @GeneratedValue(generator = "native") @GenericGenerator( name = "native", strategy = "native" ) private Long id; @Embedded private Audit audit; @ManyToOne private Post post; private String review; //Getters and setters omitted for brevity }
We are using the native
Hibernate-specific generator because, for MySQL, the AUTO
generator is to be avoided.
Tag entity
@Entity(name = "Tag") @Table(name = "tag") @EntityListeners(AuditListener.class) public class Tag implements Auditable { @Id private String name; @Embedded private Audit audit; //Getters and setters omitted for brevity }
Testing time
Now, when inserting three Tag
entities:
Tag jdbc = new Tag(); jdbc.setName("JDBC"); entityManager.persist(jdbc); Tag hibernate = new Tag(); hibernate.setName("Hibernate"); entityManager.persist(hibernate); Tag jOOQ = new Tag(); jOOQ.setName("jOOQ"); entityManager.persist(jOOQ);
Hibernate is going to issue the following SQL INSERT statements:
INSERT INTO tag ( created_by, created_on, updated_by, updated_on, name ) VALUES ( 'Alice', '2017-11-20 11:17:40.453', 'NULL(VARCHAR)', 'NULL(TIMESTAMP)', 'JDBC' ) INSERT INTO tag ( created_by, created_on, updated_by, updated_on, name ) VALUES ( 'Alice', '2017-11-20 11:17:40.473', 'NULL(VARCHAR)', 'NULL(TIMESTAMP)', 'Hibernate' ) INSERT INTO tag ( created_by, created_on, updated_by, updated_on, name ) VALUES ( 'Alice', '2017-11-20 11:17:40.473', 'NULL(VARCHAR)', 'NULL(TIMESTAMP)', 'jOOQ' )
Notice that the created_by
and created_on
have been properly populated by the AuditListener
.
When persisting a Post
along with its associated PostDetails
child entity:
Post post = new Post(); post.setId(1L); post.setTitle( "High-Performance Java Persistence, 1st Edition" ); PostDetails details = new PostDetails(); details.setImage(imageBytes); post.setDetails(details); post.getTags().add( entityManager.find(Tag.class, "JDBC") ); post.getTags().add( entityManager.find(Tag.class, "Hibernate") ); post.getTags().add( entityManager.find(Tag.class, "jOOQ") ); entityManager.persist(post);
Hibernate takes care of the audit-based columns:
INSERT INTO post ( created_by, created_on, updated_by, updated_on, title, id ) VALUES ( 'Alice', '2017-11-20 11:17:40.552', NULL(VARCHAR), NULL(TIMESTAMP), 'High-Performance Java Persistence, 1st Edition', 1 ) INSERT INTO post_details ( created_by, created_on, updated_by, updated_on, image, post_id ) VALUES ( 'Alice', '2017-11-20 11:17:40.56', NULL(VARCHAR), NULL(TIMESTAMP), [1, 2, 3, 4, 5, 6, 7, 8, 9], 1 ) INSERT INTO post_tag (post_id, tag_id) VALUES (1, 'JDBC') INSERT INTO post_tag (post_id, tag_id) VALUES (1, 'Hibernate') INSERT INTO post_tag (post_id, tag_id) VALUES (1, 'jOOQ')
When updating the Post
entity:
Post post = entityManager.find(Post.class, 1L); post.setTitle( "High-Performance Java Persistence, 2nd Edition" );
Hibernate populates the updated_by
and updated_on
columns as well:
UPDATE post SET created_by = 'Alice', created_on = '2017-11-20 11:17:40.552', updated_by = 'Alice', updated_on = '2017-11-20 11:17:40.605', title = 'High-Performance Java Persistence, 2nd Edition' WHERE id = 1
Great!
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
As demonstrated, JPA allows you to provide entity event listeners which you can register via the @EntityListeners
annotation. This way, we can encapsulate the audit-based properties in an @Embeddable
type and make it available to multiple entities using the @Embedded
annotation.
This way, you can reuse both the data structures (e.g. @Embeddable
) and behavior as well (e.g. @EntityListeners
).
