Data auditing is a basic capability of the business system. It requires the system to save all the change records of key data and support the query of the change records.
Saving and querying data change records can be easily achieved through spring-data-envers.
In some cases, we need to save only the data change records that meet specific conditions, and not save the change records that do not meet the conditions. For example, only save the change records with a value in a certain field.
This article introduces methods to support the saving and querying of conditional change records.
For specific code, please refer to the sample project https://github.com/qihaiyan/springcamp/tree/master/spring-data-envers-conditional
1. Overview
You can easily save and query change records through spring-data-envers. You only need to add a few annotations. However, to achieve conditional storage and query of change records requires some complex processing.
2. Use spring-data-envers
First introduce the spring-data-envers dependency.
Add a line of code to build.gradle:
implementation 'org.springframework.data:spring-data-envers'
Add the Audited annotation to the entity class:
@Data @Entity @Audited public class MyData {<!-- --> @Id @GeneratedValue private Long id; private String author; }
Repository extends RevisionRepository method:
public interface MyDataRepository extends JpaRepository<MyData, Long>, RevisionRepository<MyData, Long, Integer> {<!-- --> }
Through the above three steps, the change record saving function has been added. We can confirm that the change record is saved successfully by calling the change record query method.
When Repository extends the RevisionRepository method, there will be a default implementation of the findRevisions method, which we can call directly:
public Revisions<Integer, MyData> findRevisions(Long id) {<!-- --> return myDataRepository.findRevisions(id); }
Finally, we can save the complete subject data and print the change record in the console:
@Override public void run(String... args) {<!-- --> MyData myData = new MyData(); myData.setId(1L); myData.setAuthor("test"); dbService.saveData(myData); dbService.findRevisions(myData.getId()).forEach(r -> System.out.println("revision: " + r.toString())); myData.setAuthor("newAuthor"); dbService.saveData(myData); dbService.findRevisions(myData.getId()).forEach(r -> System.out.println("revision: " + r.toString())); }
After executing the program, you can see that the corresponding change record can be queried for both data saving operations, and the change record also shows whether it is an insert or update operation through revisionType:
revision: Revision 1 of entity MyData(id=1, author=test) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 1, revisionDate = Oct 15, 2023, 11:41:15 AM), revisionType=INSERT } revision: Revision 2 of entity MyData(id=1, author=newAuthor) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 2, revisionDate = Oct 15, 2023, 11:41:16 AM), revisionType=UPDATE}
3. Saving conditional change records through custom Event Listener
When making data changes, Envers performs corresponding processing by listening events. There are a total of the following listening events:
EventType.POST_INSERT EventType.PRE_UPDATE EventType.POST_UPDATE EventType.POST_DELETE EventType.POST_COLLECTION_RECREATE EventType.PRE_COLLECTION_REMOVE EventType.PRE_COLLECTION_UPDATE
Each listening event corresponds to a specific Listener. In the example of this article, we expect that when the value of author is updated to be empty, the change record will not be saved. We can achieve this by customizing the Listeners of PRE_UPDATE and POST_UPDATE.
Because the framework provides a default Listener, custom Listeners only need to extend the default Listener and add our own unique logic.
MyEnversPostUpdateEventListenerImpl:
public class MyEnversPreUpdateEventListenerImpl extends EnversPreUpdateEventListenerImpl {<!-- --> public MyEnversPreUpdateEventListenerImpl(EnversService enversService) {<!-- --> super(enversService); } @Override public boolean onPreUpdate(PreUpdateEvent event) {<!-- --> if (event.getEntity() instanceof MyData & amp; & amp; ((MyData) event.getEntity()).getAuthor() == null) {<!-- --> return false; } return super.onPreUpdate(event); } }
MyEnversPostUpdateEventListenerImpl:
public class MyEnversPostUpdateEventListenerImpl extends EnversPostUpdateEventListenerImpl {<!-- --> public MyEnversPostUpdateEventListenerImpl(EnversService enversService) {<!-- --> super(enversService); } @Override public void onPostUpdate(PostUpdateEvent event) {<!-- --> if (event.getEntity() instanceof MyData & amp; & amp; ((MyData) event.getEntity()).getAuthor() == null) {<!-- --> return; } super.onPostUpdate(event); } }
In the custom Listener, we added judgment logic for whether the author field is empty.
4. Register the custom Event Listener into the system
After the custom Event Listener is completed, we also need to let the framework execute our customized Listener instead of using the default Listener.
The framework registers the Listener through the EnversIntegrator class. What we have to do is to reimplement EnversIntegrator. In this example, the reimplemented class is MyEnversIntegrator:
public class MyEnversIntegrator implements Integrator {<!-- --> @Override public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) {<!-- --> final ServiceRegistry serviceRegistry = sessionFactory.getServiceRegistry(); final EnversService enversService = serviceRegistry.getService(EnversService.class); final EventListenerRegistry listenerRegistry = serviceRegistry.getService(EventListenerRegistry.class); listenerRegistry.addDuplicationStrategy(EnversListenerDuplicationStrategy.INSTANCE); if (enversService.getEntitiesConfigurations().hasAuditedEntities()) {<!-- --> listenerRegistry.appendListeners( EventType.POST_DELETE, new EnversPostDeleteEventListenerImpl(enversService) ); listenerRegistry.appendListeners( EventType.POST_INSERT, new EnversPostInsertEventListenerImpl(enversService) ); listenerRegistry.appendListeners( EventType.PRE_UPDATE, new MyEnversPreUpdateEventListenerImpl(enversService) ); listenerRegistry.appendListeners( EventType.POST_UPDATE, new MyEnversPostUpdateEventListenerImpl(enversService) ); listenerRegistry.appendListeners( EventType.POST_COLLECTION_RECREATE, new EnversPostCollectionRecreateEventListenerImpl(enversService) ); listenerRegistry.appendListeners( EventType.PRE_COLLECTION_REMOVE, new EnversPreCollectionRemoveEventListenerImpl(enversService) ); listenerRegistry.appendListeners( EventType.PRE_COLLECTION_UPDATE, new EnversPreCollectionUpdateEventListenerImpl(enversService) ); } } @Override public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {<!-- --> // nothing to do } }
It can be found from the code that we only modified the Listeners registered by PRE_UPDATE and POST_UPDATE, and the Listeners for other events still use the framework’s default.
Finally, we need to put the MyEnversIntegrator we implemented into the META-INF/services/org.hibernate.integrator.spi.Integrator configuration file.
cn.springcamp.springdata.envers.MyEnversIntegrator
5. Confirm whether the preservation of conditional change records is effective
Finally, we modify the console printing program, update the author field to null and save it, and check whether there is a record of this update operation in the change record.
Add save code:
// won't generate audit record when author is null myData.setAuthor(null); dbService.saveData(myData); dbService.findRevisions(myData.getId()).forEach(r -> System.out.println("revision: " + r.toString()));
Execute the program and observe what is printed on the console:
revision: Revision 1 of entity MyData(id=1, author=test) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 1, revisionDate = Oct 15, 2023, 11:41:15 AM), revisionType=INSERT } revision: Revision 2 of entity MyData(id=1, author=newAuthor) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 2, revisionDate = Oct 15, 2023, 11:41:16 AM), revisionType=UPDATE}
It can be confirmed by printing the content that the change record in which the author field is updated to null is not recorded, indicating that our processing is effective.