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

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

Enter your email address to follow this blog and receive notifications of new posts by email.

Advertisements

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

  1. Great post, Vlad!

    Although your solution was VERY cool, the most interesting part of the post is not using an embedded object with @Embedable annotation, but using Polymorphism to solve the auditing issue in an elegant way. This is the way (using OO) we teach our students during our course “Persistence with JPA 2 and Hibernate” at TriadWorks.

    By the way, congrats for being a Java Champion!

  2. Hello Vlad,
    Nice example.
    I refactor one of my projects to use this solution instead of extending a super class with the audit fields.
    The disadvantage of this is the fact that every entity has to declare the annotation @EntityListeners
    Having it declared on the interfaces didn’t make a difference.

  3. I kind of prefer making Auditable an abstract class and extending it from entities. That way you won’t have to implement getAudit and setAudit methods in all auditable entities. That is until your entities do not need to extend any other base class.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s