DATAMONGO-1992 - Add mutation support for immutable objects through ReactiveMongoTemplate.

Persisting methods of ReactiveMongoTemplate now return potentially new object instances of immutable objects. New instances are created using wither methods/Kotlin copy(…) methods if an immutable object requires association with an Id or the version number needs to be incremented.
This commit is contained in:
Mark Paluch
2018-06-04 11:01:11 +02:00
committed by Oliver Gierke
parent 1eab66aff4
commit ba2ab183ed
3 changed files with 106 additions and 76 deletions

View File

@@ -927,7 +927,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Insert is used to initially store the object into the database. To update an existing object use the save method.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @return the saved object.
* @return the inserted object.
*/
<T> Mono<T> insert(T objectToSave);
@@ -941,7 +941,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @param collectionName name of the collection to store the object in. Must not be {@literal null}.
* @return the saved object.
* @return the inserted object.
*/
<T> Mono<T> insert(T objectToSave, String collectionName);
@@ -950,7 +950,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @param batchToSave the batch of objects to save. Must not be {@literal null}.
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
* @return the saved objects.
* @return the inserted objects .
*/
<T> Flux<T> insert(Collection<? extends T> batchToSave, Class<?> entityClass);
@@ -959,7 +959,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @param batchToSave the list of objects to save. Must not be {@literal null}.
* @param collectionName name of the collection to store the object in. Must not be {@literal null}.
* @return the saved objects.
* @return the inserted objects.
*/
<T> Flux<T> insert(Collection<? extends T> batchToSave, String collectionName);
@@ -987,7 +987,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Insert is used to initially store the object into the database. To update an existing object use the save method.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @return the saved object.
* @return the inserted objects.
*/
<T> Mono<T> insert(Mono<? extends T> objectToSave);
@@ -996,7 +996,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @param batchToSave the publisher which provides objects to save. Must not be {@literal null}.
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
* @return the saved objects.
* @return the inserted objects.
*/
<T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> batchToSave, Class<?> entityClass);
@@ -1005,7 +1005,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*
* @param batchToSave the publisher which provides objects to save. Must not be {@literal null}.
* @param collectionName name of the collection to store the object in. Must not be {@literal null}.
* @return the saved objects.
* @return the inserted objects.
*/
<T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> batchToSave, String collectionName);
@@ -1014,7 +1014,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* class.
*
* @param objectsToSave the publisher which provides objects to save. Must not be {@literal null}.
* @return the saved objects.
* @return the inserted objects.
*/
<T> Flux<T> insertAll(Mono<? extends Collection<? extends T>> objectsToSave);

View File

