Saturday, June 22, 2013

Spring MongoDB Auditing and Extensions


In this blog I will explain how to do update of audit fields (create/modified) and version in MongoDB using spring-data-mongodb library. I would also show how to workaround some of the limitations. 


Basic

A Model can be made Auditable by one of the following ways.
  1. Model implementing org.springframework.data.domain.Auditable interface.
  2. use @CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate annotations defined in package org.springframework.data.annotation
For maintaining version use @Version annotation.

To make it simple, create a base class as shown below  which all Model classes will extend.

 public class Audit implements Auditable<String,String> {  
      @Id  
      private String id;  
      @Version  
      private Long version;  
      private String createdBy;  
      private DateTime createdDate;  
      private String lastModifiedBy;  
      private DateTime lastModifiedDate;  
      @Override  
      public boolean isNew() {  
      /**Assuming the Id is generated by database. If not, then need to override in each model depending on how to identify if a Model is a new one or already saved Model.  
 **/  
           return id == null;  
      }  
 // remaining getters & setters.  
 }  
 public class MyModel extends Audit{  
   private String firstName;  
      private String lastName;  
 //getters & setters.  
 }  

In above, isNew() is crucial and used to differentiate if a model is new or already saved model. The auditing listeners will use this method to decide if created fields to be updated or not. The above implementation is on assumption that the Ids are generated by DB and hence it will be NULL for models created newly.

We also require to define beans which provide auditing principal and current time.

Auditing principal

The auditing principal can be obtained by implementing the interface org.springframework.data.domain.AuditorAware. A sample implementation is as below.

 public class MyAuditor implements AuditorAware<String> {  
      @Override  
      public String getCurrentAuditor() {  
 //If you are using spring-security, you may get this from SecurityContext.  
           return "System";  
      }  
 }  

Date Provider

To get the auditing time, we need an implemenation of interface org.springframework.data.auditing.DateTimeProvider. Spring provides a defulat implementation for current time is org.springframework.data.auditing.CurrentDateTimeProvider

Spring configuration to make all this work is defined as below.

 <mongo:auditing auditor-aware-ref="myAuditor" date-time-provider-ref="datTimeProvider"/>  
      <bean id="myAuditor" class="poorna.samples.mongo.audit.MyAuditor"/>  
      <bean id="datTimeProvider" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">  
           <property name="staticField" value="org.springframework.data.auditing.CurrentDateTimeProvider.INSTANCE"></property>  
      </bean>  
This configuration will create listener bean org.springframework.data.mongodb.core.mapping.event.AuditingEventListener which listens for BeforConvertEvent that is fired by MongoTemplate(MongoOperations) during save operations.

Gaps

  1. If due to some reason <mongo:auditing> is included multiple times in your application context, then spring creates multiple instances of AuditingEventListener i.e., the audit update logic gets executed multiple times. This happens as the spring NameSpaceHandler creates a different listener bean each time.  
  2. MongoTemplate fires BeforeConvert event only during save and insert operations i.e., the AuditEventListener and hence the audit fields are updated only if the whole entity is being saved (either by insert or update). But MongoTemplate provides many other methods like update*(**) and findAndModify(**) methods which will update the model, but not the audit and version.

Custom AuditEventListener

The first gap can be overcome by creating custom AuditEventListener and registering this bean instead of <mongo:auditing>. A Sample implementation is defined as below.
 public class MyAuditingEventListner implements  
           ApplicationListener<BeforeConvertEvent<Object>> {  
      private AuditorAware<String> auditorAware;  
      private DateTimeProvider dateTimeProvider;  
      public MyAuditingEventListner(AuditorAware<String> auditorAware,  
                DateTimeProvider dateTimeProvider) {  
           this.auditorAware = auditorAware;  
           this.dateTimeProvider = dateTimeProvider;  
      }  
      @Override  
      public void onApplicationEvent(BeforeConvertEvent<Object> event) {  
           Object obj = event.getSource();  
           if (Audit.class.isAssignableFrom(obj.getClass())) {  
                Audit entity = (Audit) obj;  
                if (entity.isNew()) {  
                     entity.setCreatedBy(auditorAware.getCurrentAuditor());  
                     entity.setCreatedDate(dateTimeProvider.getDateTime());  
                }  
                entity.setLastModifiedBy(auditorAware.getCurrentAuditor());  
                entity.setLastModifiedDate(dateTimeProvider.getDateTime());  
           }  
      }  
 }  
     

