Spring Boot更新MongoDB未预查询文档时如何避免DuplicateKeyException
解决方案
问题根因
- Spring Data MongoDB 的
save()方法默认通过规则判断实体是新增还是更新:当实体标注了@Version注解时,只要 version 字段为null,就会直接判定为新实体,执行 insert 操作而非 update。 - 你的实体ID为手动赋值,并非数据库生成,从DTO转换为实体时如果没有携带version值,转换后的实体version为null,触发insert逻辑,遇到已存在的ID就会抛出DuplicateKeyException。
可选实现方案
方案1:实现Persistable接口自定义新旧判断(适配Repository体系,推荐)
通过让基类实现Spring Data提供的Persistable接口,自行控制isNew判断逻辑,无需改动现有Repository调用逻辑。
- 修改抽象基类BeanA:
@Data @SuperBuilder @NoArgsConstructor @Validated public abstract class BeanA implements Persistable<Serializable> { @Version private Long version; @CreatedDate private ZonedDateTime createdAt; @LastModifiedDate private ZonedDateTime modifiedAt; @CreatedBy private String createdBy; @LastModifiedBy private String modifiedBy; // 新增transient标记字段,不会持久化到MongoDB @Transient @Builder.Default private boolean isNewEntity = false; @Override public boolean isNew() { return isNewEntity; } }
- 业务层调用时手动标记实体状态:
- 新增场景:构造实体时将
isNewEntity设为true - 更新场景:构造实体时将
isNewEntity设为false
修改后调用save()方法就会按照你标记的状态选择insert/update,无需提前读库。
- 新增场景:构造实体时将
方案2:使用MongoTemplate直接执行更新操作
无需改动现有实体结构,直接通过MongoTemplate构造更新语句,灵活度更高:
@Autowired private MongoTemplate mongoTemplate; public void updateBeanB(BeanB beanB) { // 构造查询条件,匹配复合ID Query query = Query.query(Criteria.where("_id").is(beanB.getCompositeId())); // 构造需要更新的字段 Update update = new Update() .set("modifiedBy", beanB.getModifiedBy()) .set("modifiedAt", ZonedDateTime.now()) .inc("version", 1); // 需要乐观锁能力时添加,手动递增版本号 // 执行更新,如需不存在则插入可替换为upsert方法 mongoTemplate.updateFirst(query, update, BeanB.class); }
注意事项
- 如果需要保留
@Version的乐观锁能力,更新请求的DTO必须携带当前记录的version值,否则乐观锁校验会失效,无法检测并发修改冲突。 - 若不需要乐观锁能力,可直接删除
@Version注解,Spring Data会默认根据ID是否为null判断实体状态,手动赋值ID的场景会直接走update逻辑,也不会触发重复键异常。
内容的提问来源于stack exchange,提问作者du-it
相关产品推荐
相关产品推荐

