DATAMONGO-2479 - Add AfterConvertCallback and AfterSaveCallback (and their reactive versions).

Previously, only BeforeConvertCallback and BeforeSaveCallback were supported (and their reactive counterparts). This commit adds support for 'after-convert' and 'after-save' events using entity callbacks feature.

Original pull request: #839.
This commit is contained in:
Roman Puchkovskiy
2020-02-17 22:43:25 +04:00
committed by Mark Paluch
parent b0b905ddb7
commit ee59c6b774
12 changed files with 1095 additions and 139 deletions

View File

@@ -32,6 +32,7 @@ import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
@@ -62,6 +63,7 @@ import com.mongodb.client.model.*;
* @author Minsu Kim
* @author Jens Schauder
* @author Michail Nikolaev
* @author Roman Puchkovskiy
* @since 1.9
*/
class DefaultBulkOperations implements BulkOperations {
@@ -300,6 +302,7 @@ class DefaultBulkOperations implements BulkOperations {
Assert.state(result != null, "Result must not be null.");
models.forEach(this::maybeEmitAfterSaveEvent);
models.forEach(this::maybeInvokeAfterSaveCallback);
return result;
} finally {
@@ -447,6 +450,19 @@ class DefaultBulkOperations implements BulkOperations {
}
}
private void maybeInvokeAfterSaveCallback(SourceAwareWriteModelHolder it) {
if (it.getModel() instanceof InsertOneModel) {
Document target = ((InsertOneModel<Document>) it.getModel()).getDocument();
maybeInvokeAfterSaveCallback(it.getSource(), target);
} else if (it.getModel() instanceof ReplaceOneModel) {
Document target = ((ReplaceOneModel<Document>) it.getModel()).getReplacement();
maybeInvokeAfterSaveCallback(it.getSource(), target);
}
}
private <E extends MongoMappingEvent<T>, T> E maybeEmitEvent(E event) {
if (null != bulkOperationContext.getEventPublisher()) {
@@ -475,6 +491,16 @@ class DefaultBulkOperations implements BulkOperations {
collectionName);
}
private Object maybeInvokeAfterSaveCallback(Object value, Document mappedDocument) {
if (bulkOperationContext.getEntityCallbacks() == null) {
return value;
}
return bulkOperationContext.getEntityCallbacks().callback(AfterSaveCallback.class, value, mappedDocument,
collectionName);
}
private static BulkWriteOptions getBulkWriteOptions(BulkMode bulkMode) {
BulkWriteOptions options = new BulkWriteOptions();

View File

@@ -33,6 +33,7 @@ import org.bson.Document;
import org.bson.conversions.Bson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -70,16 +71,7 @@ import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.JsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.convert.*;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher;
@@ -87,16 +79,7 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCre
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback;
import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent;
import org.springframework.data.mongodb.core.mapping.event.*;
import org.springframework.data.mongodb.core.mapreduce.GroupBy;
import org.springframework.data.mongodb.core.mapreduce.GroupByResults;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
@@ -128,16 +111,7 @@ import com.mongodb.ClientSessionOptions;
import com.mongodb.MongoException;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.client.AggregateIterable;
import com.mongodb.client.ClientSession;
import com.mongodb.client.DistinctIterable;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MapReduceIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import com.mongodb.client.*;
import com.mongodb.client.model.*;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
@@ -166,6 +140,7 @@ import com.mongodb.client.result.UpdateResult;
* @author Andreas Zink
* @author Cimon Lucas
* @author Michael J. Simons
* @author Roman Puchkovskiy
*/
public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider {
@@ -1070,8 +1045,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName));
maybeCallBeforeSave(replacement, mappedReplacement, collectionName);
return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort,
T saved = doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort,
queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, resultType);
if (saved != null) {
maybeEmitEvent(new AfterSaveEvent<>(saved, mappedReplacement, collectionName));
return maybeCallAfterSave(saved, mappedReplacement, collectionName);
}
return saved;
}
// Find methods that take a Query to express the query and that return a single object that is also removed from the
@@ -1233,8 +1214,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
T saved = populateIdIfNecessary(initialized, id);
maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName));
return saved;
return maybeCallAfterSave(saved, dbDoc, collectionName);
}
@Override
@@ -1327,8 +1307,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
if (i < ids.size()) {
T saved = populateIdIfNecessary(obj, ids.get(i));
maybeEmitEvent(new AfterSaveEvent<>(saved, documentList.get(i), collectionName));
savedObjects.add(saved);
Document doc = documentList.get(i);
maybeEmitEvent(new AfterSaveEvent<>(saved, doc, collectionName));
savedObjects.add(maybeCallAfterSave(saved, doc, collectionName));
} else {
savedObjects.add(obj);
}
@@ -1398,7 +1379,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
maybeEmitEvent(new AfterSaveEvent<>(toSave, mapped.getDocument(), collectionName));
return toSave;
return maybeCallAfterSave(toSave, mapped.getDocument(), collectionName);
}
protected <T> T doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
@@ -1419,7 +1400,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
T saved = populateIdIfNecessary(objectToSave, id);
maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName));
return saved;
return maybeCallAfterSave(saved, dbDoc, collectionName);
}
@SuppressWarnings("ConstantConditions")
@@ -2312,7 +2293,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return event;
}
@SuppressWarnings("unchecked")
protected <T> T maybeCallBeforeConvert(T object, String collection) {
if (null != entityCallbacks) {
@@ -2322,7 +2302,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return object;
}
@SuppressWarnings("unchecked")
protected <T> T maybeCallBeforeSave(T object, Document document, String collection) {
if (null != entityCallbacks) {
@@ -2332,6 +2311,24 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return object;
}
protected <T> T maybeCallAfterSave(T object, Document document, String collection) {
if (null != entityCallbacks) {
return entityCallbacks.callback(AfterSaveCallback.class, object, document, collection);
}
return object;
}
protected <T> T maybeCallAfterConvert(T object, Document document, String collection) {
if (null != entityCallbacks) {
return entityCallbacks.callback(AfterConvertCallback.class, object, document, collection);
}
return object;
}
/**
* Create the specified collection using the provided options
*
@@ -3101,6 +3098,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Roman Puchkovskiy
*/
@RequiredArgsConstructor
private class ReadDocumentCallback<T> implements DocumentCallback<T> {
@@ -3110,16 +3108,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
private final String collectionName;
@Nullable
public T doWith(@Nullable Document object) {
public T doWith(@Nullable Document document) {
if (null != object) {
maybeEmitEvent(new AfterLoadEvent<>(object, type, collectionName));
if (null != document) {
maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName));
}
T source = reader.read(type, object);
T source = reader.read(type, document);
if (null != source) {
maybeEmitEvent(new AfterConvertEvent<>(object, source, collectionName));
maybeEmitEvent(new AfterConvertEvent<>(document, source, collectionName));
source = maybeCallAfterConvert(source, document, collectionName);
}
return source;
@@ -3148,24 +3147,25 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*/
@SuppressWarnings("unchecked")
@Nullable
public T doWith(@Nullable Document object) {
public T doWith(@Nullable Document document) {
if (object == null) {
if (document == null) {
return null;
}
Class<?> typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType
: targetType;
if (null != object) {
maybeEmitEvent(new AfterLoadEvent<>(object, targetType, collectionName));
if (null != document) {
maybeEmitEvent(new AfterLoadEvent<>(document, targetType, collectionName));
}
Object source = reader.read(typeToRead, object);
Object source = reader.read(typeToRead, document);
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, source) : source;
if (null != result) {
maybeEmitEvent(new AfterConvertEvent<>(object, result, collectionName));
maybeEmitEvent(new AfterConvertEvent<>(document, result, collectionName));
result = maybeCallAfterConvert(result, document, collectionName);
}
return (T) result;

View File

@@ -74,16 +74,7 @@ import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.PrefixingDelegatingAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.JsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.convert.*;
import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher;
import org.springframework.data.mongodb.core.index.ReactiveIndexOperations;
import org.springframework.data.mongodb.core.index.ReactiveMongoPersistentEntityIndexCreator;
@@ -91,16 +82,7 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent;
import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback;
import org.springframework.data.mongodb.core.mapping.event.*;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Meta;
@@ -127,16 +109,7 @@ import com.mongodb.CursorType;
import com.mongodb.MongoException;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndReplaceOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.ValidationOptions;
import com.mongodb.client.model.*;
import com.mongodb.client.model.changestream.FullDocument;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.InsertOneResult;
@@ -163,6 +136,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase;
*
* @author Mark Paluch
* @author Christoph Strobl
* @author Roman Puchkovskiy
* @since 2.0
*/
public class ReactiveMongoTemplate implements ReactiveMongoOperations, ApplicationContextAware {
@@ -1050,7 +1024,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
cursor = cursor.maxTime(options.getMaxTime().toMillis(), TimeUnit.MILLISECONDS);
}
return Flux.from(cursor).map(readCallback::doWith);
return Flux.from(cursor).concatMap(readCallback::doWith);
}
/*
@@ -1093,7 +1067,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
.withOptions(AggregationOptions.builder().collation(near.getCollation()).build());
return aggregate($geoNear, collection, Document.class) //
.map(callback::doWith);
.concatMap(callback::doWith);
}
/*
@@ -1186,9 +1160,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}).flatMap(it -> {
PersistableEntityModel<S> flowObject = (PersistableEntityModel<S>) it;
return doFindAndReplace(flowObject.getCollection(), mappedQuery, mappedFields, mappedSort,
queryContext.getCollation(entityType).orElse(null), entityType, flowObject.getTarget(), options,
resultType);
Mono<T> afterFindAndReplace = doFindAndReplace(flowObject.getCollection(), mappedQuery,
mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null),
entityType, flowObject.getTarget(), options, resultType);
return afterFindAndReplace.flatMap(saved -> {
maybeEmitEvent(new AfterSaveEvent<>(saved, flowObject.getTarget(), flowObject.getCollection()));
return maybeCallAfterSave(saved, flowObject.getTarget(), flowObject.getCollection());
});
});
}
@@ -1345,12 +1323,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}).flatMap(it -> {
return insertDocument(it.getCollection(), it.getTarget(), it.getSource().getClass()).map(id -> {
return insertDocument(it.getCollection(), it.getTarget(), it.getSource().getClass()).flatMap(id -> {
T saved = operations.forEntity(it.getSource(), mongoConverter.getConversionService())
.populateIdIfNecessary(id);
maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), collectionName));
return saved;
return maybeCallAfterSave(saved, it.getTarget(), collectionName);
});
});
}
@@ -1436,13 +1414,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return insertDocumentList(collectionName, documents).thenMany(Flux.fromIterable(tuples));
});
return insertDocuments.map(tuple -> {
return insertDocuments.flatMap(tuple -> {
Object id = MappedDocument.of(tuple.getT2()).getId();
Document document = tuple.getT2();
Object id = MappedDocument.of(document).getId();
T saved = tuple.getT1().populateIdIfNecessary(id);
maybeEmitEvent(new AfterSaveEvent<>(saved, tuple.getT2(), collectionName));
return saved;
maybeEmitEvent(new AfterSaveEvent<>(saved, document, collectionName));
return maybeCallAfterSave(saved, document, collectionName);
});
}
@@ -1522,9 +1501,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
maybeEmitEvent(new BeforeSaveEvent<>(toConvert, document, collectionName));
return maybeCallBeforeSave(toConvert, document, collectionName).flatMap(it -> {
return doUpdate(collectionName, query, mapped.updateWithoutId(), it.getClass(), false, false).map(result -> {
return maybeEmitEvent(new AfterSaveEvent<T>(it, document, collectionName)).getSource();
});
return doUpdate(collectionName, query, mapped.updateWithoutId(), it.getClass(), false, false)
.flatMap(result -> {
maybeEmitEvent(new AfterSaveEvent<T>(it, document, collectionName));
return maybeCallAfterSave(it, document, collectionName);
});
});
});
});
@@ -1546,10 +1527,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return maybeCallBeforeSave(toConvert, dbDoc, collectionName).flatMap(it -> {
return saveDocument(collectionName, dbDoc, it.getClass()).map(id -> {
return saveDocument(collectionName, dbDoc, it.getClass()).flatMap(id -> {
T saved = entity.populateIdIfNecessary(id);
return maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)).getSource();
maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName));
return maybeCallAfterSave(saved, dbDoc, collectionName);
});
});
});
@@ -2213,7 +2195,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
publisher = collation.map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher);
return Flux.from(publisher)
.map(new ReadDocumentCallback<>(mongoConverter, resultType, inputCollectionName)::doWith);
.concatMap(new ReadDocumentCallback<>(mongoConverter, resultType, inputCollectionName)::doWith);
});
}
@@ -2613,7 +2595,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return event;
}
@SuppressWarnings("unchecked")
protected <T> Mono<T> maybeCallBeforeConvert(T object, String collection) {
if (null != entityCallbacks) {
@@ -2623,7 +2604,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return Mono.just(object);
}
@SuppressWarnings("unchecked")
protected <T> Mono<T> maybeCallBeforeSave(T object, Document document, String collection) {
if (null != entityCallbacks) {
@@ -2633,6 +2613,24 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return Mono.just(object);
}
protected <T> Mono<T> maybeCallAfterSave(T object, Document document, String collection) {
if (null != entityCallbacks) {
return entityCallbacks.callback(ReactiveAfterSaveCallback.class, object, document, collection);
}
return Mono.just(object);
}
protected <T> Mono<T> maybeCallAfterConvert(T object, Document document, String collection) {
if (null != entityCallbacks) {
return entityCallbacks.callback(ReactiveAfterConvertCallback.class, object, document, collection);
}
return Mono.just(object);
}
private MongoCollection<Document> getAndPrepareCollection(MongoDatabase db, String collectionName) {
try {
@@ -2720,7 +2718,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
DocumentCallback<T> objectCallback, String collectionName) {
return createMono(collectionName,
collection -> Mono.from(collectionCallback.doInCollection(collection)).map(objectCallback::doWith));
collection -> Mono.from(collectionCallback.doInCollection(collection)).flatMap(objectCallback::doWith));
}
/**
@@ -2746,7 +2744,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return createFlux(collectionName, collection -> {
return Flux.from(preparer.initiateFind(collection, collectionCallback::doInCollection))
.map(objectCallback::doWith);
.concatMap(objectCallback::doWith);
});
}
@@ -3042,7 +3040,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
interface DocumentCallback<T> {
T doWith(Document object);
Mono<T> doWith(Document object);
}
/**
@@ -3071,6 +3069,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* {@link EntityReader}.
*
* @author Mark Paluch
* @author Roman Puchkovskiy
*/
class ReadDocumentCallback<T> implements DocumentCallback<T> {
@@ -3088,27 +3087,33 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.collectionName = collectionName;
}
public T doWith(@Nullable Document object) {
public Mono<T> doWith(Document document) {
if (null != object) {
maybeEmitEvent(new AfterLoadEvent<>(object, type, collectionName));
}
T source = reader.read(type, object);
maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName));
T source = reader.read(type, document);
if (null != source) {
maybeEmitEvent(new AfterConvertEvent<>(object, source, collectionName));
maybeEmitEvent(new AfterConvertEvent<>(document, source, collectionName));
}
return source;
return Mono.defer(() -> {
if (null != source) {
return maybeCallAfterConvert(source, document, collectionName);
} else {
return Mono.empty();
}
});
}
}
/**
* {@link MongoTemplate.DocumentCallback} transforming {@link Document} into the given {@code targetType} or
* {@link DocumentCallback} transforming {@link Document} into the given {@code targetType} or
* decorating the {@code sourceType} with a {@literal projection} in case the {@code targetType} is an
* {@litera interface}.
* {@literal interface}.
*
* @param <S>
* @param <T>
* @author Christoph Strobl
* @author Roman Puchkovskiy
* @since 2.0
*/
@RequiredArgsConstructor
@@ -3119,29 +3124,30 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final @NonNull Class<T> targetType;
private final @NonNull String collectionName;
@Nullable
@SuppressWarnings("unchecked")
public T doWith(@Nullable Document object) {
if (object == null) {
return null;
}
public Mono<T> doWith(Document document) {
Class<?> typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) //
? entityType //
: targetType;
if (null != object) {
maybeEmitEvent(new AfterLoadEvent<>(object, typeToRead, collectionName));
}
maybeEmitEvent(new AfterLoadEvent<>(document, typeToRead, collectionName));
Object source = reader.read(typeToRead, object);
Object source = reader.read(typeToRead, document);
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, source) : source;
if (null != source) {
maybeEmitEvent(new AfterConvertEvent<>(object, result, collectionName));
T castEntity = (T) result;
if (null != castEntity) {
maybeEmitEvent(new AfterConvertEvent<>(document, castEntity, collectionName));
}
return (T) result;
return Mono.defer(() -> {
if (null != castEntity) {
return maybeCallAfterConvert(castEntity, document, collectionName);
} else {
return Mono.empty();
}
});
}
}
@@ -3151,6 +3157,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*
* @author Mark Paluch
* @author Chrstoph Strobl
* @author Roman Puchkovskiy
*/
static class GeoNearResultDocumentCallback<T> implements DocumentCallback<GeoResult<T>> {
@@ -3175,16 +3182,17 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.metric = metric;
}
public GeoResult<T> doWith(Document object) {
public Mono<GeoResult<T>> doWith(Document object) {
double distance = Double.NaN;
final double distance;
if (object.containsKey(distanceField)) {
distance = NumberUtils.convertNumberToTargetClass(object.get(distanceField, Number.class), Double.class);
} else {
distance = Double.NaN;
}
T doWith = delegate.doWith(object);
return new GeoResult<>(doWith, new Distance(distance, metric));
return delegate.doWith(object)
.map(doWith -> new GeoResult<>(doWith, new Distance(distance, metric)));
}
}
@@ -3202,7 +3210,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.type = type;
}
@SuppressWarnings("deprecation")
public FindPublisher<Document> prepare(FindPublisher<Document> findPublisher) {
FindPublisher<Document> findPublisherToUse = operations.forType(type) //

View File

@@ -50,6 +50,7 @@ import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
@@ -64,6 +65,7 @@ import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent;
import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent;
@@ -91,6 +93,7 @@ import com.mongodb.DBRef;
* @author Christoph Strobl
* @author Jordi Llach
* @author Mark Paluch
* @author Roman Puchkovskiy
*/
public class MappingMongoConverter extends AbstractMongoConverter implements ApplicationContextAware {
@@ -110,6 +113,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
protected @Nullable CodecRegistryProvider codecRegistryProvider;
private SpELContext spELContext;
private @Nullable EntityCallbacks entityCallbacks;
/**
* Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}.
@@ -212,6 +216,26 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
this.applicationContext = applicationContext;
this.spELContext = new SpELContext(this.spELContext, applicationContext);
if (entityCallbacks == null) {
setEntityCallbacks(EntityCallbacks.create(applicationContext));
}
}
/**
* Set the {@link EntityCallbacks} instance to use when invoking
* {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the {@link AfterConvertCallback}.
* <p />
* Overrides potentially existing {@link EntityCallbacks}.
*
* @param entityCallbacks must not be {@literal null}.
* @throws IllegalArgumentException if the given instance is {@literal null}.
* @since 3.0
*/
public void setEntityCallbacks(EntityCallbacks entityCallbacks) {
Assert.notNull(entityCallbacks, "EntityCallbacks must not be null!");
this.entityCallbacks = entityCallbacks;
}
/*
@@ -1605,7 +1629,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
: bulkReadRefs(dbrefs);
String collectionName = dbrefs.iterator().next().getCollectionName();
List<T> targeList = new ArrayList<>(dbrefs.size());
List<T> targetList = new ArrayList<>(dbrefs.size());
for (Document document : referencedRawDocuments) {
@@ -1613,15 +1637,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
maybeEmitEvent(new AfterLoadEvent<>(document, (Class<T>) rawType, collectionName));
}
final T target = (T) read(type, document, path);
targeList.add(target);
T target = (T) read(type, document, path);
if (target != null) {
maybeEmitEvent(new AfterConvertEvent<>(document, target, collectionName));
target = maybeCallAfterConvert(target, document, collectionName);
}
targetList.add(target);
}
return targeList;
return targetList;
}
private void maybeEmitEvent(MongoMappingEvent<?> event) {
@@ -1635,6 +1661,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return this.applicationContext != null;
}
protected <T> T maybeCallAfterConvert(T object, Document document, String collection) {
if (null != entityCallbacks) {
return entityCallbacks.callback(AfterConvertCallback.class, object, document, collection);
}
return object;
}
/**
* Performs the fetch operation for the given {@link DBRef}.
*

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping.event;
import org.bson.Document;
import org.springframework.data.mapping.callback.EntityCallback;
/**
* Callback being invoked after a domain object is converted from a Document (when reading from the DB).
*
* @author Roman Puchkovskiy
* @since 3.0
*/
@FunctionalInterface
public interface AfterConvertCallback<T> extends EntityCallback<T> {
/**
* Entity callback method invoked after a domain object is converted from a Document. Can return either the same
* or a modified instance of the domain object.
*
* @param entity the domain object (the result of the conversion).
* @param document must not be {@literal null}.
* @param collection name of the collection.
* @return the domain object that is the result of the conversion from the Document.
*/
T onAfterConvert(T entity, Document document, String collection);
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping.event;
import org.bson.Document;
import org.springframework.data.mapping.callback.EntityCallback;
/**
* Entity callback triggered after save of a document.
*
* @author Roman Puchkovskiy
* @since 3.0
*/
@FunctionalInterface
public interface AfterSaveCallback<T> extends EntityCallback<T> {
/**
* Entity callback method invoked after a domain object is saved. Can return either the same or a modified instance
* of the domain object.
*
* @param entity the domain object that was saved.
* @param document {@link Document} representing the {@code entity}.
* @param collection name of the collection.
* @return the domain object that was persisted.
*/
T onAfterSave(T entity, Document document, String collection);
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping.event;
import org.bson.Document;
import org.reactivestreams.Publisher;
import org.springframework.data.mapping.callback.EntityCallback;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
/**
* Callback being invoked after a domain object is converted from a Document (when reading from the DB).
*
* @author Roman Puchkovskiy
* @since 3.0
* @see ReactiveEntityCallbacks
*/
@FunctionalInterface
public interface ReactiveAfterConvertCallback<T> extends EntityCallback<T> {
/**
* Entity callback method invoked after a domain object is converted from a Document. Can return either the same
* or a modified instance of the domain object.
*
* @param entity the domain object (the result of the conversion).
* @param document must not be {@literal null}.
* @param collection name of the collection.
* @return a {@link Publisher} emitting the domain object that is the result of the conversion from the Document.
*/
Publisher<T> onAfterConvert(T entity, Document document, String collection);
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping.event;
import org.bson.Document;
import org.reactivestreams.Publisher;
import org.springframework.data.mapping.callback.EntityCallback;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
/**
* Entity callback triggered after save of a document.
*
* @author Roman Puchkovskiy
* @since 3.0
* @see ReactiveEntityCallbacks
*/
@FunctionalInterface
public interface ReactiveAfterSaveCallback<T> extends EntityCallback<T> {
/**
* Entity callback method invoked after a domain object is saved. Can return either the same or a modified instance
* of the domain object.
*
* @param entity the domain object that was saved.
* @param document {@link Document} representing the {@code entity}.
* @param collection name of the collection.
* @return a {@link Publisher} emitting the domain object to be returned to the caller.
*/
Publisher<T> onAfterSave(T entity, Document document, String collection);
}

View File

@@ -54,6 +54,7 @@ import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
@@ -84,6 +85,7 @@ import com.mongodb.client.model.WriteModel;
* @author Mark Paluch
* @author Minsu Kim
* @author Jens Schauder
* @author Roman Puchkovskiy
*/
@ExtendWith(MockitoExtension.class)
class DefaultBulkOperationsUnitTests {
@@ -208,16 +210,17 @@ class DefaultBulkOperationsUnitTests {
assertThat(updateModel.getReplacement().getString("lastName")).isEqualTo("Kim");
}
@Test // DATAMONGO-2261
@Test // DATAMONGO-2261, DATAMONGO-2479
void bulkInsertInvokesEntityCallbacks() {
BeforeConvertPersonCallback beforeConvertCallback = spy(new BeforeConvertPersonCallback());
BeforeSavePersonCallback beforeSaveCallback = spy(new BeforeSavePersonCallback());
AfterSavePersonCallback afterSaveCallback = spy(new AfterSavePersonCallback());
ops = new DefaultBulkOperations(template, "collection-1",
new BulkOperationContext(BulkMode.ORDERED, Optional.of(mappingContext.getPersistentEntity(Person.class)),
new QueryMapper(converter), new UpdateMapper(converter), null,
EntityCallbacks.create(beforeConvertCallback, beforeSaveCallback)));
EntityCallbacks.create(beforeConvertCallback, beforeSaveCallback, afterSaveCallback)));
Person entity = new Person("init");
ops.insert(entity);
@@ -229,11 +232,13 @@ class DefaultBulkOperationsUnitTests {
ops.execute();
verify(beforeSaveCallback).onBeforeSave(personArgumentCaptor.capture(), any(), eq("collection-1"));
assertThat(personArgumentCaptor.getAllValues()).extracting("firstName").containsExactly("init", "before-convert");
verify(afterSaveCallback).onAfterSave(personArgumentCaptor.capture(), any(), eq("collection-1"));
assertThat(personArgumentCaptor.getAllValues()).extracting("firstName")
.containsExactly("init", "before-convert", "before-convert");
verify(collection).bulkWrite(captor.capture(), any());
InsertOneModel<Document> updateModel = (InsertOneModel<Document>) captor.getValue().get(0);
assertThat(updateModel.getDocument()).containsEntry("firstName", "before-save");
assertThat(updateModel.getDocument()).containsEntry("firstName", "after-save");
}
@Test // DATAMONGO-2290
@@ -365,6 +370,16 @@ class DefaultBulkOperationsUnitTests {
}
}
static class AfterSavePersonCallback implements AfterSaveCallback<Person> {
@Override
public Person onAfterSave(Person entity, Document document, String collection) {
document.put("firstName", "after-save");
return new Person("after-save");
}
}
static class NullExceptionTranslator implements PersistenceExceptionTranslator {
@Override

View File

@@ -25,7 +25,9 @@ import java.math.BigInteger;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -37,6 +39,7 @@ import org.assertj.core.api.Assertions;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -81,6 +84,9 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCre
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback;
@@ -97,10 +103,13 @@ import org.springframework.lang.Nullable;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.CollectionUtils;
import com.google.common.collect.ImmutableMap;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoException;
import com.mongodb.MongoNamespace;
import com.mongodb.ReadPreference;
import com.mongodb.ServerAddress;
import com.mongodb.ServerCursor;
import com.mongodb.WriteConcern;
import com.mongodb.client.AggregateIterable;
import com.mongodb.client.DistinctIterable;
@@ -129,6 +138,7 @@ import com.mongodb.client.result.UpdateResult;
* @author Christoph Strobl
* @author Mark Paluch
* @author Michael J. Simons
* @author Roman Puchkovskiy
*/
@MockitoSettings(strictness = Strictness.LENIENT)
public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@@ -1921,6 +1931,241 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
verify(findIterable).projection(new Document("country", 1).append("userid", 1));
}
@Test // DATAMONGO-2479
public void findShouldInvokeAfterConvertCallback() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(findIterable.iterator()).thenReturn(new OneElementCursor<>(document));
template.find(new Query(), Person.class);
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
}
private Document initialLukeDocument() {
return new Document(ImmutableMap.of(
"_id", "init",
"firstname", "luke"
));
}
private Person initialLuke() {
Person expectedEnitty = new Person();
expectedEnitty.id = "init";
expectedEnitty.firstname = "luke";
return expectedEnitty;
}
@Test // DATAMONGO-2479
public void findByIdShouldInvokeAfterConvertCallback() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(findIterable.first()).thenReturn(document);
template.findById("init", Person.class);
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
}
@Test // DATAMONGO-2479
public void findOneShouldInvokeAfterConvertCallback() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(findIterable.first()).thenReturn(document);
template.findOne(new Query(), Person.class);
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
}
@Test // DATAMONGO-2479
public void findAllShouldInvokeAfterConvertCallback() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(findIterable.iterator()).thenReturn(new OneElementCursor<>(document));
template.findAll(Person.class);
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
}
@Test // DATAMONGO-2479
public void findAndModifyShouldInvokeAfterConvertCallback() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(collection.findOneAndUpdate(any(Bson.class), any(Bson.class), any())).thenReturn(document);
template.findAndModify(new Query(), new Update(), Person.class);
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
}
@Test // DATAMONGO-2479
public void findAndRemoveShouldInvokeAfterConvertCallback() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(collection.findOneAndDelete(any(Bson.class), any())).thenReturn(document);
template.findAndRemove(new Query(), Person.class);
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
}
@Test // DATAMONGO-2479
public void findAllAndRemoveShouldInvokeAfterConvertCallback() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(findIterable.iterator()).thenReturn(new OneElementCursor<>(document));
template.findAllAndRemove(new Query(), Person.class);
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
}
@Test // DATAMONGO-2479
public void findAndReplaceShouldInvokeAfterConvertCallbacks() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback));
Person entity = initialLuke();
Document document = initialLukeDocument();
when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(document);
Person saved = template.findAndReplace(new Query(), entity);
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
assertThat(saved.id).isEqualTo("after-convert");
}
@Test // DATAMONGO-2479
public void saveShouldInvokeAfterSaveCallbacks() {
ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterSaveCallback));
Person entity = initialLuke();
Person saved = template.save(entity);
verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString());
assertThat(saved.id).isEqualTo("after-save");
}
@Test // DATAMONGO-2479
public void insertShouldInvokeAfterSaveCallbacks() {
ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterSaveCallback));
Person entity = initialLuke();
Person saved = template.insert(entity);
verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString());
assertThat(saved.id).isEqualTo("after-save");
}
@Test // DATAMONGO-2479
public void insertAllShouldInvokeAfterSaveCallbacks() {
ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterSaveCallback));
Person entity1 = new Person();
entity1.id = "1";
entity1.firstname = "luke";
Person entity2 = new Person();
entity1.id = "2";
entity1.firstname = "luke";
Collection<Person> saved = template.insertAll(Arrays.asList(entity1, entity2));
verify(afterSaveCallback, times(2)).onAfterSave(any(), any(), anyString());
assertThat(saved.iterator().next().getId()).isEqualTo("after-save");
}
@Test // DATAMONGO-2479
public void findAndReplaceShouldInvokeAfterSaveCallbacks() {
ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback());
template.setEntityCallbacks(EntityCallbacks.create(afterSaveCallback));
Person entity = initialLuke();
Document document = initialLukeDocument();
when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(document);
Person saved = template.findAndReplace(new Query(), entity);
verify(afterSaveCallback).onAfterSave(eq(initialLuke()), any(), anyString());
assertThat(saved.id).isEqualTo("after-save");
}
@Test // DATAMONGO-2479
public void findAndReplaceShouldEmitAfterSaveEvent() {
AbstractMongoEventListener<Person> eventListener = new AbstractMongoEventListener<Person>() {
@Override
public void onAfterSave(AfterSaveEvent<Person> event) {
assertThat(event.getSource().id).isEqualTo("init");
event.getSource().id = "after-save-event";
}
};
StaticApplicationContext ctx = new StaticApplicationContext();
ctx.registerBean(ApplicationListener.class, () -> eventListener);
ctx.refresh();
template.setApplicationContext(ctx);
Person entity = initialLuke();
Document document = initialLukeDocument();
when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(document);
Person saved = template.findAndReplace(new Query(), entity);
assertThat(saved.id).isEqualTo("after-save-event");
}
class AutogenerateableId {
@Id BigInteger id;
@@ -2097,4 +2342,74 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
return entity;
}
}
static class ValueCapturingAfterSaveCallback extends ValueCapturingEntityCallback<Person>
implements AfterSaveCallback<Person> {
@Override
public Person onAfterSave(Person entity, Document document, String collection) {
capture(entity);
return new Person() {{
id = "after-save";
firstname = entity.firstname;
}};
}
}
static class ValueCapturingAfterConvertCallback extends ValueCapturingEntityCallback<Person>
implements AfterConvertCallback<Person> {
@Override
public Person onAfterConvert(Person entity, Document document, String collection) {
capture(entity);
return new Person() {{
id = "after-convert";
firstname = entity.firstname;
}};
}
}
static class OneElementCursor<T> implements MongoCursor<T> {
private final Iterator<T> iterator;
OneElementCursor(T element) {
iterator = Collections.singletonList(element).iterator();
}
@Override
public void close() {
// nothing to close
}
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
return iterator.next();
}
@Override
public T tryNext() {
if (iterator.hasNext()) {
return iterator.next();
} else {
return null;
}
}
@Override
public ServerCursor getServerCursor() {
throw new IllegalStateException("Not implemented");
}
@Override
public ServerAddress getServerAddress() {
throw new IllegalStateException("Not implemented");
}
}
}

