DATAMONGO-1880 - Polishing.

Turn instance methods into static ones where applicable. Avoid parameter type array cloning where possible.
Add reference to rework stack-trace inspection in order to throw ClientSessionException. Migrate MongoPersistentEntityIndexCreatorUnitTests to AssertJ. Add tests to verify simple session proxy wrapping on subsequent MongoDbFactory.withSession(…) calls.

Guard ClientSession tests with replica set rule. Remove unused code. Add non-null guards. Add missing Nullable annotations. Slightly tweak Javadoc and reference documentation.

Original pull request: #536.
This commit is contained in:
Mark Paluch
2018-03-19 11:42:56 +01:00
parent b9f7f23b8f
commit 91717e5566
28 changed files with 315 additions and 201 deletions

View File

@@ -95,7 +95,7 @@ public interface MongoDbFactory extends CodecRegistryProvider {
* Obtain a {@link ClientSession} bound instance of {@link MongoDbFactory} returning {@link MongoDatabase} instances
* that are aware and bound to the given session.
*
* @param options must not be {@literal null}.
* @param session must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/

View File

@@ -83,10 +83,9 @@ public interface ReactiveMongoDatabaseFactory extends CodecRegistryProvider {
* Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoDatabaseFactory} returning
* {@link MongoDatabase} instances that are aware and bound to the given session.
*
* @param options must not be {@literal null}.
* @param session must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
ReactiveMongoDatabaseFactory withSession(ClientSession session);
}

View File

