
本文旨在深入探讨Spring Boot与MongoDB集成时,使用Spring Data Auditing功能可能遇到的`DuplicateKeyException`问题,并提供基于`Persistable`接口的解决方案。同时,文章将详细分析在解决重复键异常后,`@CreatedDate`字段可能无法正确保存的后续问题,并给出正确的实践方法,以确保审计字段的完整性和准确性。
Spring Data MongoDB 审计功能概述
Spring Data MongoDB提供了强大的审计功能,允许开发者自动记录实体(Document)的创建时间、最后修改时间、创建者和最后修改者。这对于追踪数据变更历史、满足合规性要求以及调试都非常有价值。核心注解包括:
- @CreatedDate: 标记实体创建时的日期时间。
- @LastModifiedDate: 标记实体最后一次修改时的日期时间。
- @CreatedBy: 标记实体创建者的用户ID或名称。
- @LastModifiedBy: 标记实体最后修改者的用户ID或名称。
- @Version: 用于乐观锁,防止并发更新冲突。
要启用审计功能,通常需要在配置类上添加@EnableMongoAuditing注解,并提供一个AuditorAware实现来获取当前操作的用户信息(如果需要@CreatedBy和@LastModifiedBy)。
示例配置 (AuditingConfig.java):
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import java.util.Optional;
@Configuration
@EnableMongoAuditing
public class AuditingConfig {
@Bean
public AuditorAware myAuditorProvider() {
return new AuditorAwareImpl();
}
} 审计元数据基类 (AuditMetadata.java):
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Version;
import java.time.LocalDateTime;
// @Setter @Getter 是Lombok注解,此处省略
public class AuditMetadata {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@Version
private Long version;
// ... getters and setters
}审计员实现 (AuditorAwareImpl.java):
import org.springframework.data.domain.AuditorAware; import java.util.Optional; public class AuditorAwareImpl implements AuditorAware{ @Override public Optional getCurrentAuditor() { return Optional.of("Admin"); // 实际应用中应从安全上下文中获取 } }
DuplicateKeyException:问题分析与根源
在使用Spring Data MongoDB的审计功能时,有时会遇到org.springframework.dao.DuplicateKeyException,尤其是在尝试更新现有实体时。错误信息通常类似于:
org.springframework.dao.DuplicateKeyException: Write operation error on server user.domain.com:27017. Write error: WriteError{code=11000, message='E11000 duplicate key error collection: springboot.category index: *id* dup key: { _id: "21" }', details={}}.这个错误表明MongoDB尝试插入一个已经存在_id值的文档。其根本原因在于Spring Data在执行save()操作时,未能正确判断当前实体是“新”的(需要执行insert操作)还是“已存在”的(需要执行update操作)。当Spring Data误认为一个带有ID的现有实体是新实体时,就会尝试进行插入操作,从而触发DuplicateKeyException。
这通常发生在实体类没有实现org.springframework.data.domain.Persistable接口,或者Persistable接口的isNew()方法实现不正确的情况下。
解决方案:实现 Persistable 接口
为了解决DuplicateKeyException,我们需要明确告诉Spring Data一个实体是否是新创建的。这通过实现org.springframework.data.domain.Persistable接口来完成。
Persistable接口包含两个方法:
- getId(): 返回实体的ID。
- isNew(): 返回一个布尔值,指示实体是否是新创建的。如果为true,Spring Data将执行插入操作;如果为false,则执行更新操作。
1. 更新 AuditMetadata 基类
为了更好地管理实体的“新旧”状态,可以在AuditMetadata中引入一个persisted标志。
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Version;
import java.time.LocalDateTime;
// @Setter @Getter 是Lombok注解,此处省略
public class AuditMetadata {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@Version
private Long version;
// 用于Persistable接口判断实体是否已持久化
protected boolean persisted;
// ... getters and setters
}2. 实现 Persistable 接口的实体类
让你的MongoDB实体类实现Persistable
示例实体类 (CategoryMongo.java):
import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.domain.Persistable; import org.springframework.lang.Nullable; // @Getter @Setter @NoArgsConstructor 是Lombok注解,此处省略 @Document(collection = "category") public class CategoryMongo extends AuditMetadata implements Persistable{ @Id @JsonProperty("category_id") private String category_id; @JsonProperty("id_colletion") private String emberId; // 注意:此字段在示例中用于返回category_id,可能引起混淆,建议统一ID字段 @JsonProperty("category_name") private String name; @JsonProperty("category_active") private ProductEnum active = ProductEnum.ativo; @JsonProperty("category_slug") private String slug; // 实现Persistable接口的方法 @Override @Nullable public String getId() { return category_id; } @Override public boolean isNew() { // 根据persisted标志判断实体是否为新 // 默认情况下,新创建的实体persisted为false,isNew()返回true // 当从数据库加载实体时,或者手动标记为已持久化时,persisted为true,isNew()返回false return !persisted; } // ... getters and setters (包括 emberId 的 getter,如果需要) public String getEmberId() { return category_id; // 示例中将emberId指向category_id } }
通过这种方式,当Spring Data调用isNew()方法时,它会根据persisted字段的值来判断是否执行插入或更新。
CreatedDate 字段丢失问题及正确处理
在解决了DuplicateKeyException之后,可能会出现一个新的问题:@CreatedDate字段在更新操作后消失,只剩下@LastModifiedDate。这通常是由于对persisted标志的错误管理导致的。
问题分析
@CreatedDate注解的字段只会在实体第一次被持久化(即被认为是“新”实体时)时被Spring Data填充。如果isNew()方法返回false,即使实体是新创建的,@CreatedDate也不会被设置。
在原始的解决方案中,save方法如下:
// 原始解决方案中的save方法 CategoryMongo catm = new CategoryMongo(); catm.setName(category.getName()); catm.setSlug(category.getSlug()); catm.setActive(category.getActive()); catm.setCategory_id(category.getCategory_id().toString()); catm.setPersisted(true); // 问题所在:为新创建的实体过早地设置persisted为true categoryRepositoryMongo.save(catm);
这段代码中,catm.setPersisted(true);在categoryRepositoryMongo.save(catm);之前被调用,而catm是一个刚刚通过new CategoryMongo()创建的对象。这意味着:
- new CategoryMongo()创建了一个新的实体对象。
- catm.setPersisted(true);将persisted标志设置为true。
- 当categoryRepositoryMongo.save(catm)被调用时,Spring Data会检查catm.isNew()。由于isNew()返回!persisted,此时它将返回false。
- Spring Data误认为这是一个已存在的实体,因此不会设置@CreatedDate。如果category_id已经存在,则会尝试更新;如果category_id不存在,由于isNew()为false,它可能不会执行插入操作,或者行为变得不确定(取决于Spring Data版本和具体实现)。
正确的 save 方法策略
为了确保@CreatedDate和@LastModifiedDate都能正确工作,关键在于正确管理isNew()的返回值,使其准确反映实体的真实状态:
- 创建新实体时: isNew()必须返回true。
- 更新现有实体时: isNew()必须返回false。
1. 创建新实体
当创建一个全新的实体并首次保存时,不应手动设置persisted为true。AuditMetadata中persisted字段的默认值(false)是正确的。
// 创建新实体并保存
public CategoryMongo createCategory(CategoryDto category) {
CategoryMongo newCatm = new CategoryMongo();
newCatm.setName(category.getName());
newCatm.setSlug(category.getSlug());
newCatm.setActive(category.getActive());
// 如果ID由应用程序生成,可以在此处设置
// newCatm.setCategory_id(UUID.randomUUID().toString());
// 如果ID由数据库生成,则无需设置
// 关键:不要在这里设置 newCatm.setPersisted(true);
// 此时 newCatm.isNew() 默认为 true,@CreatedDate 会










