1. 概述

默认情况下,MongoDB Java驱动程序生成的是 ObjectId 类型的ID。有时,我们可能希望使用其他类型的数据作为对象的唯一标识,例如UUID。然而,MongoDB Java驱动程序无法自动生成UUID

在这个教程中,我们将探讨三种使用MongoDB Java驱动程序和Spring Data MongoDB生成UUID的方法。

2. 共同点

在应用程序中管理单一类型的数据非常罕见。为了简化MongoDB数据库中ID的管理,我们更容易实现一个抽象类,定义所有Document类的ID。

public abstract class UuidIdentifiedEntity {

    @Id   
    protected UUID id;    

    public void setId(UUID id) {

        if (this.id != null) {
            throw new UnsupportedOperationException("ID is already defined");
        }

        this.id = id;
    }

    // Getter
}

本教程中的示例将假设所有存储在MongoDB数据库中的类都继承自这个类。

3. 配置UUID支持

为了在MongoDB中存储UUID,我们必须配置驱动程序。这个配置非常简单,只告诉驱动程序如何在数据库中存储UUID。如果多个应用程序使用同一个数据库,我们需要谨慎处理。

我们只需在启动MongoDB客户端时指定uuidRepresentation参数

@Bean
public MongoClient mongo() throws Exception {
    ConnectionString connectionString = new ConnectionString("mongodb://localhost:27017/test");
    MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
      .uuidRepresentation(UuidRepresentation.STANDARD)
      .applyConnectionString(connectionString).build();
    return MongoClients.create(mongoClientSettings);
}

如果我们使用Spring Boot,可以在application.properties文件中指定这个参数:

spring.data.mongodb.uuid-representation=standard

4. 使用生命周期事件

处理UUID生成的第一种方法是利用Spring的生命周期事件。对于MongoDB实体,我们不能使用JPA注解如@PrePersist等。因此,我们需要实现注册在ApplicationContext中的事件监听器类。为此,我们的类必须继承Spring的AbstractMongoEventListener

public class UuidIdentifiedEntityEventListener extends AbstractMongoEventListener<UuidIdentifiedEntity> {
    
    @Override
    public void onBeforeConvert(BeforeConvertEvent<UuidIdentifiedEntity> event) {
        
        super.onBeforeConvert(event);
        UuidIdentifiedEntity entity = event.getSource();
        
        if (entity.getId() == null) {
            entity.setId(UUID.randomUUID());
        } 
    }    
}

在这种情况下,我们使用的是OnBeforeConvert事件,它在Spring将我们的Java对象转换为Document对象并发送给MongoDB驱动程序之前触发。

将我们的事件类型设置为UuidIdentifiedEntity类,可以处理这个抽象超类型的所有子类。一旦使用UUID作为ID的对象被转换,Spring就会调用我们的代码。

需要注意的是,Spring将事件处理委托给一个TaskExecutor,这可能是异步的。Spring并不能保证事件在对象实际转换之前被处理。如果你的TaskExecutor是异步的,这种方法不推荐,因为ID可能在对象转换后生成,导致异常:

InvalidDataAccessApiUsageException: 不能为类型java.util.UUID的实体自动生成ID

我们可以通过在ApplicationContext中注解它为@Component或在@Configuration类中生成它来在ApplicationContext中注册事件监听器:

@Bean
public UuidIdentifiedEntityEventListener uuidIdentifiedEntityEventListener() {
    return new UuidIdentifiedEntityEventListener();
}

5. 使用实体回调

Spring框架提供了在实体生命周期中的某些点执行自定义代码的钩子,称为EntityCallbacks。在我们的场景中,我们可以利用它们在对象被持久化到数据库之前生成UUID。

与之前看到的事件监听器方法不同,回调方法保证它们的执行是同步的,并且代码将在预期的时间点执行在对象的生命周期中。

Spring Data MongoDB提供了一组我们在应用中可以使用的回调。在这种情况下,我们将使用与之前相同的事件。回调可以直接在@Configuration类中提供

@Bean
public BeforeConvertCallback<UuidIdentifiedEntity> beforeSaveCallback() {
        
    return (entity, collection) -> {
          
        if (entity.getId() == null) {
            entity.setId(UUID.randomUUID());
        }
        return entity;
    };
}

我们也可以使用实现BeforeConvertCallback接口的Component

6. 使用自定义仓库

Spring Data MongoDB提供了实现目标的第三种方法:使用自定义仓库实现。通常,我们只需要声明一个继承自MongoRepository的接口,然后Spring会处理与仓库相关的代码。

如果我们想改变Spring Data处理我们对象的方式,我们可以定义自定义代码,Spring将在仓库级别执行。为此,**首先需要定义一个接口,它扩展MongoRepository**:

@NoRepositoryBean
public interface CustomMongoRepository<T extends UuidIdentifiedEntity> extends MongoRepository<T, UUID> { }

@NoRepositoryBean注解防止Spring为MongoRepository生成通常关联的代码。这个接口强制使用UUID作为对象ID的类型。

然后,我们需要创建一个仓库类,定义处理我们UUID所需的行为:

public class CustomMongoRepositoryImpl<T extends UuidIdentifiedEntity> 
  extends SimpleMongoRepository<T, UUID> implements CustomMongoRepository<T>

在这个仓库中,我们必须重写SimpleMongoRepository的相关方法来处理需要生成ID的情况,例如在我们的例子中是save()insert()方法:

@Override
public <S extends T> S save(S entity) {
    generateId(entity);
    return super.save(entity);
}

最后,我们需要告诉Spring使用我们自定义的类作为仓库的实现,而不是默认实现。我们在@Configuration类中做这件事:

@EnableMongoRepositories(basePackages = "com.baeldung.repository", repositoryBaseClass = CustomMongoRepositoryImpl.class)

然后,我们可以像往常一样声明仓库,无需任何更改:

public interface BookRepository extends MongoRepository<Book, UUID> { }

7. 总结

在这篇文章中,我们了解了三种使用Spring Data MongoDB实现MongoDB对象ID为UUID的方法。

如往常一样,本文中的代码可在GitHub上找到。