View File

@@ -21,6 +21,7 @@ import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
import lombok.Data;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -32,6 +33,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.assertj.core.api.Assertions;
import org.bson.Document;
@@ -46,9 +48,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
@@ -65,6 +69,10 @@ import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback;
import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
@@ -78,6 +86,7 @@ import org.springframework.lang.Nullable;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.CollectionUtils;
import com.google.common.collect.ImmutableMap;
import com.mongodb.MongoClientSettings;
import com.mongodb.ReadPreference;
import com.mongodb.client.model.CountOptions;
@@ -88,6 +97,9 @@ import com.mongodb.client.model.FindOneAndReplaceOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.InsertManyResult;
import com.mongodb.client.result.InsertOneResult;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.reactivestreams.client.AggregatePublisher;
import com.mongodb.reactivestreams.client.DistinctPublisher;
@@ -102,6 +114,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase;
*
* @author Mark Paluch
* @author Christoph Strobl
* @author Roman Puchkovskiy
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -1096,6 +1109,286 @@ public class ReactiveMongoTemplateUnitTests {
verify(findPublisher).projection(new Document("country", 1).append("userid", 1));
}
@Test // DATAMONGO-2479
public void findShouldInvokeAfterConvertCallbacks() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(collection.find(Document.class)).thenReturn(findPublisher);
makeFindPublisherPublishJust(document);
List<Person> results = template.find(new Query(), Person.class).timeout(Duration.ofSeconds(1))
.toStream().collect(Collectors.toList());
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
assertThat(results.get(0).id).isEqualTo("after-convert");
}
private Document initialLukeDocument() {
return new Document(ImmutableMap.of(
"_id", "init",
"firstname", "luke"
));
}
private Person initialLuke() {
Person expectedEnitty = new Person();
expectedEnitty.id = "init";
expectedEnitty.firstname = "luke";
return expectedEnitty;
}
@Test // DATAMONGO-2479
public void findByIdShouldInvokeAfterConvertCallbacks() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(collection.find(any(Bson.class), eq(Document.class))).thenReturn(findPublisher);
makeFindPublisherPublishJust(document);
Person result = template.findById("init", Person.class).block(Duration.ofSeconds(1));
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
assertThat(result.id).isEqualTo("after-convert");
}
@Test // DATAMONGO-2479
public void findOneShouldInvokeAfterConvertCallbacks() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(collection.find(any(Bson.class), eq(Document.class))).thenReturn(findPublisher);
makeFindPublisherPublishJust(document);
Person result = template.findOne(new Query(), Person.class).block(Duration.ofSeconds(1));
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
assertThat(result.id).isEqualTo("after-convert");
}
@Test // DATAMONGO-2479
public void findAllShouldInvokeAfterConvertCallbacks() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(collection.find(Document.class)).thenReturn(findPublisher);
makeFindPublisherPublishJust(document);
List<Person> results = template.findAll(Person.class).timeout(Duration.ofSeconds(1))
.toStream().collect(Collectors.toList());
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
assertThat(results.get(0).id).isEqualTo("after-convert");
}
@Test // DATAMONGO-2479
public void findAndModifyShouldInvokeAfterConvertCallbacks() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(collection.findOneAndUpdate(any(Bson.class), any(Bson.class), any())).thenReturn(findPublisher);
makeFindPublisherPublishJust(document);
Person result = template.findAndModify(new Query(), new Update(), Person.class).block(Duration.ofSeconds(1));
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
assertThat(result.id).isEqualTo("after-convert");
}
@Test // DATAMONGO-2479
public void findAndRemoveShouldInvokeAfterConvertCallbacks() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(collection.findOneAndDelete(any(Bson.class), any())).thenReturn(findPublisher);
makeFindPublisherPublishJust(document);
Person result = template.findAndRemove(new Query(), Person.class).block(Duration.ofSeconds(1));
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
assertThat(result.id).isEqualTo("after-convert");
}
@Test // DATAMONGO-2479
public void findAllAndRemoveShouldInvokeAfterConvertCallbacks() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback));
Document document = initialLukeDocument();
when(collection.find(Document.class)).thenReturn(findPublisher);
makeFindPublisherPublishJust(document);
when(collection.deleteMany(any(Bson.class), any(DeleteOptions.class)))
.thenReturn(Mono.just(spy(DeleteResult.class)));
List<Person> results = template.findAllAndRemove(new Query(), Person.class).timeout(Duration.ofSeconds(1))
.toStream().collect(Collectors.toList());
verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString());
assertThat(results.get(0).id).isEqualTo("after-convert");
}
private void makeFindPublisherPublishJust(Document document) {
Publisher<Document> realPublisher = Flux.just(document);
doAnswer(invocation -> {
Subscriber<Document> subscriber = invocation.getArgument(0);
realPublisher.subscribe(subscriber);
return null;
}).when(findPublisher).subscribe(any());
}
@Test // DATAMONGO-2479
public void findAndReplaceShouldInvokeAfterConvertCallbacks() {
ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback));
when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(findPublisher);
makeFindPublisherPublishJust(initialLukeDocument());
Person entity = new Person();
entity.id = "init";
entity.firstname = "luke";
Person saved = template.findAndReplace(new Query(), entity).block(Duration.ofSeconds(1));
verify(afterConvertCallback).onAfterConvert(eq(entity), any(), anyString());
assertThat(saved.id).isEqualTo("after-convert");
}
@Test // DATAMONGO-2479
public void saveShouldInvokeAfterSaveCallbacks() {
ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterSaveCallback));
when(collection.replaceOne(any(Bson.class), any(Document.class), any(ReplaceOptions.class)))
.thenReturn(Mono.just(mock(UpdateResult.class)));
Person entity = initialLuke();
Person saved = template.save(entity).block(Duration.ofSeconds(1));
verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString());
assertThat(saved.id).isEqualTo("after-save");
}
@Test // DATAMONGO-2479
public void insertShouldInvokeAfterSaveCallbacks() {
ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterSaveCallback));
when(collection.insertOne(any())).thenReturn(Mono.just(mock(InsertOneResult.class)));
Person entity = initialLuke();
Person saved = template.insert(entity).block(Duration.ofSeconds(1));
verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString());
assertThat(saved.id).isEqualTo("after-save");
}
@Test // DATAMONGO-2479
public void insertAllShouldInvokeAfterSaveCallbacks() {
ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterSaveCallback));
Person entity1 = new Person();
entity1.id = "1";
entity1.firstname = "luke";
Person entity2 = new Person();
entity1.id = "2";
entity1.firstname = "luke";
when(collection.insertMany(anyList())).then(invocation -> {
List<?> list = invocation.getArgument(0);
return Flux.fromIterable(list).map(i -> mock(InsertManyResult.class));
});
List<Person> saved = template.insertAll(Arrays.asList(entity1, entity2))
.timeout(Duration.ofSeconds(1))
.toStream().collect(Collectors.toList());
verify(afterSaveCallback, times(2)).onAfterSave(any(), any(), anyString());
assertThat(saved.get(0).id).isEqualTo("after-save");
assertThat(saved.get(1).id).isEqualTo("after-save");
}
@Test // DATAMONGO-2479
public void findAndReplaceShouldInvokeAfterSaveCallbacks() {
ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback());
template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterSaveCallback));
when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(findPublisher);
makeFindPublisherPublishJust(initialLukeDocument());
Person entity = initialLuke();
Person saved = template.findAndReplace(new Query(), entity).block(Duration.ofSeconds(1));
verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString());
assertThat(saved.id).isEqualTo("after-save");
}
@Test // DATAMONGO-2479
public void findAndReplaceShouldEmitAfterSaveEvent() {
AbstractMongoEventListener<Person> eventListener = new AbstractMongoEventListener<Person>() {
@Override
public void onAfterSave(AfterSaveEvent<Person> event) {
assertThat(event.getSource().id).isEqualTo("init");
event.getSource().id = "after-save-event";
}
};
StaticApplicationContext ctx = new StaticApplicationContext();
ctx.registerBean(ApplicationListener.class, () -> eventListener);
ctx.refresh();
template.setApplicationContext(ctx);
Person entity = initialLuke();
Document document = initialLukeDocument();
when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(Mono.just(document));
Person saved = template.findAndReplace(new Query(), entity).block(Duration.ofSeconds(1));
assertThat(saved.id).isEqualTo("after-save-event");
}
@Data
@org.springframework.data.mongodb.core.mapping.Document(collection = "star-wars")
static class Person {
@@ -1179,4 +1472,33 @@ public class ReactiveMongoTemplateUnitTests {
return Mono.just(entity);
}
}
static class ValueCapturingAfterConvertCallback extends ValueCapturingEntityCallback<Person>
implements ReactiveAfterConvertCallback<Person> {
@Override
public Mono<Person> onAfterConvert(Person entity, Document document, String collection) {
capture(entity);
return Mono.just(new Person() {{
id = "after-convert";
firstname = entity.firstname;
}});
}
}
static class ValueCapturingAfterSaveCallback extends ValueCapturingEntityCallback<Person>
implements ReactiveAfterSaveCallback<Person> {
@Override
public Mono<Person> onAfterSave(Person entity, Document document, String collection) {
capture(entity);
return Mono.just(new Person() {{
id = "after-save";
firstname = entity.firstname;
}});
}
}
}

