MongoTemplate实现Upsert时如何结合@CreatedDate/@LastModifiedDate?
你遇到的核心问题是:Spring Data MongoDB的审计注解(@CreatedDate、@LastModifiedDate)不会自动作用于findAndReplace这类原子操作——因为findAndReplace是直接向MongoDB发送原子命令,绕过了Spring Data的实体生命周期回调机制(而save/insert会触发AuditingEntityListener来自动填充审计字段)。
下面提供两种解决方案,优先推荐第一种(兼顾原子性和需求逻辑):
方案1:利用MongoDB原生操作符实现原子Upsert(推荐)
直接使用mongoTemplate.upsert()结合Update对象,通过MongoDB的$set和$setOnInsert操作符,在原子操作中实现你的时间字段逻辑:
- 无论文档是否存在,都用
$set更新modifiedAt为当前时间 - 仅当插入新文档时,用
$setOnInsert设置createdAt
代码示例:
// 1. 构建查询条件 Query query = new Query().addCriteria(Criteria.where("yourField").is(yourValue)); // 替换为你的查询条件 // 2. 构建Update操作,实现Upsert的时间逻辑 ZonedDateTime now = ZonedDateTime.now(); Update update = new Update() .set("modifiedAt", now) .setOnInsert("createdAt", now); // 3. 执行原子Upsert操作 UpdateResult result = mongoTemplate.upsert(query, update, MyPojo.class); // (可选)如果后续需要使用实体对象,手动填充生成的ID和时间字段 if (result.getUpsertedId() != null) { MyPojo newObj = new MyPojo(); // 填充其他业务字段... newObj.setId(result.getUpsertedId().asString().getValue()); newObj.setCreatedAt(now); newObj.setModifiedAt(now); // 使用newObj做后续操作 }
这个方案的优势:
- 完全原子性,避免并发场景下的竞态问题
- 直接利用MongoDB原生能力,不需要依赖Spring Data的审计回调
- 完美匹配你的需求:新文档同时设置两个时间,已存在文档仅更新
modifiedAt
方案2:手动触发审计逻辑(不推荐,存在并发风险)
如果坚持要使用findAndReplace和审计注解,你可以手动调用AuditingHandler来填充审计字段,但需要注意查询和更新之间不是原子操作,可能出现并发问题:
// 1. 先查询文档是否存在(非原子步骤) Query query = new Query().addCriteria(...); boolean exists = mongoTemplate.exists(query, MyPojo.class); // 2. 获取Spring容器中的AuditingHandler,手动触发审计 AuditingHandler auditingHandler = applicationContext.getBean(AuditingHandler.class); MyPojo obj = new MyPojo(); // 填充obj的其他业务字段... if (exists) { // 文档已存在,仅更新modifiedAt auditingHandler.markModified(obj); } else { // 文档不存在,设置createdAt和modifiedAt auditingHandler.markCreated(obj); } // 3. 执行findAndReplace MyPojo result = mongoTemplate.findAndReplace(query, obj, new FindAndReplaceOptions().upsert());
为什么不推荐这个方案?
因为“查询是否存在”和“执行findAndReplace”是两个独立操作,在高并发场景下,可能出现查询时文档不存在,但执行upsert前其他线程已经插入了文档的情况,导致createdAt被错误覆盖。
补充说明:为什么save/insert能生效?
save和insert方法会触发Spring Data MongoDB的实体生命周期回调,AuditingEntityListener会在@PrePersist(插入前)和@PreUpdate(更新前)阶段自动填充@CreatedDate和@LastModifiedDate字段,而findAndReplace是直接操作数据库的命令,不会经过这些回调环节,所以注解不生效。
内容的提问来源于stack exchange,提问作者Jade Dezo

