Spring Data Envers tutorial for beginners
Team Spring IO translated an article that is perfect for those who are not yet familiar with Spring Data Envers. This article explains with simple examples how to track data changes in an application using this tool.
Introduction
In this article, we'll take a look at the Spring Data Envers project and figure out how to get the most out of it.
Hibernate Envers is an extension to Hibernate ORM that allows you to track changes to entities with minimal changes at the application level.
Just as Envers integrates with Hibernate ORM to log entity changes, the Spring Data Envers project connects to Spring Data JPA to add change logging capabilities using JPA repositories.
Domain model
Let's say we have an entity Post
which is marked with annotation @Audited
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;
}
Essence Post
has a child entity PostComment
which is also annotated @Audited
:
@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;
}
Hidden text
As I explained in this articlewe will use the strategy ValidityAuditStrategy
as it can speed up changelog-related queries.
To enable the strategy ValidityAuditStrategy
you need to set the following Hibernate configuration property:
properties.setProperty(
EnversSettings.AUDIT_STRATEGY,
ValidityAuditStrategy.class.getName()
);
When generating a schema using hbm2ddl toolHibernate will create the following tables in the database:
Each time a transaction completes, a revision is created and stored in a table revinfo
.
Table post_aud
tracks changes to records in a table post
and the table post_comment_aud
stores change log information for a table post_comment
.
Spring Data Envers Repositories
The Spring Data Envers project provides an interface RevisionRepositorywhich your JPA repositories can extend to add the ability to query against change history.
For example, repository PostRepository
expands JpaRepository
from Spring Data JPA and RevisionRepository
from Spring Data Envers:
@Repository
public interface PostRepository extends JpaRepository<Post, Long>,
RevisionRepository<Post, Long, Long> {
}
Exactly the same PostCommentRepository
expands like JpaRepository
so RevisionRepository
from Spring Data Envers:
@Repository
public interface PostCommentRepository extends JpaRepository<PostComment, Long>,
RevisionRepository<PostComment, Long, Long> {
void deleteByPost(Post post);
}
On the service layer we have a class PostService
which provides methods for saving and deleting entities Post
And PostComment
. These methods will help us see how the change 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 parent entity Post
along with two child entities PostComment
:
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 will generate the following SQL queries:
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 performs INSERT queries on records in tables post
And post_comment
Hibernate Envers creates entries in tables REVINFO
, post_AUD
And post_comment_AUD
.
When an entity changes Post
:
post.setTitle("High-Performance Java Persistence 2nd edition");
postService.savePost(post);
Hibernate will generate the following queries:
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
Please note that a new entry has been created in REVINFO
which is associated with the entry in post_AUD
.
And when deleting an entity Post
:
postService.deletePost(post);
Hibernate will execute the following queries:
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
Interface RevisionRepository
from Spring Data Envers provides several methods for loading entity revisions.
For example, if you want to download the latest revision of an entity Post
you can use the method findLastChangeRevision
which is inherited from 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 you run the example, you will see the following message in the logs:
The latest Post entity operation was [DELETE] at revision [3]
To load all revisions for an entity, you can use the method findRevisions
which is also inherited from 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 you run this code, the following entries will appear in the logs:
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 page selection
Let's say we created several revisions for an entity Post
:
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 revisions with pagination using the method findRevisions(ID id, Pageable pageable)
.
For example, to get the first page with revisions in descending order, you could use PageRequest
as shown in the following example:
int pageSize = 10;
Page<Revision<Long, Post>> firstPage = postRepository.findRevisions(
post.getId(),
PageRequest.of(0, pageSize, RevisionSort.desc())
);
logPage(firstPage);
When we run this code for the first page we will see the following revisions:
Hidden text
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 revisions received for the second page:
Page<Revision<Long, Post>> secondPage = postRepository.findRevisions(
post.getId(),
PageRequest.of(1, pageSize, RevisionSort.desc())
);
logPage(secondPage);
The following entries will appear in the logs:
Hidden text
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
}
]
Great, right?
Conclusion
Although there are many CDC (Change Data Capture) solutions for tracking entity changes, Envers is probably the easiest option if you are already using Hibernate ORM.
And if you use Spring Data JPA, then using Spring Data Envers you can add the ability to work with revisions to your repositories.
Join the Russian-speaking community of Spring Boot developers in telegram – Spring IOto stay up to date with the latest news from the world of Spring Boot development and everything related to it.
We are waiting for everyone join us