A beginner’s guide to Spring Data Envers
Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?
What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?
Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.
So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!
Introduction
In this article, we are going to investigate the Spring Data Envers project and see how to get the best out of it.
Hibernate Envers is a Hibernate ORM extension that allows us to track entity changes with almost no changes required on the application part.
Just like Envers plugs into Hibernate ORM in order to build an audit log for entity changes, the Spring Data Envers project plugs into Spring Data JPA to provide audit logging capabilities to JPA Repositories.
Domain Model
Let’s assume we have the following Post entity that’s mapped with the @Audited annotation from the Hibernate Envers project:
@Entity
@Table(
name = "post",
uniqueConstraints = @UniqueConstraint(
name = "UK_POST_SLUG",
columnNames = "slug"
)
)
@Audited
public class Post {
⠀
@Id
@GeneratedValue
private Long id;
⠀
@Column(length = 100)
private String title;
⠀
@NaturalId
@Column(length = 75)
private String slug;
⠀
@Enumerated(EnumType.ORDINAL)
@Column(columnDefinition = "NUMERIC(2)")
private PostStatus status;
}
The Post entity has a child PostComment entity that’s also annotated with the @Audited Hibernate Envers annotation:
@Entity
@Table(name = "post_comment")
@Audited
public class PostComment {
⠀
@Id
@GeneratedValue
private Long id;
⠀
@Column(length = 250)
private String review;
⠀
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey =
@ForeignKey(
name = "FK_POST_COMMENT_POST_ID"
)
)
private Post post;
}
As I explained in this article, we are going to use the
ValidityAuditStrategysince it can speed up the audit logging queries.To enable the
ValidityAuditStrategy, you can set the following Hibernate configuration property:properties.setProperty( EnversSettings.AUDIT_STRATEGY, ValidityAuditStrategy.class.getName() );
When generating the schema with the hbl2ddl tool, Hibernate generates the following database tables:

Whenever a transaction is committed, a revision is created and stored in the revinfo table.
The post_aud table tracks the post table record changes, and the post_comment_aud stores the audit logging information for the post_comment table.
Spring Data Envers Repositories
The Spring Data Envers project provides the RevisionRepository that your JPA Repository interfaces can extend in order to augment them with audit log querying options.
For example, the PostRepository extends the JpaRepository from Spring Data JPA and the RevisionRepository from the Spring Data Envers project:
@Repository
public interface PostRepository extends JpaRepository<Post, Long>,
RevisionRepository<Post, Long, Long> {
}
The same goes for the PostCommentRepository, which extends both the default JpaRepository and the Spring Data Envers RevisionRepository:
@Repository
public interface PostCommentRepository extends JpaRepository<PostComment, Long>,
RevisionRepository<PostComment, Long, Long> {
void deleteByPost(Post post);
}
On the service layer, we have the following PostService that provides us methods to save and delete the Post and PostComment entities so that we can see how the Envers audit logging mechanism works:
@Transactional(readOnly = true)
public class PostService {
⠀
@Autowired
private PostRepository postRepository;
⠀
@Autowired
private PostCommentRepository postCommentRepository;
⠀
@Transactional
public Post savePost(Post post) {
return postRepository.save(post);
}
⠀
@Transactional
public Post savePostAndComments(Post post, PostComment... comments) {
post = postRepository.save(post);
⠀
if(comments.length > 0) {
postCommentRepository.saveAll(Arrays.asList(comments));
}
⠀
return post;
}
⠀
@Transactional
public void deletePost(Post post) {
postCommentRepository.deleteByPost(post);
postRepository.delete(post);
}
}
Tracking INSERT, UPDATE, and DELETE operations
When creating a Post parent entity along with two PostComment child entities:
Post post = new Post()
.setTitle("High-Performance Java Persistence 1st edition")
.setSlug("high-performance-java-persistence")
.setStatus(PostStatus.APPROVED);
postService.savePostAndComments(
post,
new PostComment()
.setPost(post)
.setReview("A must-read for every Java developer!"),
new PostComment()
.setPost(post)
.setReview("Best book on JPA and Hibernate!")
);
Hibernate generates the following SQL statements:
SELECT nextval('post_SEQ')
SELECT nextval('post_comment_SEQ')
SELECT nextval('post_comment_SEQ')
INSERT INTO post (slug,status,title,id)
VALUES (
'high-performance-java-persistence', 1,
'High-Performance Java Persistence 1st edition', 1
)
INSERT INTO post_comment (post_id,review,id)
VALUES (
1, 'A must-read for every Java developer!', 1
),(
1, 'Best book on JPA and Hibernate!', 2
)
SELECT nextval('REVINFO_SEQ')
INSERT INTO REVINFO (REVTSTMP,REV)
VALUES (1726724588078, 1)
INSERT INTO post_AUD (
REVEND,REVTYPE,slug,status,
title,REV,id
)
VALUES (
null, 0, 'high-performance-java-persistence', 1,
'High-Performance Java Persistence 1st edition', 1, 1
)
INSERT INTO post_comment_AUD (
REVEND,REVTYPE,post_id,
review,REV,id
)
VALUES (
null, 0, 1,
'A must-read for every Java developer!', 1, 1
), (
null, 0, 1,
'Best book on JPA and Hibernate!', 1, 2
)
While Hibernate ORM executes the INSERT statements for the post and psot_comment table records, Hibernate Envers creates the REVINFO and the post_AUD and post_comment_AUD audit log entries.
When changing the Post entity:
post.setTitle("High-Performance Java Persistence 2nd edition");
postService.savePost(post);
Hibernate generates the following statements:
SELECT p1_0.id,p1_0.slug,p1_0.status,p1_0.title
FROM post p1_0
WHERE p1_0.id = 1
UPDATE post
SET
status = 1,
title = 'High-Performance Java Persistence 2nd edition'
WHERE id = 1
SELECT nextval('REVINFO_SEQ')
INSERT INTO REVINFO (REVTSTMP,REV)
VALUES (1726724799884, 2)
INSERT INTO post_AUD (
REVEND,REVTYPE,slug,status,
title,REV,id
)
VALUES (
null, 1, 'high-performance-java-persistence', 1,
'High-Performance Java Persistence 2nd edition', 2, 1
)
UPDATE post_AUD
SET REVEND = 2
WHERE
id = 1 AND
REV <> 2 AND
REVEND IS NULL
Notice that a new REVINFO was created, which has a post_AUD entry associated with it.
And, when deleting the Post entity:
postService.deletePost(post);
Hibernate will execute the following statements:
SELECT pc1_0.id,pc1_0.post_id,pc1_0.review
FROM post_comment pc1_0
WHERE pc1_0.post_id = 1
SELECT p1_0.id,p1_0.slug,p1_0.status,p1_0.title
FROM post p1_0
WHERE p1_0.id = 1
DELETE FROM post_comment WHERE id = 1
DELETE FROM post_comment WHERE id = 2
DELETE FROM post WHERE id = 1
INSERT INTO REVINFO (REVTSTMP,REV)
VALUES (1726724982890, 3)
INSERT INTO post_comment_AUD (
REVEND,REVTYPE,post_id,review,REV,id
)
VALUES (
null, 2, null, null, 3, 1
), (
null, 2, null, null, 3, 2
)
INSERT INTO post_AUD (
REVEND,REVTYPE,slug,status,title,REV,id
)
VALUES (
null, 2, null, null, null, 3, 1
)
UPDATE post_comment_AUD
SET REVEND = 3
WHERE
id = 1 AND
REV <> 3 AND
REVEND IS NULL
UPDATE post_comment_AUD
SET REVEND = 3
WHERE
id = 2 AND
REV <> 3 AND
REVEND IS NULL
UPDATE post_AUD
SET REVEND = 3
WHERE
id = 1 AND
REV <> 3 AND
REVEND IS NULL
Loading revisions using Spring Data Envers
The RevisionRepository from Spring Data Envers provides several methods we can use to load entity revisions.
For instance, if you want to load the latest revision of the Post entity, we can use the findLastChangeRevision method that we inherited from the RevisionRepository:
Revision<Long, Post> latestRevision = postRepository
.findLastChangeRevision(post.getId())
.orElseThrow();
LOGGER.info(
"The latest Post entity operation was [{}] at revision [{}]",
latestRevision.getMetadata().getRevisionType(),
latestRevision.getRevisionNumber().orElseThrow()
);
When running the above example, we get the following log message:
The latest Post entity operation was [DELETE] at revision [3]
To load all the revisions for a given entity, we can use the findRevisions method that we inherited from the RevisionRepository:
for(Revision<Long, Post> revision : postRepository
.findRevisions(post.getId())) {
LOGGER.info(
"At revision [{}], the Post entity state was: [{}]",
revision.getRevisionNumber().orElseThrow(),
revision.getEntity()
);
}
When running the above test case, we get the following log entries:
At revision [1], the Post entity state was: [{
id = 1,
title = 'High-Performance Java Persistence 1st edition',
slug = 'high-performance-java-persistence',
status = APPROVED
}]
At revision [2], the Post entity state was: [{
id = 1,
title = 'High-Performance Java Persistence 2nd edition',
slug = 'high-performance-java-persistence',
status = APPROVED
}]
At revision [3], the Post entity state was: [{
id = 1,
title = null,
slug = null,
status = null
}]
Loading revisions using pagination
Assuming we have created multiple revisions for a given Post entity:
Post post = new Post()
.setTitle("Hypersistence Optimizer, version 1.0.0")
.setSlug("hypersistence-optimizer")
.setStatus(PostStatus.APPROVED);
postService.savePost(post);
for (int i = 1; i < 20; i++) {
post.setTitle(
String.format(
"Hypersistence Optimizer, version 1.%d.%d",
i/10,
i%10
)
);
postService.savePost(post);
}
We can load the revisions using pagination via the findRevisions(ID id, Pageable pageable) method.
For instance, if we want to get the first page when loading the revisions in descending order, we can use the PageRequest as illustrated by the following example:
int pageSize = 10;
Page<Revision<Long, Post>> firstPage = postRepository
.findRevisions(
post.getId(),
PageRequest.of(0, pageSize, RevisionSort.desc())
);
logPage(firstPage);
When running the above test case, we get the following revisions for the first page:
At revision [23], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.9', slug='hypersistence-optimizer', status=APPROVED}]
At revision [22], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.8', slug='hypersistence-optimizer', status=APPROVED}]
At revision [21], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.7', slug='hypersistence-optimizer', status=APPROVED}]
At revision [20], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.6', slug='hypersistence-optimizer', status=APPROVED}]
At revision [19], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.5', slug='hypersistence-optimizer', status=APPROVED}]
At revision [18], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.4', slug='hypersistence-optimizer', status=APPROVED}]
At revision [17], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.3', slug='hypersistence-optimizer', status=APPROVED}]
At revision [16], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.2', slug='hypersistence-optimizer', status=APPROVED}]
At revision [15], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.1', slug='hypersistence-optimizer', status=APPROVED}]
At revision [14], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.1.0', slug='hypersistence-optimizer', status=APPROVED}]
When logging the revisions fetched by the second page:
Page<Revision<Long, Post>> secondPage = postRepository
.findRevisions(
post.getId(),
PageRequest.of(1, pageSize, RevisionSort.desc())
);
logPage(secondPage);
We will get the following entries printed to the log:
At revision [13], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.9', slug='hypersistence-optimizer', status=APPROVED}]
At revision [12], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.8', slug='hypersistence-optimizer', status=APPROVED}]
At revision [11], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.7', slug='hypersistence-optimizer', status=APPROVED}]
At revision [10], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.6', slug='hypersistence-optimizer', status=APPROVED}]
At revision [09], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.5', slug='hypersistence-optimizer', status=APPROVED}]
At revision [08], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.4', slug='hypersistence-optimizer', status=APPROVED}]
At revision [07], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.3', slug='hypersistence-optimizer', status=APPROVED}]
At revision [06], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.2', slug='hypersistence-optimizer', status=APPROVED}]
At revision [05], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.1', slug='hypersistence-optimizer', status=APPROVED}]
At revision [04], the Post entity state was: [Post{id=2, title='Hypersistence Optimizer, version 1.0.0', slug='hypersistence-optimizer', status=APPROVED}]
Awesome, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
While there are plenty of CDC (Change Data Capture) options we can use to track entity changes, Envers is probably the easiest one if you are already using Hibernate ORM.
And if you are using Spring Data JPA, then you can also use the Spring Data Envers extension to augment your Repository objects with revision loading capabilities.






