How to assign custom document IDs in Spring Data MongoDB

April 14, 2017 · 4 min read – Last updated on March 09, 2019

When using Spring Data MongoDB IDs can be automatically generated for documents provided that you’re using an ObjectId, String or BigInteger as the underlying type. What if you would like to use some other, non-autogeneratable type for IDs?

Issue

Under normal circumstances the MongoDB driver generates a unique ID for objects to be persisted. The default set of types for which this works out-of-the-box are enumerated here in MongoSimpleTypes.AUTOGENERATED\_ID\_TYPES.

public abstract class MongoSimpleTypes {

  public static final Set<Class<?>> AUTOGENERATED_ID_TYPES;

  static {
    Set<Class<?>> classes = new HashSet<Class<?>>();
    classes.add(ObjectId.class);
    classes.add(String.class);
    classes.add(BigInteger.class);
    AUTOGENERATED_ID_TYPES = Collections.unmodifiableSet(classes);
    ...
   }

  ...

}

Defining a custom event listener and assign a value to an ID with another type should be working. However before Spring Data MongoDB 1.10 this wasn’t directly possible due to DATAMONGO-1617.

Let’s consider the following example.

public class Customer implements Persistable<UUID> {
 
    @Id
    private UUID id;
    ...
 
    public UUID getId() { return id; }
    public void setId(UUID id) { this.id = id; }
    public boolean isNew() { return (getId() == null); }
   ...
}

When an instance of this object is saved, it has to have field id initialized with a non-null value, otherwise the following exception occurs.

Caused by: org.springframework.dao.InvalidDataAccessApiUsageException: Cannot autogenerate id of type java.util.UUID for entity of type hello.Customer!
	at org.springframework.data.mongodb.core.MongoTemplate.assertUpdateableIdIfNotSet(MongoTemplate.java:1304) ~[spring-data-mongodb-1.10.0.RELEASE.jar:na]
	at org.springframework.data.mongodb.core.MongoTemplate.doInsert(MongoTemplate.java:845) ~[spring-data-mongodb-1.10.0.RELEASE.jar:na]
	at org.springframework.data.mongodb.core.MongoTemplate.insert(MongoTemplate.java:793) ~[spring-data-mongodb-1.10.0.RELEASE.jar:na]
	at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.save(SimpleMongoRepository.java:80) ~[spring-data-mongodb-1.10.0.RELEASE.jar:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_65]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_65]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
	at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]

Even if you try to define a custom entity listener to generate UUIDs for entities automatically, it’s not going to work due to the way how MongoTemplate is implemented.

@Component
public class GenerateUUIDListener extends AbstractMongoEventListener<Customer> {
 
    @Override
    public void onBeforeConvert(BeforeConvertEvent<Customer> event) {
        Customer customer = event.getSource();
        if (customer.isNew()) {
            customer.setId(UUID.randomUUID());
        }
    }
 
}

The following code was taken from MongoTemplate.doInsert().

protected <T> void doInsert(String collectionName, T objectToSave, MongoWriter<T> writer) {
 
  assertUpdateableIdIfNotSet(objectToSave);
 
  initializeVersionProperty(objectToSave);
 
  maybeEmitEvent(new BeforeConvertEvent<T>(objectToSave, collectionName));
 
  DBObject dbDoc = toDbObject(objectToSave, writer);
 
  maybeEmitEvent(new BeforeSaveEvent<T>(objectToSave, dbDoc, collectionName));
  Object id = insertDBObject(collectionName, dbDoc, objectToSave.getClass());
 
  populateIdIfNecessary(objectToSave, id);
  maybeEmitEvent(new AfterSaveEvent<T>(objectToSave, dbDoc, collectionName));
}

Here the problem was that assertUpdateableIdIfNotSet(objectToSave) was called before emitting BeforeConvertEvent and this way entity listeners didn’t have the chance to populate custom ID fields.

This behavior was fixed in releases 1.10.1, 1.9.8 and also in the new upcoming release 2.0.

Workaround

Should you have to use an older version for some reason, there is a possible workaround and that is to define a custom repository implementation or extend the one which you already might have.

public class CustomMongoRepositoryImpl<T extends BaseEntity>
  extends SimpleMongoRepository<T, Long> implements CustomMongoRepository<T> {

  CustomMongoRepositoryImpl(MongoEntityInformation<T, ID> entityInformation, MongoOperations mongoOperations) {
    super(entityInformation, mongoOperations);
  }

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

  @Override
  public <S extends T> List<S> insert(Iterable<S> entities) {
    for (S entity : entities) {
        generateId(entity);
    }
    return super.insert(entities);
  }

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

  @Override
  public <S extends T> List<S> save(Iterable<S> entities) {
    for (S entity : entities) {
        generateId(entity);
    }
    return super.save(entities);
  }

  protected <S extends T> void generateId(S entity) {
    if (!entity.isNew()) {
        return;
    }
    ID id = ...
    entity.setId(id);
  }

}

After having the custom repository implementation defined, it has to be registered. With Spring Boot this can be done very easily.

@SpringBootApplication
@EnableMongoRepositories(repositoryBaseClass = CustomMongoRepositoryImpl.class)
public class Application {

  public static void main(String[] args) {
    new SpringApplication(Application.class).run(args);
  }

}

If you’re not using Spring Boot however, it’s documented in Spring Data’s reference documentation how to implement custom repositories.

Conclusion

  • Generating IDs of data types other than ObjectId, String or BigInteger wasn't directly possible before Spring Data MongoDB 2.0, 1.10.1 and 1.9.8; see DATAMONGO-1617.
  • You're most likely affected if you're using Spring Boot 1.5.x and aren't affected if you're on Spring Boot 2.0+.