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.
Model implementing org.springframework.data.domain.Auditable interface.
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
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.
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.