How to audit entity modifications using the JPA @EntityListeners, @Embedded, and @Embeddable annotations

(Last Updated On: January 4, 2018)

Introduction

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:

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 setUpdadtedOn(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).

Subscribe to our Newsletter

* indicates required
10 000 readers have found this blog worth following!

If you subscribe to my newsletter, you'll get:
  • A free sample of my Video Course about running Integration tests at warp-speed using Docker and tmpfs
  • 3 chapters from my book, High-Performance Java Persistence, 
  • a 10% discount coupon for my book. 
Get the most out of your persistence layer!

Advertisements

14 thoughts on “How to audit entity modifications using the JPA @EntityListeners, @Embedded, and @Embeddable annotations

  1. Hi Vlad,

    Will this approach of using EntityListeners work if we use session and sessionfactory instead of entityManager?

  2. Hello Vlad,
    I left a comment a few days ago about a bidirectional that triggers some unexpected behaviour that generates many updates but it seems that the comment is gone. I think I found the reason for the behaviour. Instead of the logged ID for audit I used a Supplier object. My guess is that it was lazily evaluated and hibernate sees the object as modified. Am I correct? Thanks

    1. It could be. That’s also true if you change collections by reassigning to a different Object.

    1. Only if you turn the @ManyToMany into 2 bidirectional @OneToMany as explained in this article, you will get that.

      Otherwise, you could use Envers and have real-audit logging, not just the creation and modification timestamps.

Leave a Reply

Your email address will not be published. Required fields are marked *