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.
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:
YourEntityClass instance at that revisionRevisionType 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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With