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