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 Postwhich 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 PostCommentwhich 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 ValidityAuditStrategyas it can speed up changelog-related queries.

To enable the strategy ValidityAuditStrategyyou 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 postand 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 JpaRepositoryso 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 PostServicewhich 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_commentHibernate 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 REVINFOwhich 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 Postyou can use the method findLastChangeRevisionwhich 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 findRevisionswhich 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 PageRequestas 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

Similar Posts

Leave a Reply

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