When using Spring Data MongoDB IDs can be automatically generated for documents provided that you’re using an
ObjectId
,String
orBigInteger
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
.
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.
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.
ObjectId
, String
or BigInteger
wasn't directly possible before Spring
Data MongoDB 2.0, 1.10.1 and 1.9.8; see DATAMONGO-1617.If you like Java and Spring as much as I do, sign up for my newsletter.