比@EnableMongoAuditing功能強大的實現

IAyue發表於2021-08-04

問題出現

以前通過@EnableMongoAuditing、@CreateDate、@LastModifiedDate進行實體類建立時間、修改時間的自動管理。
但為了實現多資料來源的管理以及切換,自己覆蓋了mongoTemplate的bean,發現應用所有資料庫操作都出現"Couldn't find PersistentEntity"錯誤,去掉@EnableMongoAuditing註解後才能正常使用,但@CreateDate、@LastModifiedDate失效,建立時間、修改時間這些都要自己去設定,太麻煩了也容易漏,為了方便使用以及偷懶,需要實現所有資料庫儲存更新操作能自動插入、更新公用欄位,如建立時間、修改時間、建立者、修改者資訊

解決的思路

為了實現這個功能,翻了下MongoTemplate的原始碼,下面是關鍵的幾個方法

MongoTemplate.class

    public <T> T save(T objectToSave, String collectionName) {
        Assert.notNull(objectToSave, "Object to save must not be null!");
        Assert.hasText(collectionName, "Collection name must not be null or empty!");
        AdaptibleEntity<T> source = this.operations.forEntity(objectToSave, this.mongoConverter.getConversionService());
        return source.isVersionedEntity() ? this.doSaveVersioned(source, collectionName) : this.doSave(collectionName, objectToSave, this.mongoConverter);
    }

    protected <T> T doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
        objectToSave = ((BeforeConvertEvent)this.maybeEmitEvent(new BeforeConvertEvent(objectToSave, collectionName))).getSource();
        objectToSave = this.maybeCallBeforeConvert(objectToSave, collectionName);
        AdaptibleEntity<T> entity = this.operations.forEntity(objectToSave, this.mongoConverter.getConversionService());
        entity.assertUpdateableIdIfNotSet();
        MappedDocument mapped = entity.toMappedDocument(writer);
        Document dbDoc = mapped.getDocument();
        this.maybeEmitEvent(new BeforeSaveEvent(objectToSave, dbDoc, collectionName));
        objectToSave = this.maybeCallBeforeSave(objectToSave, dbDoc, collectionName);
        Object id = this.saveDocument(collectionName, dbDoc, objectToSave.getClass());
        T saved = this.populateIdIfNecessary(objectToSave, id);
        this.maybeEmitEvent(new AfterSaveEvent(saved, dbDoc, collectionName));
        return this.maybeCallAfterSave(saved, dbDoc, collectionName);
    }

   protected <T> T maybeCallBeforeConvert(T object, String collection) {
        return this.entityCallbacks != null ? this.entityCallbacks.callback(BeforeConvertCallback.class, object, new Object[]{collection}) : object;
    }

   protected <T> T maybeCallBeforeSave(T object, Document document, String collection) {
        return this.entityCallbacks != null ? this.entityCallbacks.callback(BeforeSaveCallback.class, object, new Object[]{document, collection}) : object;
    }

檢視MongoTemplate的原始碼會發現,新增以及更新操作呼叫的save方法,在執行前會有BeforeConvertEvent、BeforeSaveEvent兩個事件,我們只需要在這兩個事件裡面將我們需要儲存的內容做下處理,就可以實現所有資料庫新增、更新操作都自動加上建立時間這些資訊,而且原來MongoAudit只有幾個固定的欄位能用,自定義BeforeConvertCallback事件後,完全可以按自己的需要多管理幾個欄位。
具體要實現一個介面,並且將自定義的callback處理set到要用的mongTemplate上面

@FunctionalInterface
public interface BeforeConvertCallback<T> extends EntityCallback<T> {
    T onBeforeConvert(T var1, String var2);
}

具體實現

BeforeConvertEvent事件自定義處理

class BeforeConvert implements BeforeConvertCallback<Object> {

        @NotNull
        @Override
        public Object onBeforeConvert(Object o, String s) {
            log.info("before convert callback");

            Map<String, Field> fieldMap = ReflectUtil.getFieldMap(o.getClass());
            String userName = SecurityUtils.getUsername();
            Date now = new Date();

            if (fieldMap.containsKey("id")) {
                Field id = fieldMap.get("id");

                if (id == null || StringUtils.isBlank((String) ReflectUtil.getFieldValue(o, id))) {
                    //沒有id時為新增
                    //建立時間
                    if (fieldMap.containsKey("createTime")) {
                        ReflectUtil.setFieldValue(o, "createTime", now);
                    }
                    //建立者
                    if (fieldMap.containsKey("createUser") && StringUtils.isNotBlank(userName)) {
                        ReflectUtil.setFieldValue(o, "createUser", userName);
                    }
                }
            }
            //更新日期
            if (fieldMap.containsKey("updateTime")) {
                ReflectUtil.setFieldValue(o, "updateTime", now);
            }
            //更新者
            if (fieldMap.containsKey("updateUser") && StringUtils.isNotBlank(userName)) {
                ReflectUtil.setFieldValue(o, "updateUser", userName);
            }
            //這裡還可以加上其他各種需要設定的值

            return o;
        }
    }
    @Bean(name = "mongoTemplate")
    public DynamicMongoTemplate dynamicMongoTemplate() {
        Iterator<SimpleMongoClientDatabaseFactory> iterator = MONGO_CLIENT_DB_FACTORY_MAP.values().iterator();
        DynamicMongoTemplate mongoTemplate = new DynamicMongoTemplate(iterator.next());
	//將自定義事件處理放進mongoTemplate中
        mongoTemplate.setEntityCallbacks(EntityCallbacks.create(new BeforeConvert()));
        return mongoTemplate;
    }
   @Bean(name = "mongoDbFactory")
    public MongoDatabaseFactory mongoDbFactory() {
        Iterator<SimpleMongoClientDatabaseFactory> iterator = MONGO_CLIENT_DB_FACTORY_MAP.values().iterator();
        return iterator.next();
    }

最後的一些話

一開始我自定義BeforeSaveCallback,在裡面修改了object的內容,但發現並沒有儲存在資料庫中。在官方的文件中說,在這事件裡面修改object內容只是臨時的不會做持久化,需要修改轉換後document的內容才會儲存進資料庫,官方原話:Entity callback method invoked before a domain object is saved. Can return either the same or a modified instance of the domain object and can modify Document contents. This method is called after converting the entity to a Document so effectively the document is used as outcome of invoking this callback. Changes to the domain object are not taken into account for saving, only changes to the document. Only transient fields of the entity should be changed in this callback. To change persistent the entity before being converted, use the BeforeConvertCallback.
官方文件
為了避免實體轉document後欄位名發生改變不好找,就選用了BeforeConvertEvent,在轉換前進行了處理,但其實在BeforeSaveEvent處理也是可以的

事件的發生順序

Event:
BeforeDeleteEvent

AfterDeleteEvent

BeforeConvertEvent

BeforeSaveEvent

AfterSaveEvent

AfterLoadEvent

相關文章