
本文旨在深入探讨Spring Boot与MongoDB集成时,使用Spring Data Auditing功能可能遇到的`DuplicateKeyException`问题,并提供基于`Persistable`接口的解决方案。同时,文章将详细分析在解决重复键异常后,`@CreatedDate`字段可能无法正确保存的后续问题,并给出正确的实践方法,以确保审计字段的完整性和准确性。
Spring Data MongoDB提供了强大的审计功能,允许开发者自动记录实体(Document)的创建时间、最后修改时间、创建者和最后修改者。这对于追踪数据变更历史、满足合规性要求以及调试都非常有价值。核心注解包括:
要启用审计功能,通常需要在配置类上添加@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<String> 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<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of("Admin"); // 实际应用中应从安全上下文中获取
}
}在使用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()方法实现不正确的情况下。
为了解决DuplicateKeyException,我们需要明确告诉Spring Data一个实体是否是新创建的。这通过实现org.springframework.data.domain.Persistable接口来完成。
Persistable接口包含两个方法:
为了更好地管理实体的“新旧”状态,可以在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
}让你的MongoDB实体类实现Persistable<ID类型>接口,并实现其方法。
示例实体类 (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<String> {
@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字段的值来判断是否执行插入或更新。
在解决了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()创建的对象。这意味着:
为了确保@CreatedDate和@LastModifiedDate都能正确工作,关键在于正确管理isNew()的返回值,使其准确反映实体的真实状态:
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 会以上就是Spring Boot MongoDB 审计中的重复键异常处理及日期字段管理的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号