How to assign custom document IDs in Spring Data MongoDB

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?

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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.

1
2
3
4
5
6
7
8
9
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@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().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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.

1
2
3
4
5
6
7
8
9
@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.

Laszlo Csontos
 

I've been coding since the age of 9. I knew from childhood that all I wanted to do was code. Now I've been coding for 25 years, with Java for 18 years and professionally for 13 years. During past projects I worked in various roles as a consultant, developer, mentor, team leader and architect. My focus areas have been database- oriented back-end applications, performance tuning techniques and distributed systems. In the last 3 years, I specialized in building microservices with the Spring Ecosystem and also contributed to some of its sub-projects. The newest venture of mine is the creation of craftingjava.com, which aims at helping young software engineers learn Spring.

Leave a Reply

avatar
  Subscribe  
Notify of