3

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.

Mikko Maunu
  • 41,366
  • 10
  • 132
  • 135
Ravi
  • 1,082
  • 4
  • 15
  • 24
  • Could you tell me specifically which version of Hibernate & Envers you're using as that has some impact on which APIs are best for your desired output? – Naros Jan 24 '19 at 07:52
  • Hi @Naros, It's 5.2.17.Final for both Hibernate and Envers – Ravi Jan 24 '19 at 12:05

1 Answers1

4

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.

Naros
  • 19,928
  • 3
  • 41
  • 71
  • Hi @Naros, thanks for the reply. I am doing exactly what you just described above(as per version 5.2.17). I am using java-object-diff library to compare the 2 objects. Can you please tell me one thing, can I just upgrade envers to latest say 5.4.1.Final but keep hibernate to 5.2.17 and get the collection of properties that have changed/added/deleted? Presumably the list of properties would include all the properties whether they have been added/updated/deleted between revisions. – Ravi Jan 25 '19 at 12:57
  • Unfortunately you cannot. Since both `hibernate-envers` and `hibernate-core` are built in tandem, its critical they be deployed with the same version in order to avoid compatibility problems between the two artifacts. – Naros Jan 29 '19 at 14:02
  • ok thanks. If I need to retrieve the revision history of a particular revision of an entity. Is my code below the right one? AuditQuery auditQuery = AuditReaderFactory.get(entityManager).createQuery() .forEntitiesAtRevision(entityClass, revisionId) .add(AuditEntity.id().eq(financialGroupId)); @SuppressWarnings("unchecked") List resultList = auditQuery.getResultList(); if (resultList.size() != 1) { throw new IllegalStateException("Entity at Revision not found"); } return resultList.get(0); } – Ravi Feb 01 '19 at 17:11
  • Hi @Naros, thanks for that. If I need to retrieve the revision history of a particular revision of an entity. Is my code below the right one? `AuditQuery auditQuery = AuditReaderFactory.get(entityManager).createQuery() .forEntitiesAtRevision(entityClass, revisionId) .add(AuditEntity.id().eq(financialGroupId)); @SuppressWarnings("unchecked") List resultList = auditQuery.getResultList(); if (resultList.size() != 1) { throw new IllegalStateException("Entity at Revision not found"); } return resultList.get(0); }` – Ravi Feb 06 '19 at 11:13
  • That looks right, yes. Just one note that if that revision happens to be the one that describes the delete, that method will return no results as it is defined to only return inserts and updates only. – Naros Feb 06 '19 at 16:07
  • Hi @Naros, how can I get it to return the delete entity as well? – Ravi Feb 06 '19 at 23:55
  • `forEntitiesAtRevision(Class> c, String entityName, Number revision, boolean includeDeletions)` – Naros Feb 07 '19 at 13:20
  • @Naros what is entityId in the code is it a primary key property field in the entity class? – JAVA Mar 01 '19 at 14:00
  • @JAVA, No `entityId` is not a field / attribute name but instead is the raw entity identifier value, whether that is an integer, long, or something else depends entirely on your entity's identifier data type. – Naros Mar 08 '19 at 14:24