Sample application that demonstrates entity revisions with Spring Data Envers.

Background

Spring Data Jpa provides rough audit information. However if you are looking for what are the exact changes being made to an entity you can do so with Spring Data Envers.

As the name has suggested Spring Data Envers utilises and simplifies the usage of Hibernate Envers.

Dependency and Configuration

In order to enable Envers features we will first include spring-data-envers as dependency.

implementation 'org.springframework.data:spring-data-envers'

Next is to inform Spring Boot that we would like do enable Envers' features. This can be done by annotating a @Configuration class with @EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class).

Example can be seen in RepositoryConfiguration:

@Configuration
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class, basePackages = "zin.rashidi.boot.data.envers")
class RepositoryConfiguration {
}

Enable Entity Audit

By annotating an @Entity with @Audited, we are informing Spring that we would like respective entity to be audited. The following example shows that we want all activities related to Book to be audited:

@Entity
@Audited
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    private String author;

    private String title;
}

Next is to extend a Repository class in order to allow us to utilise audit revision features. This can be done by extending RevisionRepository interface to our Repository class. An example can be seen in BookRepository:

public interface BookRepository extends JpaRepository<Book, Long>, RevisionRepository<Book, Long, Integer> {

}

Verification

We will be utilising on @SpringBootTest to verify that our implementation works.

Upon Creation an Initial Revision is Created

@Testcontainers
@SpringBootTest(properties = "spring.jpa.hibernate.ddl-auto=create-drop")
class BookAuditRevisionTests {

    @Container
    @ServiceConnection
    private static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:latest");

    @Autowired
    private BookRepository repository;

    @Test
    @DisplayName("When a book is created, then a revision information is available with revision number 1")
    void create() {
        var book = new Book();

        book.setTitle("The Jungle Book");
        book.setAuthor("Rudyard Kipling");

        var createdBook = repository.save(book);

        var revisions = repository.findRevisions(createdBook.getId());

        assertThat(revisions)
                .hasSize(1)
                .first()
                .extracting(Revision::getRevisionNumber)
                .returns(1, Optional::get);
    }

}

Revision Number Will Be Increase and Latest Revision is Available

@Testcontainers
@SpringBootTest(properties = "spring.jpa.hibernate.ddl-auto=create-drop")
class BookAuditRevisionTests {

    @Container
    @ServiceConnection
    private static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:latest");

    @Autowired
    private BookRepository repository;

    @Test
    @DisplayName("When a book is modified, then a revision number will increase")
    void modify() {
        var book = new Book();

        book.setTitle("The Jungle Book");
        book.setAuthor("Rudyard Kipling");

        var createdBook = repository.save(book);

        createdBook.setTitle("If");

        repository.save(createdBook);

        var revisions = repository.findRevisions(createdBook.getId());

        assertThat(revisions)
                .hasSize(2)
                .last()
                .extracting(Revision::getRevisionNumber)
                .extracting(Optional::get).is(matching(greaterThan(1)));
    }

}

Upon Deletion All Entity Information Will be Removed Except its ID

@Testcontainers
@SpringBootTest(properties = "spring.jpa.hibernate.ddl-auto=create-drop")
class BookAuditRevisionTests {

    @Container
    @ServiceConnection
    private static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:latest");

    @Autowired
    private BookRepository repository;

    @Test
    @DisplayName("When a book is removed, then only ID information is available")
    void remove() {
        var book = new Book();

        book.setTitle("The Jungle Book");
        book.setAuthor("Rudyard Kipling");

        var createdBook = repository.save(book);

        repository.delete(createdBook);

        var revision = repository.findLastChangeRevision(createdBook.getId());

        assertThat(revision).get()
                .extracting(Revision::getEntity)
                .extracting("id", "title", "author")
                .containsOnly(createdBook.getId(), null, null);
    }

}

All tests above can be found in BookAuditRevisionTests.