View File

@@ -30,6 +30,7 @@ import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import org.assertj.core.api.Assertions;
import org.bson.types.Code;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;
@@ -46,6 +47,7 @@ import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
@@ -62,6 +64,7 @@ import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.geo.Shape;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.model.MappingInstantiationException;
import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.NestedType;
@@ -75,6 +78,7 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.PersonPojoStringId;
import org.springframework.data.mongodb.core.mapping.TextScore;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.test.util.ReflectionTestUtils;
@@ -90,6 +94,7 @@ import com.mongodb.DBRef;
* @author Patrik Wasik
* @author Christoph Strobl
* @author Mark Paluch
* @author Roman Puchkovskiy
*/
@ExtendWith(MockitoExtension.class)
public class MappingMongoConverterUnitTests {
@@ -2103,6 +2108,62 @@ public class MappingMongoConverterUnitTests {
.isEqualTo(new BasicDBObject("property", "value"));
}
@Test // DATAMONGO-2479
public void entityCallbacksAreNotSetByDefault() {
Assertions.assertThat(ReflectionTestUtils.getField(converter, "entityCallbacks")).isNull();
}
@Test // DATAMONGO-2479
public void entityCallbacksShouldBeInitiatedOnSettingApplicationContext() {
ApplicationContext ctx = new StaticApplicationContext();
converter.setApplicationContext(ctx);
Assertions.assertThat(ReflectionTestUtils.getField(converter, "entityCallbacks")).isNotNull();
}
@Test // DATAMONGO-2479
public void setterForEntityCallbackOverridesContextInitializedOnes() {
ApplicationContext ctx = new StaticApplicationContext();
converter.setApplicationContext(ctx);
EntityCallbacks callbacks = EntityCallbacks.create();
converter.setEntityCallbacks(callbacks);
Assertions.assertThat(ReflectionTestUtils.getField(converter, "entityCallbacks")).isSameAs(callbacks);
}
@Test // DATAMONGO-2479
public void setterForApplicationContextShouldNotOverrideAlreadySetEntityCallbacks() {
EntityCallbacks callbacks = EntityCallbacks.create();
ApplicationContext ctx = new StaticApplicationContext();
converter.setEntityCallbacks(callbacks);
converter.setApplicationContext(ctx);
Assertions.assertThat(ReflectionTestUtils.getField(converter, "entityCallbacks")).isSameAs(callbacks);
}
@Test // DATAMONGO-2479
public void resolveDBRefMapValueShouldInvokeCallbacks() {
AfterConvertCallback<Person> afterConvertCallback = spy(new ReturningAfterConvertCallback());
converter.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback));
when(resolver.fetch(Mockito.any(DBRef.class))).thenReturn(new org.bson.Document());
DBRef dbRef = mock(DBRef.class);
org.bson.Document refMap = new org.bson.Document("foo", dbRef);
org.bson.Document document = new org.bson.Document("personMap", refMap);
DBRefWrapper result = converter.read(DBRefWrapper.class, document);
verify(afterConvertCallback).onAfterConvert(eq(result.personMap.get("foo")),
eq(new org.bson.Document()), any());
}
static class GenericType<T> {
T content;
}
@@ -2565,4 +2626,12 @@ public class MappingMongoConverterUnitTests {
Date dateAsObjectId;
}
static class ReturningAfterConvertCallback implements AfterConvertCallback<Person> {
@Override
public Person onAfterConvert(Person entity, org.bson.Document document, String collection) {
return entity;
}
}
}