@@ -144,6 +144,7 @@ import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
import com.mongodb.reactivestreams.client.Success;
import com.mongodb.util.JSONParseException;
import reactor.util.function.Tuples;
/**
* Primary implementation of {@link ReactiveMongoOperations}. It simplifies the use of Reactive MongoDB usage and helps
@@ -1252,17 +1253,18 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return Mono.defer(() -> {
initializeVersionProperty(objectToSave);
maybeEmitEvent(new BeforeConvertEvent<>(objectToSave, collectionName));
T toSave = (T) initializeVersionProperty(objectToSave);
maybeEmitEvent(new BeforeConvertEvent<>(toSave, collectionName));
Document dbDoc = toDocument(objectToSave, writer);
Document dbDoc = toDocument(toSave, writer);
maybeEmitEvent(new BeforeSaveEvent<>(objectToSave, dbDoc, collectionName));
maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName));
Mono<T> afterInsert = insertDBObject(collectionName, dbDoc, objectToSave.getClass()).flatMap(id -> {
populateIdIfNecessary(objectToSave, id);
maybeEmitEvent(new AfterSaveEvent<>(objectToSave, dbDoc, collectionName));
return Mono.just(objectToSave);
Mono<T> afterInsert = insertDBObject(collectionName, dbDoc, toSave.getClass()).map(id -> {
T saved = (T) populateIdIfNecessary(toSave, id);
maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName));
return saved;
});
return afterInsert;
@@ -1326,18 +1328,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(writer, "MongoWriter must not be null!");
Mono<List<Tuple2<T, Document>>> prepareDocuments = Flux.fromIterable(batchToSave)
.flatMap(new Function<T, Flux<Tuple2<T, Document>>>() {
@Override
public Flux<Tuple2<T, Document>> apply(T o) {
.map(o -> {
initializeVersionProperty(o);
maybeEmitEvent(new BeforeConvertEvent<>(o, collectionName));
T toSave = (T) initializeVersionProperty(o);
maybeEmitEvent(new BeforeConvertEvent<>(toSave, collectionName));
Document dbDoc = toDocument(o, writer);
Document dbDoc = toDocument(toSave, writer);
maybeEmitEvent(new BeforeSaveEvent<>(o, dbDoc, collectionName));
return Flux.zip(Mono.just(o), Mono.just(dbDoc));
}
maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName));
return Tuples.of(toSave, dbDoc);
}).collectList();
Flux<Tuple2<T, Document>> insertDocuments = prepareDocuments.flatMapMany(tuples -> {
@@ -1349,9 +1348,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return insertDocuments.map(tuple -> {
populateIdIfNecessary(tuple.getT1(), tuple.getT2().get(ID_FIELD));
maybeEmitEvent(new AfterSaveEvent<>(tuple.getT1(), tuple.getT2(), collectionName));
return tuple.getT1();
T saved = (T) populateIdIfNecessary(tuple.getT1(), tuple.getT2().get(ID_FIELD));
maybeEmitEvent(new AfterSaveEvent<>(saved, tuple.getT2(), collectionName));
return saved;
});
}
@@ -1438,17 +1437,19 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
// Bump version number
convertingAccessor.setProperty(versionProperty, versionNumber.longValue() + 1);
ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeConvertEvent<>(objectToSave, collectionName));
T toSave = (T) convertingAccessor.getBean();
Document document = ReactiveMongoTemplate.this.toDocument(objectToSave, mongoConverter);
ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeConvertEvent<T>(toSave, collectionName));
ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeSaveEvent<>(objectToSave, document, collectionName));
Document document = ReactiveMongoTemplate.this.toDocument(toSave, mongoConverter);
ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeSaveEvent<>(toSave, document, collectionName));
Update update = Update.fromDocument(document, ID_FIELD);
return doUpdate(collectionName, query, update, objectToSave.getClass(), false, false).map(updateResult -> {
return doUpdate(collectionName, query, update, toSave.getClass(), false, false).map(updateResult -> {
maybeEmitEvent(new AfterSaveEvent<>(objectToSave, document, collectionName));
return objectToSave;
maybeEmitEvent(new AfterSaveEvent<>(toSave, document, collectionName));
return toSave;
});
});
}
@@ -1465,9 +1466,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return saveDocument(collectionName, dbDoc, objectToSave.getClass()).map(id -> {
populateIdIfNecessary(objectToSave, id);
maybeEmitEvent(new AfterSaveEvent<>(objectToSave, dbDoc, collectionName));
return objectToSave;
T saved = (T) populateIdIfNecessary(objectToSave, id);
maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName));
return saved;
});
});
}
@@ -2521,10 +2522,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @param id
*/
@SuppressWarnings("unchecked")
private void populateIdIfNecessary(Object savedObject, @Nullable Object id) {
private Object populateIdIfNecessary(Object savedObject, @Nullable Object id) {
if (id == null) {
return;
return null;
}
if (savedObject instanceof Map) {
@@ -2532,13 +2533,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Map<String, Object> map = (Map<String, Object>) savedObject;
map.put(ID_FIELD, id);
return;
return map;
}
MongoPersistentProperty idProp = getIdPropertyFor(savedObject.getClass());
if (idProp == null) {
return;
return savedObject;
}
ConversionService conversionService = mongoConverter.getConversionService();
@@ -2546,10 +2547,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(savedObject);
if (accessor.getProperty(idProp) != null) {
return;
return accessor.getBean();
}
new ConvertingPropertyAccessor(accessor, conversionService).setProperty(idProp, id);
return accessor.getBean();
}
private MongoCollection<Document> getAndPrepareCollection(MongoDatabase db, String collectionName) {
@@ -2807,7 +2810,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
}
private void initializeVersionProperty(Object entity) {
private Object initializeVersionProperty(Object entity) {
MongoPersistentEntity<?> mongoPersistentEntity = getPersistentEntity(entity.getClass());
@@ -2815,7 +2818,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(
mongoPersistentEntity.getPropertyAccessor(entity), mongoConverter.getConversionService());
accessor.setProperty(mongoPersistentEntity.getRequiredVersionProperty(), 0);
return accessor.getBean();
}
return entity;
}
// Callback implementations

View File

@@ -23,6 +23,7 @@ import static org.springframework.data.mongodb.test.util.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Wither;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -57,6 +58,7 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.geo.Metrics;
@@ -96,17 +98,16 @@ public class ReactiveMongoTemplateTests {
@Before
public void setUp() {
StepVerifier
.create(template.dropCollection("people") //
.mergeWith(template.dropCollection("personX")) //
.mergeWith(template.dropCollection("collection")) //
.mergeWith(template.dropCollection(Person.class)) //
.mergeWith(template.dropCollection(Venue.class)) //
.mergeWith(template.dropCollection(PersonWithAList.class)) //
.mergeWith(template.dropCollection(PersonWithIdPropertyOfTypeObjectId.class)) //
.mergeWith(template.dropCollection(PersonWithVersionPropertyOfTypeInteger.class)) //
.mergeWith(template.dropCollection(Sample.class)) //
.mergeWith(template.dropCollection(MyPerson.class))) //
StepVerifier.create(template.dropCollection("people") //
.mergeWith(template.dropCollection("personX")) //
.mergeWith(template.dropCollection("collection")) //
.mergeWith(template.dropCollection(Person.class)) //
.mergeWith(template.dropCollection(Venue.class)) //
.mergeWith(template.dropCollection(PersonWithAList.class)) //
.mergeWith(template.dropCollection(PersonWithIdPropertyOfTypeObjectId.class)) //
.mergeWith(template.dropCollection(PersonWithVersionPropertyOfTypeInteger.class)) //
.mergeWith(template.dropCollection(Sample.class)) //
.mergeWith(template.dropCollection(MyPerson.class))) //
.verifyComplete();
}
@@ -310,12 +311,9 @@ public class ReactiveMongoTemplateTests {
public void updateFirstByEntityTypeShouldUpdateObject() {
Person person = new Person("Oliver2", 25);
StepVerifier
.create(template.insert(person) //
.then(template.updateFirst(new Query(where("age").is(25)), new Update().set("firstName", "Sven"),
Person.class)) //
.flatMapMany(p -> template.find(new Query(where("age").is(25)), Person.class)))
.consumeNextWith(actual -> {
StepVerifier.create(template.insert(person) //
.then(template.updateFirst(new Query(where("age").is(25)), new Update().set("firstName", "Sven"), Person.class)) //
.flatMapMany(p -> template.find(new Query(where("age").is(25)), Person.class))).consumeNextWith(actual -> {
assertThat(actual.getFirstName()).isEqualTo("Sven");
}).verifyComplete();
@@ -325,10 +323,9 @@ public class ReactiveMongoTemplateTests {
public void updateFirstByCollectionNameShouldUpdateObjects() {
Person person = new Person("Oliver2", 25);
StepVerifier
.create(template.insert(person, "people") //
.then(template.updateFirst(new Query(where("age").is(25)), new Update().set("firstName", "Sven"), "people")) //
.flatMapMany(p -> template.find(new Query(where("age").is(25)), Person.class, "people")))
StepVerifier.create(template.insert(person, "people") //
.then(template.updateFirst(new Query(where("age").is(25)), new Update().set("firstName", "Sven"), "people")) //
.flatMapMany(p -> template.find(new Query(where("age").is(25)), Person.class, "people")))
.consumeNextWith(actual -> {
assertThat(actual.getFirstName()).isEqualTo("Sven");
@@ -741,9 +738,8 @@ public class ReactiveMongoTemplateTests {
Document dbObject = new Document();
dbObject.put("firstName", "Oliver");
StepVerifier
.create(template.insert(dbObject, //
template.determineCollectionName(PersonWithVersionPropertyOfTypeInteger.class))) //
StepVerifier.create(template.insert(dbObject, //
template.determineCollectionName(PersonWithVersionPropertyOfTypeInteger.class))) //
.expectNextCount(1) //
.verifyComplete();
}
@@ -884,6 +880,23 @@ public class ReactiveMongoTemplateTests {
assertThat(person.version).isZero();
}
@Test // DATAMONGO-1992
public void initializesIdAndVersionAndOfImmutableObject() {
ImmutableVersioned versioned = new ImmutableVersioned();
StepVerifier.create(template.insert(versioned)).consumeNextWith(actual -> {
assertThat(actual).isNotSameAs(versioned);
assertThat(versioned.id).isNull();
assertThat(versioned.version).isNull();
assertThat(actual.id).isNotNull();
assertThat(actual.version).isEqualTo(0);
}).verifyComplete();
}
@Test // DATAMONGO-1444
public void queryCanBeNull() {
@@ -920,9 +933,8 @@ public class ReactiveMongoTemplateTests {
ReactiveMongoTemplate template = new ReactiveMongoTemplate(factory);
template.setWriteResultChecking(WriteResultChecking.EXCEPTION);
StepVerifier
.create(template.indexOps(Person.class) //
.ensureIndex(new Index().on("firstName", Direction.DESC).unique())) //
StepVerifier.create(template.indexOps(Person.class) //
.ensureIndex(new Index().on("firstName", Direction.DESC).unique())) //
.expectNextCount(1) //
.verifyComplete();
@@ -1040,9 +1052,8 @@ public class ReactiveMongoTemplateTests {
@Test // DATAMONGO-1444
public void tailStreamsData() throws InterruptedException {
StepVerifier.create(template.dropCollection("capped")
.then(template.createCollection("capped", //
CollectionOptions.empty().size(1000).maxDocuments(10).capped()))
StepVerifier.create(template.dropCollection("capped").then(template.createCollection("capped", //
CollectionOptions.empty().size(1000).maxDocuments(10).capped()))
.then(template.insert(new Document("random", Math.random()).append("key", "value"), //
"capped")))
.expectNextCount(1).verifyComplete();
@@ -1062,9 +1073,8 @@ public class ReactiveMongoTemplateTests {
@Test // DATAMONGO-1444
public void tailStreamsDataUntilCancellation() throws InterruptedException {
StepVerifier.create(template.dropCollection("capped")
.then(template.createCollection("capped", //
CollectionOptions.empty().size(1000).maxDocuments(10).capped()))
StepVerifier.create(template.dropCollection("capped").then(template.createCollection("capped", //
CollectionOptions.empty().size(1000).maxDocuments(10).capped()))
.then(template.insert(new Document("random", Math.random()).append("key", "value"), //
"capped")))
.expectNextCount(1).verifyComplete();
@@ -1397,6 +1407,19 @@ public class ReactiveMongoTemplateTests {
return p;
}
@AllArgsConstructor
@Wither
static class ImmutableVersioned {
final @Id String id;
final @Version Long version;
public ImmutableVersioned() {
id = null;
version = null;
}
}
@Data
static class Sample {