缘起
最近在公司使用 MongoDB 这种文档数据库,和 MySQL 一样在设计表实体时候都有一些公有的字段,比如,创建人、更新人、创建时间、更新时间。
那么 MongoDB 如何统一维护这些字段呢?
其实有两种方式:
-
SpringDataJpa审计
在使用MySQL的时候,我们经常使用springDataJpa作为数据库持久层框架,我们知道jpa有审计功能,那springDataMongoDB作为springData下的产物,它也是可以使用jpa的审计功能的,具体使用方式可以百度。 -
SpringDataMongoDB生命周期事件
SpringDataMongoDB发布⼀些⾮常有⽤的⽣命周期事件,比如实体转换之前,实体保存之前,实体保存之后,实体加载之后等都有对应的钩子函数,我们可以在这些钩子函数里面统一维护公共字段。
问题
MongoDB 维护实体统一的字段,在上面介绍了两种方式,但是他们都有缺陷:
利用 SpringDataJpa 审计的方式,只有继承自 MongoRepository 的 Repository接口,它们的写入和修改可以使用审计功能,如果你使用 MongoTemplate 来保存和修改是不能自动维护公共字段的。
而使用 SpringDataMongoDB 的生命周期函数无论是使用 Repository 接口来保存或者修改,还是使用 MongoTemplate 的保存和修改都能走到钩子函数,然后我们自己在钩子函数中统一维护。
因为我们写的 Repository 是继承 MongoRepository ,具体实现是 SimpleMongoRepository, 翻开 SimpleMongoRepository 的源码:
保存数据是通过
MongoOperations 来实现的,而 MongoTemplate 正是 MongoOperations 的实现。
那么,是不是只要使用 SpringDataMongoDB 的生命周期函数就行了?其实还有一个问题,就是我们使用 MongoTemplate 工具类中以 update 开头的API,比如 updateFirst,还有 upsert 等API是不走钩子函数的。
翻开 MongoTemplate 的源码发现:
它们都统一调用
doUpdate 方法,
这个方法并没有增加生命周期事件的调用。我们在看
MongoTemplate 的 save 方法,
可以看到这个
doSave 方法中有 和保存相关的生命周期事件。
解决思路
那么对应上面说的问题,该如何解决呢?
第一个想法是改写工具类,在 update 之前主动维护公共字段,但是这种方式代价太大了,而且也不符合 “开闭原则”,那这种方式行不通,不能修改,就只能考虑扩展了。
扩展的思路就是怎么在使用 MongoTemplate 的 update 方法之前或者之后做一些我们的逻辑。可以想到的很简单的方式就是 代理模式。
我们通过 代理一个 MongoTemplate 工具类,在调用 MongoTemplate 的 update 方法之前维护自己的公共字段。
方案实现
-
实现
SpringDataMongoDB的生命周期事件监听
MongoDB的生命周期,只需重写org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener的子类,这样在分派事件时会调用它们。
AbstractMappingEventListener中提供了以下回调方法:方法 描述 onBeforeConvert调用 MongoTemplate的insert、insertList和save操作,在通过MongoConverter将对象转换为文档之前的处理。onBeforeSave调用 MongoTemplate的insert、insertList和save操作,在数据库中插入或保存文档之前的处理。onAfterSave调用 MongoTemplate的insert、insertList和save操作,在数据库中插入或保存文档之后的处理。onAfterLoad调用 MongoTemplate中的find、findAndRemove、findOne和getCollection方法,从数据库检索文档后的处理。onAfterConvert调用 MongoTemplate中的find、findAndRemove、findOne和getCollection方法,从数据库检索文档被转换为POJO后的处理。这里值得注意的是: 我们这里要使用
onBeforeConvert这个回调,我一开始使用onBeforeSave这个回调,发现设置的公共字段都没有数据,原因是onBeforeConvert这个回调,会将实体转为Document文档,那么就是先设置实体,然后将实体转Document,然后把Document写入数据库,这样回调设置的数据就转到Document对象中了。 而调用onBeforeSave这个回调时候已经将实体对象转完Document了,这样即使再设置 实体对象也没用了。当然如果使用这个回调可以直接修改Document对象内容,我想应该也是可以的,我就不试了。MongoTemplate.doInsert源码:完整代码:
package com.bokbok.content.audit; import com.bokbok.common.vo.app.AccountInfoVO; import com.bokbok.core.utils.HttpServletRequestHelper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; import java.lang.reflect.Field; import java.util.Optional; @Component @Slf4j public class AuditingListener extends AbstractMongoEventListener<Object> { // 获取用户的工具类 看自己逻辑修改 @Autowired private HttpServletRequestHelper httpServletRequestHelper; @Override public void onBeforeConvert(BeforeConvertEvent<Object> event) { log.info("On before Save inserting the record to the Delete and update collection"); Object source = event.getSource(); if(source == null){ return; } long time = System.currentTimeMillis(); Field idField = ReflectionUtils.findField(source.getClass(), "id"); if(idField != null && valueIsNotEmpty(source, idField)){ // 更新 ReflectionUtils.doWithFields(source.getClass(), field -> { handleLastModifiedDate(source, field, time); handleLastModifiedBy(source, field); }); } else { // 增加 ReflectionUtils.doWithFields(source.getClass(), field -> { handleCreatedBy(source, field); handleCreatedDate(source, field, time); handleLastModifiedDate(source, field, time); handleLastModifiedBy(source, field); }); } } // 判断字段 是否有值 private boolean valueIsNotEmpty(Object source, Field idField){ ReflectionUtils.makeAccessible(idField); return ReflectionUtils.getField(idField, source) != null; } private void handleCreatedBy(Object source, Field field) throws IllegalAccessException { CreatedBy createdBy = AnnotationUtils.getAnnotation(field, CreatedBy.class); if(createdBy != null){ AccountInfoVO accountInfo = httpServletRequestHelper.getAccountInfo(); if(accountInfo != null){ field.setAccessible(true); field.set(source, String.valueOf(accountInfo.getId())); } } } private void handleCreatedDate(Object source, Field field, long time) throws IllegalAccessException { CreatedDate createdDate = AnnotationUtils.getAnnotation(field, CreatedDate.class); if(createdDate != null){ field.setAccessible(true); field.set(source, time); } } private void handleLastModifiedBy(Object source, Field field) throws IllegalAccessException { LastModifiedBy lastModifiedBy = AnnotationUtils.getAnnotation(field, LastModifiedBy.class); if(lastModifiedBy != null){ AccountInfoVO accountInfo = httpServletRequestHelper.getAccountInfo(); if(accountInfo != null){ field.setAccessible(true); field.set(source, String.valueOf(accountInfo.getId())); } } } private void handleLastModifiedDate(Object source, Field field, long time) throws IllegalAccessException { LastModifiedDate lastModifiedDate = AnnotationUtils.getAnnotation(field, LastModifiedDate.class); if(lastModifiedDate != null){ field.setAccessible(true); field.set(source, time); } } }实体对象 :这几个注解是
springdata的
-
通过代理模式代理一个增强的工具类
package com.bokbok.content.audit; import com.bokbok.common.vo.app.AccountInfoVO; import com.bokbok.core.utils.HttpServletRequestHelper; import com.mongodb.client.result.UpdateResult; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; @Slf4j public class MongoTemplateProxy extends MongoTemplate { private HttpServletRequestHelper httpServletRequestHelper; public MongoTemplateProxy(MongoDbFactory mongoDbFactory, HttpServletRequestHelper httpServletRequestHelper) { super(mongoDbFactory); this.httpServletRequestHelper = httpServletRequestHelper; } @Override protected UpdateResult doUpdate(String collectionName, Query query, Update update, @Nullable Class<?> entityClass, boolean upsert, boolean multi) { log.info("-------MongoTemplateProxy---doUpdate----"); ReflectionUtils.doWithFields(entityClass, field -> { LastModifiedDate lastModifiedDate = AnnotationUtils.getAnnotation(field, LastModifiedDate.class); if(lastModifiedDate != null){ update.set(field.getName(), System.currentTimeMillis()); } LastModifiedBy lastModifiedBy = AnnotationUtils.getAnnotation(field, LastModifiedBy.class); if(lastModifiedBy != null){ AccountInfoVO accountInfo = httpServletRequestHelper.getAccountInfo(); if(accountInfo != null){ update.set(field.getName(), String.valueOf(accountInfo.getId())); } } }); return super.doUpdate(collectionName, query, update, entityClass, upsert, multi); } }
好了,以上就是完成 SpringDataMongodb 的审计功能。