Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hibernate Envers Delta Between Revisions

I need to implement audit history for all the CRUD operations on my project. The project uses Spring JPA Data Rest. I looked around for good library to achieve the required task and came across this Hibernate Envers, which seems quite good and easy to implement. Having incorporated this in my project, I am able to record the revisions of all the CRUD operations.

Now I need to expose the changes wherein users can see the changes done as part of any revisions. Here is how I would want the delta output(I put it in JSON format for easy readability).

[
  {
    "date": "9 may 2018, 6:06 pm",
    "user": "user.name (FName LName)",
    "actions": [
        {
            "field": "name",
            "oldValue": "Old Name very long",
            "newValue": "New Name also quite long."
        },
        {
            "field": "score",
            "oldValue": 2,
            "newValue": 4
        },
        {
            "field": "average_rating",
            "oldValue": "AA",
            "newValue": "A"
        }
    ]
},{
    "date": "10 may 2018, 5:06 pm",
    "user": "user.name (FName LName)",
    "actions": [
        {
            "field":"name",
            "oldValue": "Old Name",
            "newValue": "New Name"
        },
        {
            "field":"score",
            "oldValue": 1,
            "newValue": 6
        },
        {
            "field":"average_rating",
            "oldValue": "D",
            "newValue": "A+"
        },
        {
            "field":"rating",
            "oldValue": "A-",
            "newValue": "A"
        }
    ]
},{
    "date": "10 may 2018, 5:06 pm",
    "user": "user.name3 (FName3 LName3)",
    "actions": [
        {
            "field":"average_rating",
            "oldValue": "D",
            "newValue": "B"
        },
        {
            "field":"rating",
            "oldValue": "C",
            "newValue": "D"
        }
    ]
},{
    "date": "11 may 2018, 5:06 pm",
    "user": "user2.name2 (FName2 LName2)",
    "actions": [
        {
            "field":"score",
            "oldValue": 3,
            "newValue": 4
        },
        {
            "field":"average_rating",
            "oldValue": "C",
            "newValue": "B"
        }
    ]
},{
    "date": "9 apr 2018, 3:00 pm",
    "user": "user.name (FName LName)",
    "actions": [
        {
            "field":"name",
            "oldValue": "Old Name very long",
            "newValue": "New Name also quite long."
        },
        {
            "field":"score",
            "oldValue": 5,
            "newValue": 3
        },
        {
            "field":"average_rating",
            "oldValue": "AA",
            "newValue": "B"
        },
        {
            "field":"edf_score",
            "oldValue": 4,
            "newValue": 2
        },
        {
            "field":"edf_average_rating",
            "oldValue": "BBB+",
            "newValue": "BB"
        }
    ]
  }
]

I need to expose these in JSON-HAL format.

Thanks in advance.

like image 685
Ravi Avatar asked Oct 27 '25 03:10

Ravi


1 Answers

There are a couple of ways to accomplish what you ask but it mainly depends on the version of Hibernate and Envers that you are using. If you are using Hibernate 5.2 and before, there is going to be some extra processing your code will have to do in order to determine the information you want.

I am going to assume you have the primary key for the entity you're interested in.

List results = AuditReaderFactory.get( session ).createQuery()
  .forRevisionsOfEntity( YourEntityClass.class, false, true )
  .add( AuditEntity.id().eq( entityId ) )
  .addOrder( AuditEntity.revisionNumber().asc() )
  .getResultList();

This query actually returns a List<Object[]> because the second argument to forRevisionsOfEntity is false. Had the value of this argument been true, the return would have been List<YourEntityClass>.

From this each entry in the List is an object array based on the following configuration:

  • Index 0 - The YourEntityClass instance at that revision
  • Index 1 - The specific implementation of the revision entity (more on this later).
  • Index 2 - The RevisionType enum value, either ADD, MOD, or DEL. If third argument of forRevisionsOfEntity had been false, there would never be any DEL types.

At this point the logic becomes something like:

YourEntityClass previousInstance = null;
for ( int i = 0; i < results.size(); ++i ) {
  Object[] row = (Object[]) results.get( i );
  if ( previousInstance == null ) {
    // this is the first revision, consider nothing changed here
    // so store a reference to it for the next row.
    previousInstance = row[0];
  }
  else {
    final YourRevisionEntity revEntity = (YourRevisionEntity) row[1];
    final String userName = revEntity.getUserName();
    final long revisionTimestamp = revEntity.getTimestamp();

    final YourEntityClass currentInstance = (YourEntityClass) row[0];
    List<Action> actions = resolveActions( previousInstance, currentInstance );
    // build your things

    previousInstance = currentInstance;
  }
}

The main takeaway here is that in your resolveActions method, you basically use something like reflection or some java object diff library to determine the changes between the two instances. If you're using the idea of withModifiedFlag, you could run a query for each property, but that could be taxing on your system if the entity type in question has numerous columns or if the you tend to have numerous revisions.

If you are using Hibernate 5.3, we've added a convenience method that simplifies this process a bit but it relies on the withModifiedFlag concept too. In this particular case you'd initially run a slightly different modified version of the original query

List results = AuditReaderFactory.get( session ).createQuery()
  .forRevisionsOfEntityWithChanges( YourEntityClass.class, false, true )
  .add( AuditEntity.id().eq( entityId ) )
  .addOrder( AuditEntity.revisionNumber().asc() )
  .getResultList();

This query returns the same type of array as the 5.2 one mentioned above except it contains one additional object in the object array:

  • Index 3 - Collection of Strings that are the properties that changed.

The nice idea about this new approach is rather than using reflection or some type of difference library like I mentioned in for resolveActions, now you already know specifically which properties were altered, its just a matter of getting only those specific values from the object instance which is super trivial.

This last approach is still @Incubating so its considered experimental. I could potentially see changing Index 3 such that you get back a Tuple<String,Object> where it contains the property/field name potentially with the value, making it much more straight forward for users to use.

like image 111
Naros Avatar answered Oct 30 '25 16:10

Naros



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!