@@ -24,9 +24,9 @@ import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import com.mongodb.WriteConcern;
@@ -40,6 +40,10 @@ import com.mongodb.session.ClientSession;
* like (eg. {@link com.mongodb.reactivestreams.client.MongoCollection#withWriteConcern(WriteConcern)} and decorate them
* if not already proxied.
*
* @param <D> Type of the actual Mongo Database.
* @param <C> Type of the actual Mongo Collection.
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
@@ -47,8 +51,8 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
private static final MethodCache METHOD_CACHE = new MethodCache();
private final ClientSession session;
private final BiFunction collectionDecorator;
private final BiFunction databaseDecorator;
private final ClientSessionOperator collectionDecorator;
private final ClientSessionOperator databaseDecorator;
private final Object target;
private final Class<?> targetType;
private final Class<?> collectionType;
@@ -56,20 +60,27 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
/**
* Create a new SessionAwareMethodInterceptor for given target.
*
*
* @param session the {@link ClientSession} to be used on invocation.
* @param target the original target object.
* @param databaseType the MongoDB database type
* @param databaseDecorator a {@link BiFunction} used to create the proxy for an imperative / reactive
* @param databaseDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive
* {@code MongoDatabase}.
* @param collectionType the MongoDB collection type.
* @param collectionCallback a {@link BiFunction} used to create the proxy for an imperative / reactive
* @param collectionDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive
* {@code MongoCollection}.
* @param <T>
* @param <T> target object type.
*/
public <T> SessionAwareMethodInterceptor(ClientSession session, T target, Class<D> databaseType,
BiFunction<ClientSession, D, D> databaseDecorator, Class<C> collectionType,
BiFunction<ClientSession, C, C> collectionDecorator) {
ClientSessionOperator<D> databaseDecorator, Class<C> collectionType,
ClientSessionOperator<C> collectionDecorator) {
Assert.notNull(session, "ClientSession must not be null!");
Assert.notNull(target, "Target must not be null!");
Assert.notNull(databaseType, "Database type must not be null!");
Assert.notNull(databaseDecorator, "Database ClientSessionOperator must not be null!");
Assert.notNull(collectionType, "Collection type must not be null!");
Assert.notNull(collectionDecorator, "Collection ClientSessionOperator must not be null!");
this.session = session;
this.target = target;
@@ -85,10 +96,11 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor(org.aopalliance.intercept.MethodInvocation)
*/
@Nullable
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
if (requiresDecoration(methodInvocation)) {
if (requiresDecoration(methodInvocation.getMethod())) {
Object target = methodInvocation.proceed();
if (target instanceof Proxy) {
@@ -98,43 +110,47 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
return decorate(target);
}
if (!requiresSession(methodInvocation)) {
if (!requiresSession(methodInvocation.getMethod())) {
return methodInvocation.proceed();
}
Optional<Method> targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType);
return !targetMethod.isPresent() ? methodInvocation.proceed()
: ReflectionUtils.invokeMethod(targetMethod.get(), target, prependSessionToArguments(methodInvocation));
: ReflectionUtils.invokeMethod(targetMethod.get(), target,
prependSessionToArguments(session, methodInvocation));
}
private boolean requiresDecoration(MethodInvocation methodInvocation) {
private boolean requiresDecoration(Method method) {
return ClassUtils.isAssignable(databaseType, methodInvocation.getMethod().getReturnType())
|| ClassUtils.isAssignable(collectionType, methodInvocation.getMethod().getReturnType());
return ClassUtils.isAssignable(databaseType, method.getReturnType())
|| ClassUtils.isAssignable(collectionType, method.getReturnType());
}
@SuppressWarnings("unchecked")
protected Object decorate(Object target) {
return ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseDecorator.apply(session, target)
: collectionDecorator.apply(session, target);
}
private boolean requiresSession(MethodInvocation methodInvocation) {
private static boolean requiresSession(Method method) {
if (ObjectUtils.isEmpty(methodInvocation.getMethod().getParameterTypes())
|| !ClassUtils.isAssignable(ClientSession.class, methodInvocation.getMethod().getParameterTypes()[0])) {
if (method.getParameterCount() == 0
|| !ClassUtils.isAssignable(ClientSession.class, method.getParameterTypes()[0])) {
return true;
}
return false;
}
private Object[] prependSessionToArguments(MethodInvocation invocation) {
private static Object[] prependSessionToArguments(ClientSession session, MethodInvocation invocation) {
Object[] args = new Object[invocation.getArguments().length + 1];
args[0] = session;
System.arraycopy(invocation.getArguments(), 0, args, 1, invocation.getArguments().length);
return args;
}
@@ -148,6 +164,13 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
private final ConcurrentReferenceHashMap<MethodClassKey, Optional<Method>> cache = new ConcurrentReferenceHashMap<>();
/**
* Lookup the target {@link Method}.
*
* @param method
* @param targetClass
* @return
*/
Optional<Method> lookup(Method method, Class<?> targetClass) {
return cache.computeIfAbsent(new MethodClassKey(method, targetClass),
@@ -165,9 +188,24 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
return ReflectionUtils.findMethod(targetType, sourceMethod.getName(), args);
}
/**
* Check whether the cache contains an entry for {@link Method} and {@link Class}.
*
* @param method
* @param targetClass
* @return
*/
boolean contains(Method method, Class<?> targetClass) {
return cache.containsKey(new MethodClassKey(method, targetClass));
}
}
/**
* Represents an operation upon two operands of the same type, producing a result of the same type as the operands
* accepting {@link ClientSession}. This is a specialization of {@link BiFunction} for the case where the operands and
* the result are all of the same type.
*
* @param <T> the type of the operands and result of the operator
*/
public interface ClientSessionOperator<T> extends BiFunction<ClientSession, T, T> {}
}

View File

@@ -97,12 +97,15 @@ public class DefaultIndexOperations implements IndexOperations {
* Creates a new {@link DefaultIndexOperations}.
*
* @param mongoOperations must not be {@literal null}.
* @param collectionName must not be {@literal null}.
* @param collectionName must not be {@literal null} or empty.
* @param type can be {@literal null}.
* @since 2.1
*/
public DefaultIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class<?> type) {
Assert.notNull(mongoOperations, "MongoOperations must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
this.mongoOperations = mongoOperations;
this.mapper = new QueryMapper(mongoOperations.getConverter());
this.collectionName = collectionName;

View File

@@ -132,6 +132,8 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
return new UncategorizedMongoDbException(ex.getMessage(), ex);
}
// may interfere with OmitStackTraceInFastThrow (enabled by default).
// see https://jira.spring.io/browse/DATAMONGO-1905
if (ex instanceof IllegalStateException) {
for (StackTraceElement elm : ex.getStackTrace()) {
if (elm.getClassName().contains("ClientSession")) {

View File

@@ -41,6 +41,7 @@ import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.CloseableIterator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.ClientSessionOptions;
import com.mongodb.Cursor;
@@ -156,8 +157,8 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T execute(String collectionName, CollectionCallback<T> action);
/**
* Obtain a session bound instance of {@link SessionScoped} binding a new {@link ClientSession} with given
* {@literal sessionOptions} to each and every command issued against MongoDB.
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding a new {@link ClientSession}
* with given {@literal sessionOptions} to each and every command issued against MongoDB.
*
* @param sessionOptions must not be {@literal null}.
* @return new instance of {@link SessionScoped}. Never {@literal null}.
@@ -166,17 +167,19 @@ public interface MongoOperations extends FluentMongoOperations {
SessionScoped withSession(ClientSessionOptions sessionOptions);
/**
* Obtain a session bound instance of {@link SessionScoped} binding the {@link ClientSession} provided by the given
* {@link Supplier} to each and every command issued against MongoDB.
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession}
* provided by the given {@link Supplier} to each and every command issued against MongoDB.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle.
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use the
* {@link SessionScoped#execute(SessionCallback, Consumer)} hook to potentially close the {@link ClientSession}.
*
* @param sessionProvider must not be {@literal null}.
* @param onComplete a simple hook called when done .Must not be {@literal null}.
* @since 2.1
*/
default SessionScoped withSession(Supplier<ClientSession> sessionProvider) {
Assert.notNull(sessionProvider, "SessionProvider must not be null!");
return new SessionScoped() {
private final Object lock = new Object();
@@ -201,7 +204,7 @@ public interface MongoOperations extends FluentMongoOperations {
}
/**
* Obtain a {@link ClientSession} bound instance of MongoOperations.
* Obtain a {@link ClientSession} bound instance of {@link MongoOperations}.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle.
*

View File

@@ -71,16 +71,7 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.Fields;
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;
@@ -141,16 +132,7 @@ import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.ValidationAction;
import com.mongodb.client.model.ValidationLevel;
import com.mongodb.client.model.*;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.session.ClientSession;
@@ -273,7 +255,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.updateMapper = that.updateMapper;
this.schemaMapper = that.schemaMapper;
this.projectionFactory = that.projectionFactory;
this.mappingContext = that.mappingContext;
}
@@ -522,10 +503,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*/
public <T> T execute(DbCallback<T> action) {
Assert.notNull(action, "DbCallbackmust not be null!");
Assert.notNull(action, "DbCallback must not be null!");
try {
MongoDatabase db = prepareDatabase(this.getDbInternal());
MongoDatabase db = prepareDatabase(this.doGetDatabase());
return action.doInDB(db);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -552,7 +533,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Assert.notNull(callback, "CollectionCallback must not be null!");
try {
MongoCollection<Document> collection = getAndPrepareCollection(getDbInternal(), collectionName);
MongoCollection<Document> collection = getAndPrepareCollection(doGetDatabase(), collectionName);
return callback.doInCollection(collection);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -565,6 +546,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*/
@Override
public SessionScoped withSession(ClientSessionOptions options) {
Assert.notNull(options, "ClientSessionOptions must not be null!");
return withSession(() -> mongoDbFactory.getSession(options));
}
@@ -574,6 +558,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*/
@Override
public MongoTemplate withSession(ClientSession session) {
Assert.notNull(session, "ClientSession must not be null!");
return new SessionBoundMongoTemplate(session, MongoTemplate.this);
}
@@ -1819,7 +1806,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
String mapFunc = replaceWithResourceIfNecessary(mapFunction);
String reduceFunc = replaceWithResourceIfNecessary(reduceFunction);
MongoCollection<Document> inputCollection = getAndPrepareCollection(getDbInternal(), inputCollectionName);
MongoCollection<Document> inputCollection = getAndPrepareCollection(doGetDatabase(), inputCollectionName);
// MapReduceOp
MapReduceIterable<Document> result = inputCollection.mapReduce(mapFunc, reduceFunc, Document.class);
@@ -2252,10 +2239,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
public MongoDatabase getDb() {
return getDbInternal();
return doGetDatabase();
}
protected MongoDatabase getDbInternal() {
protected MongoDatabase doGetDatabase() {
return mongoDbFactory.getDb();
}
@@ -2605,7 +2592,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
try {
T result = objectCallback
.doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDbInternal(), collectionName)));
.doWith(collectionCallback.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName)));
return result;
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -2640,7 +2627,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
try {
FindIterable<Document> iterable = collectionCallback
.doInCollection(getAndPrepareCollection(getDbInternal(), collectionName));
.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName));
if (preparer != null) {
iterable = preparer.prepare(iterable);
@@ -2676,7 +2663,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
try {
FindIterable<Document> iterable = collectionCallback
.doInCollection(getAndPrepareCollection(getDbInternal(), collectionName));
.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName));
if (preparer != null) {
iterable = preparer.prepare(iterable);
@@ -3437,8 +3424,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
/**
* @param session must not be {@literal null}.
* @param mongoDbFactory must not be {@literal null}.
* @param mongoConverter must not be {@literal null}.
* @param that must not be {@literal null}.
*/
SessionBoundMongoTemplate(ClientSession session, MongoTemplate that) {

View File

@@ -147,11 +147,12 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Flux<T> execute(String collectionName, ReactiveCollectionCallback<T> action);
/**
* Obtain a session bound instance of {@link SessionScoped} binding the {@link ClientSession} provided by the given
* {@link Supplier} to each and every command issued against MongoDB.
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession}
* provided by the given {@link Supplier} to each and every command issued against MongoDB.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use
* {@link #withSession(Supplier, Consumer)} to provide a hook for processing the {@link ClientSession} when done.
* {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the
* {@link ClientSession} when done.
*
* @param sessionProvider must not be {@literal null}.
* @return new instance of {@link SessionScoped}. Never {@literal null}.
@@ -165,8 +166,8 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
}
/**
* Obtain a session bound instance of {@link SessionScoped} binding a new {@link ClientSession} with given
* {@literal sessionOptions} to each and every command issued against MongoDB.
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding a new {@link ClientSession}
* with given {@literal sessionOptions} to each and every command issued against MongoDB.
*
* @param sessionOptions must not be {@literal null}.
* @return new instance of {@link SessionScoped}. Never {@literal null}.
@@ -175,11 +176,11 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions);
/**
* Obtain a session bound instance of {@link SessionScoped} binding the {@link ClientSession} provided by the given
* {@link Supplier} to each and every command issued against MongoDB.
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession}
* provided by the given {@link Supplier} to each and every command issued against MongoDB.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use the
* {@litera onComplete} hook to potentially close the {@link ClientSession}.
* {@literal onComplete} hook to potentially close the {@link ClientSession}.
*
* @param sessionProvider must not be {@literal null}.
* @return new instance of {@link SessionScoped}. Never {@literal null}.
@@ -202,6 +203,15 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
};
}
/**
* Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoOperations}.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle.
*
* @param session must not be {@literal null}.
* @return {@link ClientSession} bound instance of {@link ReactiveMongoOperations}.
* @since 2.1
*/
ReactiveMongoOperations withSession(ClientSession session);
/**

View File

@@ -105,16 +105,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.ClientSessionOptions;
import com.mongodb.CursorType;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBRef;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.*;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
@@ -430,9 +421,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
@Override
public ReactiveSessionScoped withSession(Publisher<ClientSession> sessionProvider) {
return new ReactiveSessionScoped() {
Mono<ClientSession> cachedSession = Mono.from(sessionProvider).cache();
private final Mono<ClientSession> cachedSession = Mono.from(sessionProvider).cache();
return new ReactiveSessionScoped() {
@Override
public <T> Flux<T> execute(ReactiveSessionCallback<T> action, Consumer<ClientSession> doFinally) {
@@ -474,7 +465,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(callback, "ReactiveDatabaseCallback must not be null!");
return Flux.defer(() -> callback.doInDB(prepareDatabase(getDbInternal()))).onErrorMap(translateException());
return Flux.defer(() -> callback.doInDB(prepareDatabase(doGetDatabase()))).onErrorMap(translateException());
}
/**
@@ -488,7 +479,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(callback, "ReactiveDatabaseCallback must not be null!");
return Mono.defer(() -> Mono.from(callback.doInDB(prepareDatabase(getDbInternal()))))
return Mono.defer(() -> Mono.from(callback.doInDB(prepareDatabase(doGetDatabase()))))
.onErrorMap(translateException());
}
@@ -505,7 +496,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(callback, "ReactiveDatabaseCallback must not be null!");
Mono<MongoCollection<Document>> collectionPublisher = Mono
.fromCallable(() -> getAndPrepareCollection(getDbInternal(), collectionName));
.fromCallable(() -> getAndPrepareCollection(doGetDatabase(), collectionName));
return collectionPublisher.flatMapMany(callback::doInCollection).onErrorMap(translateException());
}
@@ -524,7 +515,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(callback, "ReactiveCollectionCallback must not be null!");
Mono<MongoCollection<Document>> collectionPublisher = Mono
.fromCallable(() -> getAndPrepareCollection(getDbInternal(), collectionName));
.fromCallable(() -> getAndPrepareCollection(doGetDatabase(), collectionName));
return collectionPublisher.flatMap(collection -> Mono.from(callback.doInCollection(collection)))
.onErrorMap(translateException());
@@ -622,10 +613,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
public MongoDatabase getMongoDatabase() {
return getDbInternal();
return doGetDatabase();
}
protected MongoDatabase getDbInternal() {
protected MongoDatabase doGetDatabase() {
return mongoDatabaseFactory.getMongoDatabase();
}
@@ -2400,7 +2391,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(action, "MongoDatabaseCallback must not be null!");
try {
MongoDatabase db = this.getDbInternal();
MongoDatabase db = this.doGetDatabase();
return action.doInDatabase(db);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -3003,8 +2994,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
/**
* @param session must not be {@literal null}.
* @param mongoDbFactory must not be {@literal null}.
* @param mongoConverter must not be {@literal null}.
* @param that must not be {@literal null}.
*/
ReactiveSessionBoundMongoTemplate(ClientSession session, ReactiveMongoTemplate that) {

View File

@@ -19,10 +19,12 @@ import org.reactivestreams.Publisher;
import org.springframework.data.mongodb.core.query.Query;
/**
* Callback interface for executing operations within a {@link com.mongodb.session.ClientSession} reactively.
* Callback interface for executing operations within a {@link com.mongodb.session.ClientSession} using reactive
* infrastructure.
*
* @author Christoph Strobl
* @since 2.1
* @see com.mongodb.session.ClientSession
*/
@FunctionalInterface
public interface ReactiveSessionCallback<T> {
@@ -31,15 +33,15 @@ public interface ReactiveSessionCallback<T> {
* Execute operations against a MongoDB instance via session bound {@link ReactiveMongoOperations}. The session is
* inferred directly into the operation so that no further interaction is necessary.
* <p />
* Please note that only Spring Data specific abstractions like {@link ReactiveMongoOperations#find(Query, Class)} and
* Please note that only Spring Data-specific abstractions like {@link ReactiveMongoOperations#find(Query, Class)} and
* others are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway
* objects like {@link com.mongodb.reactivestreams.client.MongoCollection} or
* {@link om.mongodb.reactivestreams.client.MongoDatabase} via eg.
* {@link com.mongodb.reactivestreams.client.MongoDatabase} via eg.
* {@link ReactiveMongoOperations#getCollection(String)} we leave responsibility for
* {@link com.mongodb.session.ClientSession} again up to the caller.
*
* @param operations will never be {@literal null}.
* @return can be {@literal null}.
* @return never {@literal null}.
*/
Publisher<T> doInSession(ReactiveMongoOperations operations);
}

View File

@@ -26,12 +26,13 @@ import com.mongodb.session.ClientSession;
* {@link ReactiveSessionCallback}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
public interface ReactiveSessionScoped {
/**
* Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}
* Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
@@ -45,13 +46,15 @@ public interface ReactiveSessionScoped {
}
/**
* Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}
* Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*
* @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}.
* @param doFinally
* @param doFinally callback object that accepts {@link ClientSession} after invoking {@link ReactiveSessionCallback}.
* This {@link Consumer} is guaranteed to be notified in any case (successful and exceptional outcome of
* {@link ReactiveSessionCallback}).
* @param <T> return type.
* @return a result object returned by the action. Can be {@literal null}.
*/

View File

@@ -23,6 +23,7 @@ import org.springframework.lang.Nullable;
*
* @author Christoph Strobl
* @since 2.1
* @see com.mongodb.session.ClientSession
*/
public interface SessionCallback<T> {
@@ -30,7 +31,7 @@ public interface SessionCallback<T> {
* Execute operations against a MongoDB instance via session bound {@link MongoOperations}. The session is inferred
* directly into the operation so that no further interaction is necessary.
* <p />
* Please note that only Spring Data specific abstractions like {@link MongoOperations#find(Query, Class)} and others
* Please note that only Spring Data-specific abstractions like {@link MongoOperations#find(Query, Class)} and others
* are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway objects like
* {@link com.mongodb.client.MongoCollection} or {@link com.mongodb.client.MongoDatabase} via eg.
* {@link MongoOperations#getCollection(String)} we leave responsibility for {@link com.mongodb.session.ClientSession}

View File

@@ -27,12 +27,13 @@ import com.mongodb.session.ClientSession;
* The very same bound {@link ClientSession} is used for all invocations of {@code execute} on the instance.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
public interface SessionScoped {
/**
* Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}
* Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
@@ -47,13 +48,15 @@ public interface SessionScoped {
}
/**
* Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}
* Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*
* @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}.
* @param doFinally
* @param doFinally callback object that accepts {@link ClientSession} after invoking {@link SessionCallback}. This
* {@link Consumer} is guaranteed to be notified in any case (successful and exceptional outcome of
* {@link SessionCallback}).
* @param <T> return type.
* @return a result object returned by the action. Can be {@literal null}.
*/

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.core;
import lombok.Value;
import java.net.UnknownHostException;
import org.springframework.aop.framework.ProxyFactory;
@@ -174,20 +176,15 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
/**
* {@link ClientSession} bound {@link MongoDbFactory} decorating the database with a
* {@link SessionAwareMethodInterceptor}.
*
*
* @author Christoph Strobl
* @since 2.1
*/
@Value
static class ClientSessionBoundMongoDbFactory implements MongoDbFactory {
private final ClientSession session;
private final MongoDbFactory delegate;
ClientSessionBoundMongoDbFactory(ClientSession session, MongoDbFactory delegate) {
this.session = session;
this.delegate = delegate;
}
ClientSession session;
MongoDbFactory delegate;
/*
* (non-Javadoc)
@@ -265,7 +262,7 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase,
MongoCollection.class, this::proxyCollection));
return (T) factory.getProxy();
return targetType.cast(factory.getProxy());
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core;
import lombok.Value;
import reactor.core.publisher.Mono;
import java.net.UnknownHostException;
@@ -162,16 +163,11 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React
* @author Christoph Strobl
* @since 2.1
*/
@Value
static class ClientSessionBoundMongoDbFactory implements ReactiveMongoDatabaseFactory {
private final ClientSession session;
private final ReactiveMongoDatabaseFactory delegate;
ClientSessionBoundMongoDbFactory(ClientSession session, ReactiveMongoDatabaseFactory delegate) {
this.session = session;
this.delegate = delegate;
}
ClientSession session;
ReactiveMongoDatabaseFactory delegate;
/*
* (non-Javadoc)
@@ -240,7 +236,7 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React
factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase,
MongoCollection.class, this::proxyCollection));
return (T) factory.getProxy();
return targetType.cast(factory.getProxy());
}
}
}

View File

@@ -47,7 +47,7 @@ public interface DbRefResolver {
* @return
*/
@Nullable
Object resolveDbRef(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback,
Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback,
DbRefProxyHandler proxyHandler);
/**
@@ -59,7 +59,8 @@ public interface DbRefResolver {
* @param id will never be {@literal null}.
* @return
*/
DBRef createDbRef(org.springframework.data.mongodb.core.mapping.DBRef annotation, MongoPersistentEntity<?> entity,
DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation,
MongoPersistentEntity<?> entity,
Object id);
/**

View File

@@ -85,24 +85,12 @@ public class DefaultDbRefResolver implements DbRefResolver {
this.objenesis = new ObjenesisStd(true);
}
/**
* Creates a new {@link DefaultDbRefResolver} with the given {@link MongoDbFactory}.
*
* @param mongoDbFactory must not be {@literal null}.
*/
private DefaultDbRefResolver(DefaultDbRefResolver delegate) {
this.mongoDbFactory = delegate.mongoDbFactory;
this.exceptionTranslator = delegate.exceptionTranslator;
this.objenesis = delegate.objenesis;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#resolveDbRef(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.convert.DbRefResolverCallback)
*/
@Override
public Object resolveDbRef(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback,
public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback,
DbRefProxyHandler handler) {
Assert.notNull(property, "Property must not be null!");
@@ -121,7 +109,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#created(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.mapping.MongoPersistentEntity, java.lang.Object)
*/
@Override
public DBRef createDbRef(org.springframework.data.mongodb.core.mapping.DBRef annotation,
public DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation,
MongoPersistentEntity<?> entity, Object id) {
if (annotation != null && StringUtils.hasText(annotation.db())) {

View File

@@ -40,6 +40,8 @@ import com.mongodb.client.MongoDatabase;
import com.mongodb.session.ClientSession;
/**
* Unit tests for {@link SessionAwareMethodInterceptor}.
*
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
@@ -131,7 +133,7 @@ public class SessionAwareMethodInterceptorUnitTests {
}
@Test // DATAMONGO-1880
public void proxiesNewDbInstanceReturnedByMethdod() {
public void proxiesNewDbInstanceReturnedByMethod() {
MongoDatabase otherDb = mock(MongoDatabase.class);
when(targetDatabase.withCodecRegistry(any())).thenReturn(otherDb);
@@ -145,7 +147,7 @@ public class SessionAwareMethodInterceptorUnitTests {
}
@Test // DATAMONGO-1880
public void proxiesNewCollectionInstanceReturnedByMethdod() {
public void proxiesNewCollectionInstanceReturnedByMethod() {
MongoCollection otherCollection = mock(MongoCollection.class);
when(targetCollection.withCodecRegistry(any())).thenReturn(otherCollection);
@@ -176,7 +178,7 @@ public class SessionAwareMethodInterceptorUnitTests {
factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase,
MongoCollection.class, this::proxyCollection));
return (T) factory.getProxy();
return targetType.cast(factory.getProxy());
}
}

View File

@@ -21,8 +21,10 @@ import org.bson.Document;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.mongodb.test.util.ReplicaSet;
import org.springframework.data.util.Version;
import com.mongodb.ClientSessionOptions;
@@ -31,10 +33,12 @@ import com.mongodb.session.ClientSession;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
public class ClientSessionTests {
public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_6_0 = MongoVersionRule.atLeast(Version.parse("3.6.0"));
public static @ClassRule TestRule replSet = ReplicaSet.required();
MongoTemplate template;
MongoClient client;

View File

@@ -27,8 +27,10 @@ import org.bson.Document;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.mongodb.test.util.ReplicaSet;
import org.springframework.data.util.Version;
import com.mongodb.ClientSessionOptions;
@@ -38,10 +40,12 @@ import com.mongodb.session.ClientSession;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
public class ReactiveClientSessionTests {
public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_6_0 = MongoVersionRule.atLeast(Version.parse("3.6.0"));
public static @ClassRule TestRule replSet = ReplicaSet.required();
MongoClient client;
ReactiveMongoTemplate template;

View File

@@ -26,7 +26,6 @@ import static org.mockito.Mockito.anyString;
import java.lang.reflect.Proxy;
import com.mongodb.reactivestreams.client.MongoClient;
import org.bson.Document;
import org.bson.codecs.BsonValueCodec;
import org.bson.codecs.configuration.CodecRegistry;
@@ -42,7 +41,6 @@ import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate.ReactiveSessionBoundMongoTemplate;
import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory.ClientSessionBoundMongoDbFactory;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -57,13 +55,17 @@ import com.mongodb.client.model.UpdateOptions;
import com.mongodb.reactivestreams.client.AggregatePublisher;
import com.mongodb.reactivestreams.client.DistinctPublisher;
import com.mongodb.reactivestreams.client.FindPublisher;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
import com.mongodb.session.ClientSession;
/**
* Unit tests for {@link ReactiveSessionBoundMongoTemplate}.
*
* @author Christoph Strobl
*/
@SuppressWarnings("unchecked")
@RunWith(MockitoJUnitRunner.Silent.class)
public class ReactiveSessionBoundMongoTemplateUnitTests {

View File

@@ -108,6 +108,14 @@ public class SessionBoundMongoTemplateTests {
@Override
protected MongoCollection<Document> prepareCollection(MongoCollection<Document> collection) {
injectCollectionSpy(collection);
return super.prepareCollection(collection);
}
@SuppressWarnings({ "ConstantConditions", "unchecked" })
private void injectCollectionSpy(MongoCollection<Document> collection) {
InvocationHandler handler = Proxy.getInvocationHandler(collection);
Advised advised = (Advised) ReflectionTestUtils.getField(handler, "advised");
@@ -123,8 +131,6 @@ public class SessionBoundMongoTemplateTests {
ReflectionTestUtils.setField(advice, "target", spiedCollection);
}
}
return super.prepareCollection(collection);
}
};
}
@@ -294,5 +300,4 @@ public class SessionBoundMongoTemplateTests {
return converter;
}
}

View File

@@ -67,6 +67,7 @@ import com.mongodb.session.ClientSession;
*
* @author Christoph Strobl
*/
@SuppressWarnings("unchecked")
@RunWith(MockitoJUnitRunner.Silent.class)
public class SessionBoundMongoTemplateUnitTests {

View File

@@ -20,6 +20,8 @@ import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.util.ReflectionTestUtils.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.UnknownHostException;
import org.junit.Rule;
@@ -28,26 +30,29 @@ import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.authentication.UserCredentials;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.test.util.ReflectionTestUtils;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.MongoURI;
import com.mongodb.client.MongoDatabase;
import com.mongodb.session.ClientSession;
/**
* Unit tests for {@link SimpleMongoDbFactory}.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class SimpleMongoDbFactoryUnitTests {
public @Rule ExpectedException expectedException = ExpectedException.none();
@Mock MongoClient mongo;
@Mock ClientSession clientSession;
@Mock MongoDatabase database;
@Test // DATADOC-254
public void rejectsIllegalDatabaseNames() {
@@ -82,6 +87,22 @@ public class SimpleMongoDbFactoryUnitTests {
assertThat(getField(factory, "databaseName").toString(), is("myDataBase"));
}
@Test // DATAMONGO-1880
public void cascadedWithSessionUsesRootFactory() {
when(mongo.getDatabase("foo")).thenReturn(database);
MongoDbFactory factory = new SimpleMongoDbFactory(mongo, "foo");
MongoDbFactory wrapped = factory.withSession(clientSession).withSession(clientSession);
InvocationHandler invocationHandler = Proxy.getInvocationHandler(wrapped.getDb());
Object singletonTarget = AopProxyUtils
.getSingletonTarget(ReflectionTestUtils.getField(invocationHandler, "advised"));
assertThat(singletonTarget, is(sameInstance(database)));
}
@SuppressWarnings("deprecation")
private void rejectsDatabaseName(String databaseName) {

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2018 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
*
* http://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;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.test.util.ReflectionTestUtils;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoDatabase;
import com.mongodb.session.ClientSession;
/**
* Unit tests for {@link SimpleReactiveMongoDatabaseFactory}.
*
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class SimpleReactiveMongoDatabaseFactoryUnitTests {
@Mock MongoClient mongoClient;
@Mock ClientSession clientSession;
@Mock MongoDatabase database;
@Test // DATAMONGO-1880
public void cascadedWithSessionUsesRootFactory() {
when(mongoClient.getDatabase("foo")).thenReturn(database);
ReactiveMongoDatabaseFactory factory = new SimpleReactiveMongoDatabaseFactory(mongoClient, "foo");
ReactiveMongoDatabaseFactory wrapped = factory.withSession(clientSession).withSession(clientSession);
InvocationHandler invocationHandler = Proxy.getInvocationHandler(wrapped.getMongoDatabase());
Object singletonTarget = AopProxyUtils
.getSingletonTarget(ReflectionTestUtils.getField(invocationHandler, "advised"));
assertThat(singletonTarget, is(sameInstance(database)));
}
}

View File

@@ -15,30 +15,23 @@
*/
package org.springframework.data.mongodb.core.index;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Collections;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.hamcrest.core.IsEqual;
import org.hamcrest.number.IsCloseTo;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.DataAccessException;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.context.MappingContextEvent;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.DefaultIndexOperations;
import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.Document;
@@ -83,7 +76,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
when(factory.getDb()).thenReturn(db);
when(factory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator());
when(db.getCollection(collectionCaptor.capture(), Mockito.eq(org.bson.Document.class))).thenReturn((MongoCollection) collection);
when(db.getCollection(collectionCaptor.capture(), eq(org.bson.Document.class)))
.thenReturn((MongoCollection) collection);
mongoTemplate = new MongoTemplate(factory);
@@ -97,11 +91,10 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
assertThat(keysCaptor.getValue(), is(notNullValue()));
assertThat(keysCaptor.getValue().keySet(), hasItem("fieldname"));
assertThat(optionsCaptor.getValue().getName(), is("indexName"));
assertThat(optionsCaptor.getValue().isBackground(), is(false));
assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS), nullValue());
assertThat(keysCaptor.getValue()).isNotNull().containsKey("fieldname");
assertThat(optionsCaptor.getValue().getName()).isEqualTo("indexName");
assertThat(optionsCaptor.getValue().isBackground()).isFalse();
assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isNull();
}
@Test
@@ -128,8 +121,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
mappingContext.initialize();
MongoPersistentEntityIndexCreator creator = new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
assertThat(creator.isIndexCreatorFor(mappingContext), is(true));
assertThat(creator.isIndexCreatorFor(new MongoMappingContext()), is(false));
assertThat(creator.isIndexCreatorFor(mappingContext)).isTrue();
assertThat(creator.isIndexCreatorFor(new MongoMappingContext())).isFalse();
}
@Test // DATAMONGO-554
@@ -138,11 +131,10 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
MongoMappingContext mappingContext = prepareMappingContext(AnotherPerson.class);
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
assertThat(keysCaptor.getValue(), is(notNullValue()));
assertThat(keysCaptor.getValue().keySet(), hasItem("lastname"));
assertThat(optionsCaptor.getValue().getName(), is("lastname"));
assertThat(optionsCaptor.getValue().isBackground(), IsEqual.<Object> equalTo(true));
assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS), nullValue());
assertThat(keysCaptor.getValue()).isNotNull().containsKey("lastname");
assertThat(optionsCaptor.getValue().getName()).isEqualTo("lastname");
assertThat(optionsCaptor.getValue().isBackground()).isTrue();
assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isNull();
}
@Test // DATAMONGO-544
@@ -151,9 +143,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
MongoMappingContext mappingContext = prepareMappingContext(Milk.class);
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
assertThat(keysCaptor.getValue(), is(notNullValue()));
assertThat(keysCaptor.getValue().keySet(), hasItem("expiry"));
assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS), IsEqual.<Object> equalTo(60L));
assertThat(keysCaptor.getValue()).isNotNull().containsKey("expiry");
assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(60);
}
@Test // DATAMONGO-899
@@ -162,13 +153,13 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
MongoMappingContext mappingContext = prepareMappingContext(Wrapper.class);
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
assertThat(keysCaptor.getValue(), equalTo(new org.bson.Document().append("company.address.location", "2d")));
assertThat(keysCaptor.getValue()).isEqualTo(new org.bson.Document("company.address.location", "2d"));
IndexOptions opts = optionsCaptor.getValue();
assertThat(opts.getName(), is(equalTo("company.address.location")));
assertThat(opts.getMin(), IsCloseTo.closeTo(-180, 0));
assertThat(opts.getMax(), IsCloseTo.closeTo(180, 0));
assertThat(opts.getBits(), is(26));
assertThat(opts.getName()).isEqualTo("company.address.location");
assertThat(opts.getMin()).isCloseTo(-180d, offset(0d));
assertThat(opts.getMax()).isCloseTo(180d, offset(0d));
assertThat(opts.getBits()).isEqualTo(26);
}
@Test // DATAMONGO-827
@@ -177,10 +168,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
MongoMappingContext mappingContext = prepareMappingContext(EntityWithGeneratedIndexName.class);
new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate);
assertThat(keysCaptor.getValue().containsKey("name"), is(false));
assertThat(keysCaptor.getValue().keySet(), hasItem("lastname"));
assertThat(optionsCaptor.getValue().getName(), nullValue());
assertThat(keysCaptor.getValue()).doesNotContainKey("name").containsKey("lastname");
assertThat(optionsCaptor.getValue().getName()).isNull();
}
@Test // DATAMONGO-367
@@ -191,8 +180,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
ArgumentCaptor<String> collectionNameCapturer = ArgumentCaptor.forClass(String.class);
verify(db, times(1)).getCollection(collectionNameCapturer.capture(), Mockito.any());
assertThat(collectionNameCapturer.getValue(), equalTo("wrapper"));
verify(db, times(1)).getCollection(collectionNameCapturer.capture(), any());
assertThat(collectionNameCapturer.getValue()).isEqualTo("wrapper");
}
@Test // DATAMONGO-367
@@ -203,15 +192,15 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
ArgumentCaptor<String> collectionNameCapturer = ArgumentCaptor.forClass(String.class);
verify(db, times(1)).getCollection(collectionNameCapturer.capture(), Mockito.any());
assertThat(collectionNameCapturer.getValue(), equalTo("indexedDocumentWrapper"));
verify(db, times(1)).getCollection(collectionNameCapturer.capture(), any());
assertThat(collectionNameCapturer.getValue()).isEqualTo("indexedDocumentWrapper");
}
@Test(expected = DataAccessException.class) // DATAMONGO-1125
public void createIndexShouldUsePersistenceExceptionTranslatorForNonDataIntegrityConcerns() {
doThrow(new MongoException(6, "HostUnreachable")).when(collection).createIndex(Mockito.any(org.bson.Document.class),
Mockito.any(IndexOptions.class));
doThrow(new MongoException(6, "HostUnreachable")).when(collection).createIndex(any(org.bson.Document.class),
any(IndexOptions.class));
MongoMappingContext mappingContext = prepareMappingContext(Person.class);
@@ -221,8 +210,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
@Test(expected = ClassCastException.class) // DATAMONGO-1125
public void createIndexShouldNotConvertUnknownExceptionTypes() {
doThrow(new ClassCastException("o_O")).when(collection).createIndex(Mockito.any(org.bson.Document.class),
Mockito.any(IndexOptions.class));
doThrow(new ClassCastException("o_O")).when(collection).createIndex(any(org.bson.Document.class),
any(IndexOptions.class));
MongoMappingContext mappingContext = prepareMappingContext(Person.class);

View File

@@ -83,7 +83,7 @@ public class ReplicaSet implements TestRule {
}
if (!runsAsReplicaSet()) {
throw new AssumptionViolatedException("Not runnig in repl set mode");
throw new AssumptionViolatedException("Not running in repl set mode");
}
base.evaluate();
}

View File

@@ -5,11 +5,11 @@ As of version 3.6 MongoDB supports a concept of Sessions. The use of sessions en
WARNING: Operations within a client session are not isolated from operations outside the session.
Both `MongoOperations` and `ReactiveMongoOperations` provide gateway methods for tying a `ClientSession` to the operations themselves. Within the callback all operations on `MongoCollection` and `MongoDatabase` are called with the provided session via a `Proxy` without the need to add it manually. This means that a potential call to `MongoCollection#find()` is delegated to `MongoCollection#find(ClientSession)`.
Both `MongoOperations` and `ReactiveMongoOperations` provide gateway methods for tying a `ClientSession` to the operations themselves. `MongoCollection` and `MongoDatabase` use session proxy objects implementing MongoDB's collection and and database interfaces so there's no need to add a session on each call. This means that a potential call to `MongoCollection#find()` is delegated to `MongoCollection#find(ClientSession)`.
NOTE: Methods like `(Reactive)MongoOperations#getCollection` returning native MongoDB java driver gateway objects, such as `MongoCollection`, that themselves offer dedicated methods for `ClientSession` will *NOT* be wrapped by the `Proxy`. So please make sure to provide the `ClientSession` where needed when interacting directly with a `MongoCollection` or `MongoDatabase` and not via one of the `#excute` callbacks on `MongoOperations`.
NOTE: Methods like `(Reactive)MongoOperations#getCollection` returning native MongoDB Java Driver gateway objects, such as `MongoCollection`, that themselves offer dedicated methods for `ClientSession` are *NOT* be session-proxied. So make sure to provide the `ClientSession` where needed when interacting directly with a `MongoCollection` or `MongoDatabase` and not via one of the `#execute` callbacks on `MongoOperations`.
.ClientSession with MongoOperations.
.ClientSession with `MongoOperations`
====
[source,java]
----
@@ -37,14 +37,14 @@ session.close() <4>
----
<1> Obtain a new session from the server.
<2> Use `MongoOperation` methods as before. The `ClientSession` gets applied automatically.
<3> Important! Do not forget to close the session.
<3> Make sure to close the `ClientSession`.
====
WARNING: When dealing with ``DBRef``s, especially lazily loaded ones, it is essential to **not** close the `ClientSession` before all data is loaded.
WARNING: When dealing with ``DBRef``s, especially lazily loaded ones, it is essential to **not** close the `ClientSession` before all data is loaded. Otherwise, lazy fetch fails.
The reactive counterpart uses the very same building blocks as the imperative one.
.ClientSession with ReactiveMongoOperations.
.ClientSession with `ReactiveMongoOperations`
====
[source,java]
----
@@ -66,14 +66,14 @@ template.withSession(session)
return action.insert(azoth); <2>
});
}, ClientSession::close) <4>
}, ClientSession::close) <3>
.subscribe();
----
<1> Obtain a `Publisher` for new session retrieval.
<2> Use `MongoOperation` methods as before. The `ClientSession` is obtained and applied automatically.
<3> Important! Do not forget to close the session.
<2> Use `ReactiveMongoOperation` methods as before. The `ClientSession` is obtained and applied automatically.
<3> Make sure to close the `ClientSession`.
====
By using a `Publisher` providing the actual session you can defer session acquisition to the point of actual subscription.
Still you need to close the session when done in order to not pollute the server with stale sessions. Use the `doFinally` hook on `execute` to call `ClientSession#close()` when you don't need the session any more.
In case you prefer having more control over the session itself, you can always obtain the `ClientSession` via the driver and provide it via a `Supplier`.
In case you prefer having more control over the session itself, you can always obtain the `ClientSession` via the driver and provide it via a `Supplier`.