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. Hypersistence Optimizer is that tool!
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
).
