Polishing.
Tweak naming. Add Javadoc and documentation. See #1628 Original pull request: #4552
This commit is contained in:
@@ -18,7 +18,6 @@ package org.springframework.data.mongodb;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -30,14 +29,12 @@ import org.springframework.lang.Nullable;
|
||||
* @author Christoph Strobl
|
||||
* @since 4.3
|
||||
*/
|
||||
class DefaultMongoTransactionOptionsResolver implements MongoTransactionOptionsResolver {
|
||||
enum DefaultMongoTransactionOptionsResolver implements MongoTransactionOptionsResolver {
|
||||
|
||||
static final Lazy<MongoTransactionOptionsResolver> INSTANCE = Lazy.of(DefaultMongoTransactionOptionsResolver::new);
|
||||
INSTANCE;
|
||||
|
||||
private static final String PREFIX = "mongo:";
|
||||
|
||||
private DefaultMongoTransactionOptionsResolver() {}
|
||||
|
||||
@Override
|
||||
public MongoTransactionOptions convert(Map<String, String> options) {
|
||||
|
||||
@@ -53,7 +50,7 @@ class DefaultMongoTransactionOptionsResolver implements MongoTransactionOptionsR
|
||||
|
||||
private static void validateKeys(Set<String> keys) {
|
||||
|
||||
if (!keys.stream().allMatch(SimpleMongoTransactionOptions.KNOWN_KEYS::contains)) {
|
||||
if (!SimpleMongoTransactionOptions.KNOWN_KEYS.containsAll(keys)) {
|
||||
|
||||
throw new IllegalArgumentException("Transaction labels contained invalid values. Has to be one of %s"
|
||||
.formatted(SimpleMongoTransactionOptions.KNOWN_KEYS));
|
||||
|
||||
@@ -64,59 +64,61 @@ import com.mongodb.client.ClientSession;
|
||||
public class MongoTransactionManager extends AbstractPlatformTransactionManager
|
||||
implements ResourceTransactionManager, InitializingBean {
|
||||
|
||||
private @Nullable MongoDatabaseFactory dbFactory;
|
||||
private @Nullable MongoDatabaseFactory databaseFactory;
|
||||
private MongoTransactionOptions options;
|
||||
private MongoTransactionOptionsResolver transactionOptionsResolver;
|
||||
private final MongoTransactionOptionsResolver transactionOptionsResolver;
|
||||
|
||||
/**
|
||||
* Create a new {@link MongoTransactionManager} for bean-style usage.
|
||||
* <br />
|
||||
* Create a new {@link MongoTransactionManager} for bean-style usage. <br />
|
||||
* <strong>Note:</strong>The {@link MongoDatabaseFactory db factory} has to be
|
||||
* {@link #setDbFactory(MongoDatabaseFactory) set} before using the instance. Use this constructor to prepare a
|
||||
* {@link MongoTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}.
|
||||
* <br />
|
||||
* {@link #setDatabaseFactory(MongoDatabaseFactory) set} before using the instance. Use this constructor to prepare a
|
||||
* {@link MongoTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}. <br />
|
||||
* Optionally it is possible to set default {@link TransactionOptions transaction options} defining
|
||||
* {@link com.mongodb.ReadConcern} and {@link com.mongodb.WriteConcern}.
|
||||
*
|
||||
* @see #setDbFactory(MongoDatabaseFactory)
|
||||
* @see #setDatabaseFactory(MongoDatabaseFactory)
|
||||
* @see #setTransactionSynchronization(int)
|
||||
*/
|
||||
public MongoTransactionManager() {}
|
||||
public MongoTransactionManager() {
|
||||
this.transactionOptionsResolver = MongoTransactionOptionsResolver.defaultResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDatabaseFactory}.
|
||||
*
|
||||
* @param dbFactory must not be {@literal null}.
|
||||
* @param databaseFactory must not be {@literal null}.
|
||||
*/
|
||||
public MongoTransactionManager(MongoDatabaseFactory dbFactory) {
|
||||
this(dbFactory, null);
|
||||
public MongoTransactionManager(MongoDatabaseFactory databaseFactory) {
|
||||
this(databaseFactory, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDatabaseFactory}
|
||||
* applying the given {@link TransactionOptions options}, if present, when starting a new transaction.
|
||||
*
|
||||
* @param dbFactory must not be {@literal null}.
|
||||
* @param databaseFactory must not be {@literal null}.
|
||||
* @param options can be {@literal null}.
|
||||
*/
|
||||
public MongoTransactionManager(MongoDatabaseFactory dbFactory, @Nullable TransactionOptions options) {
|
||||
this(dbFactory, MongoTransactionOptionsResolver.defaultResolver(), MongoTransactionOptions.of(options));
|
||||
public MongoTransactionManager(MongoDatabaseFactory databaseFactory, @Nullable TransactionOptions options) {
|
||||
this(databaseFactory, MongoTransactionOptionsResolver.defaultResolver(), MongoTransactionOptions.of(options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDatabaseFactory}
|
||||
* applying the given {@link TransactionOptions options}, if present, when starting a new transaction.
|
||||
*
|
||||
* @param dbFactory must not be {@literal null}.
|
||||
* @param transactionOptionsResolver
|
||||
* @param databaseFactory must not be {@literal null}.
|
||||
* @param transactionOptionsResolver must not be {@literal null}.
|
||||
* @param defaultTransactionOptions can be {@literal null}.
|
||||
* @since 4.3
|
||||
*/
|
||||
public MongoTransactionManager(MongoDatabaseFactory dbFactory, MongoTransactionOptionsResolver transactionOptionsResolver, MongoTransactionOptions defaultTransactionOptions) {
|
||||
public MongoTransactionManager(MongoDatabaseFactory databaseFactory,
|
||||
MongoTransactionOptionsResolver transactionOptionsResolver, MongoTransactionOptions defaultTransactionOptions) {
|
||||
|
||||
Assert.notNull(dbFactory, "DbFactory must not be null");
|
||||
Assert.notNull(databaseFactory, "MongoDatabaseFactory must not be null");
|
||||
Assert.notNull(transactionOptionsResolver, "MongoTransactionOptionsResolver must not be null");
|
||||
|
||||
this.dbFactory = dbFactory;
|
||||
this.databaseFactory = databaseFactory;
|
||||
this.transactionOptionsResolver = transactionOptionsResolver;
|
||||
this.options = defaultTransactionOptions;
|
||||
}
|
||||
@@ -278,12 +280,12 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
|
||||
/**
|
||||
* Set the {@link MongoDatabaseFactory} that this instance should manage transactions for.
|
||||
*
|
||||
* @param dbFactory must not be {@literal null}.
|
||||
* @param databaseFactory must not be {@literal null}.
|
||||
*/
|
||||
public void setDbFactory(MongoDatabaseFactory dbFactory) {
|
||||
public void setDatabaseFactory(MongoDatabaseFactory databaseFactory) {
|
||||
|
||||
Assert.notNull(dbFactory, "DbFactory must not be null");
|
||||
this.dbFactory = dbFactory;
|
||||
Assert.notNull(databaseFactory, "DbFactory must not be null");
|
||||
this.databaseFactory = databaseFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,8 +303,8 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
|
||||
* @return can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public MongoDatabaseFactory getDbFactory() {
|
||||
return dbFactory;
|
||||
public MongoDatabaseFactory getDatabaseFactory() {
|
||||
return databaseFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -326,14 +328,14 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalStateException if {@link #dbFactory} is {@literal null}.
|
||||
* @throws IllegalStateException if {@link #databaseFactory} is {@literal null}.
|
||||
*/
|
||||
private MongoDatabaseFactory getRequiredDbFactory() {
|
||||
|
||||
Assert.state(dbFactory != null,
|
||||
Assert.state(databaseFactory != null,
|
||||
"MongoTransactionManager operates upon a MongoDbFactory; Did you forget to provide one; It's required");
|
||||
|
||||
return dbFactory;
|
||||
return databaseFactory;
|
||||
}
|
||||
|
||||
private static MongoTransactionObject extractMongoTransaction(Object transaction) {
|
||||
|
||||
@@ -17,13 +17,13 @@ package org.springframework.data.mongodb;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.mongodb.core.ReadConcernAware;
|
||||
import org.springframework.data.mongodb.core.ReadPreferenceAware;
|
||||
import org.springframework.data.mongodb.core.WriteConcernAware;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.Function;
|
||||
import com.mongodb.ReadConcern;
|
||||
import com.mongodb.ReadPreference;
|
||||
import com.mongodb.TransactionOptions;
|
||||
@@ -115,24 +115,23 @@ public interface MongoTransactionOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the current options using the given mapping {@link Function}.
|
||||
* Apply the current options using the given mapping {@link Function} and return its result.
|
||||
*
|
||||
* @param mappingFunction
|
||||
* @return instance of T.
|
||||
* @param <T>
|
||||
* @return result of the mapping function.
|
||||
*/
|
||||
default <T> T as(Function<MongoTransactionOptions, T> mappingFunction) {
|
||||
default <T> T map(Function<MongoTransactionOptions, T> mappingFunction) {
|
||||
return mappingFunction.apply(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MongoDB driver native {@link TransactionOptions}.
|
||||
* @see MongoTransactionOptions#as(Function)
|
||||
* @see MongoTransactionOptions#map(Function)
|
||||
*/
|
||||
@Nullable
|
||||
default TransactionOptions toDriverOptions() {
|
||||
|
||||
return as(it -> {
|
||||
return map(it -> {
|
||||
|
||||
if (MongoTransactionOptions.NONE.equals(it)) {
|
||||
return null;
|
||||
@@ -157,7 +156,7 @@ public interface MongoTransactionOptions
|
||||
|
||||
/**
|
||||
* Factory method to wrap given MongoDB driver native {@link TransactionOptions} into {@link MongoTransactionOptions}.
|
||||
*
|
||||
*
|
||||
* @param options
|
||||
* @return {@link MongoTransactionOptions#NONE} if given object is {@literal null}.
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.springframework.util.StringUtils;
|
||||
* A {@link TransactionOptionResolver} reading MongoDB specific {@link MongoTransactionOptions transaction options} from
|
||||
* a {@link TransactionDefinition}. Implementations of {@link MongoTransactionOptions} may choose a specific
|
||||
* {@link #getLabelPrefix() prefix} for {@link TransactionAttribute#getLabels() transaction attribute labels} to avoid
|
||||
* evaluating non store specific ones.
|
||||
* evaluating non-store specific ones.
|
||||
* <p>
|
||||
* {@link TransactionAttribute#getLabels()} evaluated by default should follow the property style using {@code =} to
|
||||
* separate key and value pairs.
|
||||
@@ -50,11 +50,11 @@ public interface MongoTransactionOptionsResolver extends TransactionOptionResolv
|
||||
/**
|
||||
* Obtain the default {@link MongoTransactionOptionsResolver} implementation using a {@literal mongo:}
|
||||
* {@link #getLabelPrefix() prefix}.
|
||||
*
|
||||
*
|
||||
* @return instance of default {@link MongoTransactionOptionsResolver} implementation.
|
||||
*/
|
||||
static MongoTransactionOptionsResolver defaultResolver() {
|
||||
return DefaultMongoTransactionOptionsResolver.INSTANCE.get();
|
||||
return DefaultMongoTransactionOptionsResolver.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,16 +71,16 @@ public interface MongoTransactionOptionsResolver extends TransactionOptionResolv
|
||||
* <p>
|
||||
* Splits applicable labels property style using {@literal =} as deliminator and removes a potential
|
||||
* {@link #getLabelPrefix() prefix} before calling {@link #convert(Map)} with filtered label values.
|
||||
*
|
||||
* @param txDefinition
|
||||
*
|
||||
* @param definition
|
||||
* @return {@link MongoTransactionOptions#NONE} in case the given {@link TransactionDefinition} is not a
|
||||
* {@link TransactionAttribute} if no matching {@link TransactionAttribute#getLabels() labels} could be found.
|
||||
* @throws IllegalArgumentException for options that do not map to valid transactions options or malformatted labels.
|
||||
*/
|
||||
@Override
|
||||
default MongoTransactionOptions resolve(TransactionDefinition txDefinition) {
|
||||
default MongoTransactionOptions resolve(TransactionDefinition definition) {
|
||||
|
||||
if (!(txDefinition instanceof TransactionAttribute attribute)) {
|
||||
if (!(definition instanceof TransactionAttribute attribute)) {
|
||||
return MongoTransactionOptions.NONE;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,17 +37,14 @@ import com.mongodb.reactivestreams.client.ClientSession;
|
||||
/**
|
||||
* A {@link org.springframework.transaction.ReactiveTransactionManager} implementation that manages
|
||||
* {@link com.mongodb.reactivestreams.client.ClientSession} based transactions for a single
|
||||
* {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory}.
|
||||
* <br />
|
||||
* {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory}. <br />
|
||||
* Binds a {@link ClientSession} from the specified
|
||||
* {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory} to the subscriber
|
||||
* {@link reactor.util.context.Context}.
|
||||
* <br />
|
||||
* {@link reactor.util.context.Context}. <br />
|
||||
* {@link org.springframework.transaction.TransactionDefinition#isReadOnly() Readonly} transactions operate on a
|
||||
* {@link ClientSession} and enable causal consistency, and also {@link ClientSession#startTransaction() start},
|
||||
* {@link com.mongodb.reactivestreams.client.ClientSession#commitTransaction() commit} or
|
||||
* {@link ClientSession#abortTransaction() abort} a transaction.
|
||||
* <br />
|
||||
* {@link ClientSession#abortTransaction() abort} a transaction. <br />
|
||||
* Application code is required to retrieve the {@link com.mongodb.reactivestreams.client.MongoDatabase} via
|
||||
* {@link org.springframework.data.mongodb.ReactiveMongoDatabaseUtils#getDatabase(ReactiveMongoDatabaseFactory)} instead
|
||||
* of a standard {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getMongoDatabase()} call. Spring
|
||||
@@ -68,11 +65,10 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransaction
|
||||
|
||||
private @Nullable ReactiveMongoDatabaseFactory databaseFactory;
|
||||
private @Nullable MongoTransactionOptions options;
|
||||
private MongoTransactionOptionsResolver transactionOptionsResolver;
|
||||
private final MongoTransactionOptionsResolver transactionOptionsResolver;
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveMongoTransactionManager} for bean-style usage.
|
||||
* <br />
|
||||
* Create a new {@link ReactiveMongoTransactionManager} for bean-style usage. <br />
|
||||
* <strong>Note:</strong>The {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory db factory} has to
|
||||
* be {@link #setDatabaseFactory(ReactiveMongoDatabaseFactory)} set} before using the instance. Use this constructor
|
||||
* to prepare a {@link ReactiveMongoTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}.
|
||||
@@ -82,7 +78,9 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransaction
|
||||
*
|
||||
* @see #setDatabaseFactory(ReactiveMongoDatabaseFactory)
|
||||
*/
|
||||
public ReactiveMongoTransactionManager() {}
|
||||
public ReactiveMongoTransactionManager() {
|
||||
this.transactionOptionsResolver = MongoTransactionOptionsResolver.defaultResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveMongoTransactionManager} obtaining sessions from the given
|
||||
@@ -113,14 +111,16 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransaction
|
||||
* starting a new transaction.
|
||||
*
|
||||
* @param databaseFactory must not be {@literal null}.
|
||||
* @param transactionOptionsResolver
|
||||
* @param transactionOptionsResolver must not be {@literal null}.
|
||||
* @param defaultTransactionOptions can be {@literal null}.
|
||||
*
|
||||
* @since 4.3
|
||||
*/
|
||||
public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory, MongoTransactionOptionsResolver transactionOptionsResolver,
|
||||
public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory,
|
||||
MongoTransactionOptionsResolver transactionOptionsResolver,
|
||||
@Nullable MongoTransactionOptions defaultTransactionOptions) {
|
||||
|
||||
Assert.notNull(databaseFactory, "DatabaseFactory must not be null");
|
||||
Assert.notNull(transactionOptionsResolver, "MongoTransactionOptionsResolver must not be null");
|
||||
|
||||
this.databaseFactory = databaseFactory;
|
||||
this.transactionOptionsResolver = transactionOptionsResolver;
|
||||
@@ -163,7 +163,8 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransaction
|
||||
|
||||
}).doOnNext(resourceHolder -> {
|
||||
|
||||
MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition).mergeWith(options);
|
||||
MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition)
|
||||
.mergeWith(options);
|
||||
mongoTransactionObject.startTransaction(mongoTransactionOptions.toDriverOptions());
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
||||
@@ -82,6 +82,13 @@ class SimpleMongoTransactionOptions implements MongoTransactionOptions {
|
||||
return writeConcern;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return "DefaultMongoTransactionOptions{" + "maxCommitTime=" + maxCommitTime + ", readConcern=" + readConcern
|
||||
+ ", readPreference=" + readPreference + ", writeConcern=" + writeConcern + '}';
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Duration doGetMaxCommitTime(Map<String, String> options) {
|
||||
|
||||
@@ -123,13 +130,6 @@ class SimpleMongoTransactionOptions implements MongoTransactionOptions {
|
||||
return value != null ? convertFunction.apply(value) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return "DefaultMongoTransactionOptions{" + "maxCommitTime=" + maxCommitTime + ", readConcern=" + readConcern
|
||||
+ ", readPreference=" + readPreference + ", writeConcern=" + writeConcern + '}';
|
||||
}
|
||||
|
||||
enum OptionKey {
|
||||
|
||||
MAX_COMMIT_TIME("maxCommitTime"), READ_CONCERN("readConcern"), READ_PREFERENCE("readPreference"), WRITE_CONCERN(
|
||||
|
||||
@@ -20,14 +20,22 @@ import java.time.Duration;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* MongoDB-specific transaction metadata.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.3
|
||||
*/
|
||||
public interface TransactionMetadata {
|
||||
|
||||
/**
|
||||
* @return the maximum commit time. Can be {@literal null} if not configured.
|
||||
*/
|
||||
@Nullable
|
||||
Duration getMaxCommitTime();
|
||||
|
||||
/**
|
||||
* @return {@literal true} if the max commit time is configured; {@literal false} otherwise.
|
||||
*/
|
||||
default boolean hasMaxCommitTime() {
|
||||
return getMaxCommitTime() != null;
|
||||
}
|
||||
|
||||
@@ -17,13 +17,22 @@ package org.springframework.data.mongodb;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.interceptor.TransactionAttribute;
|
||||
|
||||
/**
|
||||
* Interface that defines a resolver for {@link TransactionMetadata} based on a {@link TransactionDefinition}.
|
||||
* Transaction metadata is used to enrich the MongoDB transaction with additional information.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.3
|
||||
*/
|
||||
interface TransactionOptionResolver<T extends TransactionMetadata> {
|
||||
|
||||
/**
|
||||
* Resolves the transaction metadata from a given {@link TransactionDefinition}.
|
||||
*
|
||||
* @param definition the {@link TransactionDefinition}.
|
||||
* @return the resolved {@link TransactionMetadata} or {@literal null} if the resolver cannot resolve any metadata.
|
||||
*/
|
||||
@Nullable
|
||||
T resolve(TransactionDefinition attribute);
|
||||
T resolve(TransactionDefinition definition);
|
||||
}
|
||||
|
||||
@@ -17,25 +17,26 @@ package org.springframework.data.mongodb.core;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.ReadPreference;
|
||||
import com.mongodb.WriteConcern;
|
||||
|
||||
/**
|
||||
* Interface indicating a component that contains and exposes an {@link WriteConcern}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.3
|
||||
*/
|
||||
public interface WriteConcernAware {
|
||||
|
||||
/**
|
||||
* @return the {@link WriteConcern} to apply or {@literal null} if none set.
|
||||
*/
|
||||
@Nullable
|
||||
WriteConcern getWriteConcern();
|
||||
|
||||
/**
|
||||
* @return {@literal true} if a {@link com.mongodb.WriteConcern} is set.
|
||||
*/
|
||||
default boolean hasWriteConcern() {
|
||||
return getWriteConcern() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link ReadPreference} to apply or {@literal null} if none set.
|
||||
*/
|
||||
@Nullable
|
||||
WriteConcern getWriteConcern();
|
||||
}
|
||||
|
||||
@@ -31,19 +31,22 @@ import com.mongodb.ReadPreference;
|
||||
import com.mongodb.WriteConcern;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DefaultMongoTransactionOptionsResolver}.
|
||||
*
|
||||
* @author Yan Kardziyaka
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class SimpleMongoTransactionOptionsResolverUnitTests {
|
||||
class DefaultMongoTransactionOptionsResolverUnitTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "mongo:maxCommitTime=-PT5S", "mongo:readConcern=invalidValue",
|
||||
"mongo:readPreference=invalidValue", "mongo:writeConcern=invalidValue", "mongo:invalidPreference=jedi", "mongo:readConcern", "mongo:readConcern:local", "mongo:readConcern=" })
|
||||
"mongo:readPreference=invalidValue", "mongo:writeConcern=invalidValue", "mongo:invalidPreference=jedi",
|
||||
"mongo:readConcern", "mongo:readConcern:local", "mongo:readConcern=" })
|
||||
void shouldThrowExceptionOnInvalidAttribute(String label) {
|
||||
|
||||
TransactionAttribute attribute = transactionAttribute(label);
|
||||
|
||||
assertThatThrownBy(() -> DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute)) //
|
||||
assertThatThrownBy(() -> DefaultMongoTransactionOptionsResolver.INSTANCE.resolve(attribute)) //
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@@ -51,7 +54,7 @@ class SimpleMongoTransactionOptionsResolverUnitTests {
|
||||
public void shouldReturnEmptyOptionsIfNotTransactionAttribute() {
|
||||
|
||||
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(definition))
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.resolve(definition))
|
||||
.isSameAs(MongoTransactionOptions.NONE);
|
||||
}
|
||||
|
||||
@@ -60,7 +63,7 @@ class SimpleMongoTransactionOptionsResolverUnitTests {
|
||||
|
||||
TransactionAttribute attribute = new DefaultTransactionAttribute();
|
||||
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute))
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.resolve(attribute))
|
||||
.isSameAs(MongoTransactionOptions.NONE);
|
||||
}
|
||||
|
||||
@@ -69,7 +72,7 @@ class SimpleMongoTransactionOptionsResolverUnitTests {
|
||||
|
||||
TransactionAttribute attribute = transactionAttribute("jpa:ignore");
|
||||
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute))
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.resolve(attribute))
|
||||
.isSameAs(MongoTransactionOptions.NONE);
|
||||
}
|
||||
|
||||
@@ -78,7 +81,7 @@ class SimpleMongoTransactionOptionsResolverUnitTests {
|
||||
|
||||
TransactionAttribute attribute = transactionAttribute("mongo:maxCommitTime=PT5S");
|
||||
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute))
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.resolve(attribute))
|
||||
.returns(5L, from(options -> options.getMaxCommitTime().toSeconds())) //
|
||||
.returns(null, from(MongoTransactionOptions::getReadConcern)) //
|
||||
.returns(null, from(MongoTransactionOptions::getReadPreference)) //
|
||||
@@ -90,7 +93,7 @@ class SimpleMongoTransactionOptionsResolverUnitTests {
|
||||
|
||||
TransactionAttribute attribute = transactionAttribute("mongo:readConcern=majority");
|
||||
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute))
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.resolve(attribute))
|
||||
.returns(null, from(TransactionMetadata::getMaxCommitTime)) //
|
||||
.returns(ReadConcern.MAJORITY, from(MongoTransactionOptions::getReadConcern)) //
|
||||
.returns(null, from(MongoTransactionOptions::getReadPreference)) //
|
||||
@@ -102,7 +105,7 @@ class SimpleMongoTransactionOptionsResolverUnitTests {
|
||||
|
||||
TransactionAttribute attribute = transactionAttribute("mongo:readPreference=primaryPreferred");
|
||||
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute))
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.resolve(attribute))
|
||||
.returns(null, from(TransactionMetadata::getMaxCommitTime)) //
|
||||
.returns(null, from(MongoTransactionOptions::getReadConcern)) //
|
||||
.returns(ReadPreference.primaryPreferred(), from(MongoTransactionOptions::getReadPreference)) //
|
||||
@@ -114,7 +117,7 @@ class SimpleMongoTransactionOptionsResolverUnitTests {
|
||||
|
||||
TransactionAttribute attribute = transactionAttribute("mongo:writeConcern=w3");
|
||||
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute))
|
||||
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.resolve(attribute))
|
||||
.returns(null, from(TransactionMetadata::getMaxCommitTime)) //
|
||||
.returns(null, from(MongoTransactionOptions::getReadConcern)) //
|
||||
.returns(null, from(MongoTransactionOptions::getReadPreference)) //
|
||||
@@ -29,6 +29,8 @@ import com.mongodb.TransactionOptions;
|
||||
import com.mongodb.WriteConcern;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MongoTransactionOptions}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class MongoTransactionOptionsUnitTests {
|
||||
|
||||
@@ -12,7 +12,9 @@ Both `MongoOperations` and `ReactiveMongoOperations` provide gateway methods for
|
||||
`MongoCollection` and `MongoDatabase` use session proxy objects that implement MongoDB's collection and database interfaces, so you need not add a session on each call.
|
||||
This means that a potential call to `MongoCollection#find()` is delegated to `MongoCollection#find(ClientSession)`.
|
||||
|
||||
NOTE: Methods such as `(Reactive)MongoOperations#getCollection` return native MongoDB Java Driver gateway objects (such as `MongoCollection`) that themselves offer dedicated methods for `ClientSession`. These methods are *NOT* session-proxied. You should provide the `ClientSession` where needed when interacting directly with a `MongoCollection` or `MongoDatabase` and not through one of the `#execute` callbacks on `MongoOperations`.
|
||||
NOTE: Methods such as `(Reactive)MongoOperations#getCollection` return native MongoDB Java Driver gateway objects (such as `MongoCollection`) that themselves offer dedicated methods for `ClientSession`.
|
||||
These methods are *NOT* session-proxied.
|
||||
You should provide the `ClientSession` where needed when interacting directly with a `MongoCollection` or `MongoDatabase` and not through one of the `#execute` callbacks on `MongoOperations`.
|
||||
|
||||
[[mongo.sessions.sync]]
|
||||
[[mongo.sessions.reactive]]
|
||||
@@ -49,12 +51,15 @@ template.withSession(() -> session)
|
||||
|
||||
session.close() <4>
|
||||
----
|
||||
|
||||
<1> Obtain a new session from the server.
|
||||
<2> Use `MongoOperation` methods as before. The `ClientSession` gets applied automatically.
|
||||
<2> Use `MongoOperation` methods as before.
|
||||
The `ClientSession` gets applied automatically.
|
||||
<3> Make sure to close the `ClientSession`.
|
||||
<4> Close the session.
|
||||
|
||||
WARNING: When dealing with `DBRef` instances, especially lazily loaded ones, it is essential to *not* close the `ClientSession` before all data is loaded. Otherwise, lazy fetch fails.
|
||||
WARNING: When dealing with `DBRef` instances, especially lazily loaded ones, it is essential to *not* close the `ClientSession` before all data is loaded.
|
||||
Otherwise, lazy fetch fails.
|
||||
====
|
||||
|
||||
Reactive::
|
||||
@@ -83,25 +88,32 @@ template.withSession(session)
|
||||
}, ClientSession::close) <3>
|
||||
.subscribe(); <4>
|
||||
----
|
||||
|
||||
<1> Obtain a `Publisher` for new session retrieval.
|
||||
<2> Use `ReactiveMongoOperation` methods as before. The `ClientSession` is obtained and applied automatically.
|
||||
<2> Use `ReactiveMongoOperation` methods as before.
|
||||
The `ClientSession` is obtained and applied automatically.
|
||||
<3> Make sure to close the `ClientSession`.
|
||||
<4> Nothing happens until you subscribe. See https://projectreactor.io/docs/core/release/reference/#reactive.subscribe[the Project Reactor Reference Guide] for details.
|
||||
<4> Nothing happens until you subscribe.
|
||||
See https://projectreactor.io/docs/core/release/reference/#reactive.subscribe[the Project Reactor Reference Guide] for details.
|
||||
|
||||
By using a `Publisher` that provides the actual session, you can defer session acquisition to the point of actual subscription.
|
||||
Still, you need to close the session when done, so as to not pollute the server with stale sessions. Use the `doFinally` hook on `execute` to call `ClientSession#close()` when you no longer need the session.
|
||||
Still, you need to close the session when done, so as to not pollute the server with stale sessions.
|
||||
Use the `doFinally` hook on `execute` to call `ClientSession#close()` when you no longer need the session.
|
||||
If you prefer having more control over the session itself, you can obtain the `ClientSession` through the driver and provide it through a `Supplier`.
|
||||
|
||||
NOTE: Reactive use of `ClientSession` is limited to Template API usage. There's currently no session integration with reactive repositories.
|
||||
NOTE: Reactive use of `ClientSession` is limited to Template API usage.
|
||||
There's currently no session integration with reactive repositories.
|
||||
====
|
||||
======
|
||||
|
||||
[[mongo.transactions]]
|
||||
== MongoDB Transactions
|
||||
|
||||
As of version 4, MongoDB supports https://www.mongodb.com/transactions[Transactions]. Transactions are built on top of xref:mongodb/client-session-transactions.adoc[Sessions] and, consequently, require an active `ClientSession`.
|
||||
As of version 4, MongoDB supports https://www.mongodb.com/transactions[Transactions].
|
||||
Transactions are built on top of xref:mongodb/client-session-transactions.adoc[Sessions] and, consequently, require an active `ClientSession`.
|
||||
|
||||
NOTE: Unless you specify a `MongoTransactionManager` within your application context, transaction support is *DISABLED*. You can use `setSessionSynchronization(ALWAYS)` to participate in ongoing non-native MongoDB transactions.
|
||||
NOTE: Unless you specify a `MongoTransactionManager` within your application context, transaction support is *DISABLED*.
|
||||
You can use `setSessionSynchronization(ALWAYS)` to participate in ongoing non-native MongoDB transactions.
|
||||
|
||||
To get full programmatic control over transactions, you may want to use the session callback on `MongoOperations`.
|
||||
|
||||
@@ -138,6 +150,7 @@ template.withSession(session)
|
||||
}
|
||||
}, ClientSession::close) <5>
|
||||
----
|
||||
|
||||
<1> Obtain a new `ClientSession`.
|
||||
<2> Start the transaction.
|
||||
<3> If everything works out as expected, commit the changes.
|
||||
@@ -168,17 +181,19 @@ Mono<DeleteResult> result = Mono
|
||||
.doFinally(signal -> session.close()); <6>
|
||||
});
|
||||
----
|
||||
|
||||
<1> First we obviously need to initiate the session.
|
||||
<2> Once we have the `ClientSession` at hand, start the transaction.
|
||||
<3> Operate within the transaction by passing on the `ClientSession` to the operation.
|
||||
<4> If the operations completes exceptionally, we need to stop the transaction and preserve the error.
|
||||
<5> Or of course, commit the changes in case of success. Still preserving the operations result.
|
||||
<5> Or of course, commit the changes in case of success.
|
||||
Still preserving the operations result.
|
||||
<6> Lastly, we need to make sure to close the session.
|
||||
|
||||
The culprit of the above operation is in keeping the main flows `DeleteResult` instead of the transaction outcome
|
||||
published via either `commitTransaction()` or `abortTransaction()`, which leads to a rather complicated setup.
|
||||
The culprit of the above operation is in keeping the main flows `DeleteResult` instead of the transaction outcome published via either `commitTransaction()` or `abortTransaction()`, which leads to a rather complicated setup.
|
||||
|
||||
NOTE: Unless you specify a `ReactiveMongoTransactionManager` within your application context, transaction support is *DISABLED*. You can use `setSessionSynchronization(ALWAYS)` to participate in ongoing non-native MongoDB transactions.
|
||||
NOTE: Unless you specify a `ReactiveMongoTransactionManager` within your application context, transaction support is *DISABLED*.
|
||||
You can use `setSessionSynchronization(ALWAYS)` to participate in ongoing non-native MongoDB transactions.
|
||||
====
|
||||
======
|
||||
|
||||
@@ -213,9 +228,10 @@ txTemplate.execute(new TransactionCallbackWithoutResult() {
|
||||
process(step);
|
||||
|
||||
template.update(Step.class).apply(Update.set("state", // ...
|
||||
};
|
||||
}
|
||||
});
|
||||
----
|
||||
|
||||
<1> Enable transaction synchronization during Template API configuration.
|
||||
<2> Create the `TransactionTemplate` using the provided `PlatformTransactionManager`.
|
||||
<3> Within the callback the `ClientSession` and transaction are already registered.
|
||||
@@ -244,6 +260,7 @@ Mono<Void> process(step)
|
||||
.as(rxtx::transactional) <3>
|
||||
.then();
|
||||
----
|
||||
|
||||
<1> Enable transaction synchronization for Transactional participation.
|
||||
<2> Create the `TransactionalOperator` using the provided `ReactiveTransactionManager`.
|
||||
<3> `TransactionalOperator.transactional(…)` provides transaction management for all upstream operations.
|
||||
@@ -258,7 +275,8 @@ Mono<Void> process(step)
|
||||
It lets applications use link:{springDocsUrl}/data-access.html#transaction[the managed transaction features of Spring].
|
||||
The `MongoTransactionManager` binds a `ClientSession` to the thread whereas the `ReactiveMongoTransactionManager` is using the `ReactorContext` for this.
|
||||
`MongoTemplate` detects the session and operates on these resources which are associated with the transaction accordingly.
|
||||
`MongoTemplate` can also participate in other, ongoing transactions. The following example shows how to create and use transactions with a `MongoTransactionManager`:
|
||||
`MongoTemplate` can also participate in other, ongoing transactions.
|
||||
The following example shows how to create and use transactions with a `MongoTransactionManager`:
|
||||
|
||||
.Transactions with `MongoTransactionManager` / `ReactiveMongoTransactionManager`
|
||||
[tabs]
|
||||
@@ -294,6 +312,7 @@ public class StateService {
|
||||
});
|
||||
|
||||
----
|
||||
|
||||
<1> Register `MongoTransactionManager` in the application context.
|
||||
<2> Mark methods as transactional.
|
||||
|
||||
@@ -330,6 +349,7 @@ public class StateService {
|
||||
});
|
||||
|
||||
----
|
||||
|
||||
<1> Register `ReactiveMongoTransactionManager` in the application context.
|
||||
<2> Mark methods as transactional.
|
||||
|
||||
@@ -337,6 +357,50 @@ NOTE: `@Transactional(readOnly = true)` advises `ReactiveMongoTransactionManager
|
||||
====
|
||||
======
|
||||
|
||||
[[mongo.transaction.options]]
|
||||
=== Controlling MongoDB-specific Transaction Options
|
||||
|
||||
Transactional service methods can require specific transaction options to run a transaction.
|
||||
Spring Data MongoDB's transaction managers support evaluation of transaction labels such as `@Transactional(label = { "mongo:readConcern=available" })`.
|
||||
|
||||
By default, the label namespace using the `mongo:` prefix is evaluated by `MongoTransactionOptionsResolver` that is configured by default.
|
||||
Transaction labels are provided by `TransactionAttribute` and available to programmatic transaction control through `TransactionTemplate` and `TransactionalOperator`.
|
||||
Due to their declarative nature, `@Transactional(label = …)` provides a good starting point that also can serve as documentation.
|
||||
|
||||
Currently, the following options are supported:
|
||||
|
||||
Max Commit Time::
|
||||
|
||||
Controls the maximum execution time on the server for the commitTransaction operation.
|
||||
The format of the value corresponds with ISO-8601 duration format as used with `Duration.parse(…)`.
|
||||
+
|
||||
Usage:
|
||||
`mongo:maxCommitTime=PT1S`
|
||||
|
||||
Read Concern::
|
||||
|
||||
Sets the read concern for the transaction.
|
||||
+
|
||||
Usage:
|
||||
`mongo:readConcern=LOCAL|MAJORITY|LINEARIZABLE|SNAPSHOT|AVAILABLE`
|
||||
|
||||
Read Preference::
|
||||
|
||||
Sets the read preference for the transaction.
|
||||
+
|
||||
Usage:
|
||||
`mongo:readPreference=PRIMARY|SECONDARY|SECONDARY_PREFERRED|PRIMARY_PREFERRED|NEAREST`
|
||||
|
||||
Write Concern::
|
||||
|
||||
Sets the write concern for the transaction.
|
||||
+
|
||||
Usage:
|
||||
`mongo:writeConcern=ACKNOWLEDGED|W1|W2|W3|UNACKNOWLEDGED|JOURNALED|MAJORITY`
|
||||
|
||||
NOTE: Nested transactions that join the outer transaction do not affect the initial transaction options as the transaction is already started.
|
||||
Transaction options are only applied when a new transaction is started.
|
||||
|
||||
[[mongo.transactions.behavior]]
|
||||
== Special behavior inside transactions
|
||||
|
||||
@@ -344,39 +408,42 @@ Inside transactions, MongoDB server has a slightly different behavior.
|
||||
|
||||
*Connection Settings*
|
||||
|
||||
The MongoDB drivers offer a dedicated replica set name configuration option turing the driver into auto detection
|
||||
mode. This option helps identifying the primary replica set nodes and command routing during a transaction.
|
||||
The MongoDB drivers offer a dedicated replica set name configuration option turing the driver into auto-detection mode.
|
||||
This option helps identify the primary replica set nodes and command routing during a transaction.
|
||||
|
||||
NOTE: Make sure to add `replicaSet` to the MongoDB URI. Please refer to https://docs.mongodb.com/manual/reference/connection-string/#connections-connection-options[connection string options] for further details.
|
||||
NOTE: Make sure to add `replicaSet` to the MongoDB URI.
|
||||
Please refer to https://docs.mongodb.com/manual/reference/connection-string/#connections-connection-options[connection string options] for further details.
|
||||
|
||||
*Collection Operations*
|
||||
|
||||
MongoDB does *not* support collection operations, such as collection creation, within a transaction. This also
|
||||
affects the on the fly collection creation that happens on first usage. Therefore make sure to have all required
|
||||
structures in place.
|
||||
MongoDB does *not* support collection operations, such as collection creation, within a transaction.
|
||||
This also affects the on the fly collection creation that happens on first usage.
|
||||
Therefore make sure to have all required structures in place.
|
||||
|
||||
*Transient Errors*
|
||||
|
||||
MongoDB can add special labels to errors raised during transactional operations. Those may indicate transient failures
|
||||
that might vanish by merely retrying the operation.
|
||||
We highly recommend https://github.com/spring-projects/spring-retry[Spring Retry] for those purposes. Nevertheless
|
||||
one may override `MongoTransactionManager#doCommit(MongoTransactionObject)` to implement a https://docs.mongodb.com/manual/core/transactions/#retry-commit-operation[Retry Commit Operation]
|
||||
MongoDB can add special labels to errors raised during transactional operations.
|
||||
Those may indicate transient failures that might vanish by merely retrying the operation.
|
||||
We highly recommend https://github.com/spring-projects/spring-retry[Spring Retry] for those purposes.
|
||||
Nevertheless one may override `MongoTransactionManager#doCommit(MongoTransactionObject)` to implement a https://docs.mongodb.com/manual/core/transactions/#retry-commit-operation[Retry Commit Operation]
|
||||
behavior as outlined in the MongoDB reference manual.
|
||||
|
||||
*Count*
|
||||
|
||||
MongoDB `count` operates upon collection statistics which may not reflect the actual situation within a transaction.
|
||||
The server responds with _error 50851_ when issuing a `count` command inside of a multi-document transaction.
|
||||
Once `MongoTemplate` detects an active transaction, all exposed `count()` methods are converted and delegated to the
|
||||
aggregation framework using `$match` and `$count` operators, preserving `Query` settings, such as `collation`.
|
||||
Once `MongoTemplate` detects an active transaction, all exposed `count()` methods are converted and delegated to the aggregation framework using `$match` and `$count` operators, preserving `Query` settings, such as `collation`.
|
||||
|
||||
Restrictions apply when using geo commands inside of the aggregation count helper. The following operators cannot be used and must be replaced with a different operator:
|
||||
Restrictions apply when using geo commands inside of the aggregation count helper.
|
||||
The following operators cannot be used and must be replaced with a different operator:
|
||||
|
||||
* `$where` -> `$expr`
|
||||
* `$near` -> `$geoWithin` with `$center`
|
||||
* `$nearSphere` -> `$geoWithin` with `$centerSphere`
|
||||
|
||||
Queries using `Criteria.near(…)` and `Criteria.nearSphere(…)` must be rewritten to `Criteria.within(…)` respective `Criteria.withinSphere(…)`. Same applies for the `near` query keyword in repository query methods that must be changed to `within`. See also MongoDB JIRA ticket https://jira.mongodb.org/browse/DRIVERS-518[DRIVERS-518] for further reference.
|
||||
Queries using `Criteria.near(…)` and `Criteria.nearSphere(…)` must be rewritten to `Criteria.within(…)` respective `Criteria.withinSphere(…)`.
|
||||
Same applies for the `near` query keyword in repository query methods that must be changed to `within`.
|
||||
See also MongoDB JIRA ticket https://jira.mongodb.org/browse/DRIVERS-518[DRIVERS-518] for further reference.
|
||||
|
||||
The following snippet shows `count` usage inside the session-bound closure:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user