Spring Data Envers supports the saving and querying of conditional change records

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.