Following is updated bean configuration. Note that we no longer require <mongo:auditing>. As bean id is specified for MyAuditEventListener, the bean will be registered only once even if the file containing this bean is imported multiple times.

 <bean id="myAuditor" class="poorna.samples.mongo.audit.myAuditor" />  
      <bean id="dateTimeProvider"  
           class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">  
           <property name="staticField"  
                value="org.springframework.data.auditing.CurrentDateTimeProvider.INSTANCE"></property>  
      </bean>  
      <bean id="myAuditingEventListener"  
           class="poorna.samples.mongo.audit.MyAuditingEventListner">  
           <constructor-arg ref="myAuditor"></constructor-arg>  
           <constructor-arg ref="dateTimeProvider"></constructor-arg>  
      </bean>  


Aspects for Audit Update

The second gap can be overcome by writing some Aspects that inject the audit fields before invoking update methods of MongoTemplate. Following is implementation for Aspect.

 @Aspect  
 public class MyMongoAuditAspect {  
      @Autowired  
      private AuditorAware<String> auditorAware;  
      @Autowired  
      private DateTimeProvider dateTimeProvider;  
 @Before(value = "(execution(public * org.springframework.data.mongodb.core.MongoOperations.update*(..)) ||"  
                + "execution(public * org.springframework.data.mongodb.core.MongoOperations.findAndModify(..))) && args(query,update,.. )")  
      public void setAuditFields(Query query, Update update) {  
           if (update != null) {  
                update.inc(“version”, 1);  
                update.set(“lastModifiedBy”, auditorAware.getCurrentAuditor())  
                     .set(“lastModifiedDate”, dateTimeProvider.getDateTime());  
           }  
      }  
 }  



Note the following from above Aspect code.

  • The Aspect works on MongoOperations interface and hence on MongoTemplate.
  • PointCut expression specifies to work on all update and findAndModify methods.
  • This is @Before aspect and updates Update object with auditing fields.
  • The same dateTimeProvider and myAuditor used in MyAuditingEventListener are used in MyMongoAuditAspect also.
The application context should be configured to register this aspect as bean and also configured for AOP using <aop:aspectj-autoproxy/>

Conclusion

I have tried to explain how to achieve auditing while using spring framework for mongodb. I have also explained with code snippets on how AOP can be used to fill some gaps. I have explained AOP for few methods. The same can be extended to work for upsert method and also for update method when upsert flag is true.




9 comments:

  1. Hey Poorna, nice to see your article extending it from http://maciejwalkowiak.pl/blog. I think its too many things to be done just for auditing. Do you have a better alternative which you could have found out?

    ReplyDelete
    Replies
    1. Hi Triguna,
      As mentioned, all this only to fill the gaps (as mentioned in article) i.e., when partial updates are done directly instead of whole document.

      If you want to do partial updates, but want auditing to be simple: fetch the document first, and set all the fields that need to be modified and then call save() method.

      Note: This is possible only in spring-mongo, as the query and update are java objects. But same is not possible for relational as all partial updates are simple SQL statements.

      Delete
  2. Thanks. Your article actually helped me override IsNewStrategyFactory. I found it too much but it actually is the solution for overriding the isNew method to figure out how do you want to work out on whether the object is new or not.
    However now since I am setting the _id field by myself, I am stuck on how do we implement isNew method to figure out if object is new or not.

    May be prior insertion we need to set a flag new in the model class and then return that from isNew. Let me think about it further if I can find a better way to do it.

    Thanks for your response. It helped me!. :)

    ReplyDelete
    Replies
    1. If using created fields. For an existing model, they will be always not-null value.

      if(createdDate !=null)
      isNew= false;
      else
      isNew=true.

      Delete
  3. Hi Poorna,
    Thanks for this article, really helped me understand Spring Audit. I tried implementing the same way as this article, however, when I have advice on MongoOperations update (or findAndModify), the aspect never gets executed, but if I change the advice to one of my DAO methods, then it does. Any idea why would that be? Is there any config or something that I should do for MongoOperation joinPoint?

    ReplyDelete
    Replies
    1. may need to look at your configuration. But over the time, I have moved away from this solution. Instead I have written MyCustomRepository extdns SimpleMongoRepository and then all custom code there.

      Delete
    2. Thanks for your prompt response. My configuration is annotation based (and not xml based). Since you've moved away from it, can you please share either code (of your MyCustomRepository) or an article explaining how you have achieved, intercepting mongoDB save, insert, update*, findAndModify, upsert operations to update the spring auditing fields?

      Delete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Thanks spent day trying to get to work your post enabled me to make it work in 15 minutes

    ReplyDelete