From 77a205c229c97cdd4d36080c0faa662c2297d7c4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 12 Mar 2024 10:41:48 +0100 Subject: [PATCH] Polishing. Tweak naming. Add Javadoc and documentation. See #1628 Original pull request: #4552 --- ...efaultMongoTransactionOptionsResolver.java | 9 +- .../data/mongodb/MongoTransactionManager.java | 60 +++++---- .../data/mongodb/MongoTransactionOptions.java | 15 +-- .../MongoTransactionOptionsResolver.java | 14 +- .../ReactiveMongoTransactionManager.java | 29 ++-- .../SimpleMongoTransactionOptions.java | 14 +- .../data/mongodb/TransactionMetadata.java | 8 ++ .../mongodb/TransactionOptionResolver.java | 13 +- .../data/mongodb/core/WriteConcernAware.java | 15 ++- ...oTransactionOptionsResolverUnitTests.java} | 23 ++-- .../MongoTransactionOptionsUnitTests.java | 2 + .../mongodb/client-session-transactions.adoc | 125 ++++++++++++++---- 12 files changed, 208 insertions(+), 119 deletions(-) rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/{SimpleMongoTransactionOptionsResolverUnitTests.java => DefaultMongoTransactionOptionsResolverUnitTests.java} (83%) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java index 02447ff0e..348cc0028 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java @@ -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 INSTANCE = Lazy.of(DefaultMongoTransactionOptionsResolver::new); + INSTANCE; private static final String PREFIX = "mongo:"; - private DefaultMongoTransactionOptionsResolver() {} - @Override public MongoTransactionOptions convert(Map options) { @@ -53,7 +50,7 @@ class DefaultMongoTransactionOptionsResolver implements MongoTransactionOptionsR private static void validateKeys(Set 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)); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java index 895297b3f..35dd8ad45 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java @@ -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. - *
+ * Create a new {@link MongoTransactionManager} for bean-style usage.
* Note: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}. - *
+ * {@link #setDatabaseFactory(MongoDatabaseFactory) set} before using the instance. Use this constructor to prepare a + * {@link MongoTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}.
* 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) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java index 4c9957b6e..80c3feb47 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java @@ -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 + * @return result of the mapping function. */ - default T as(Function mappingFunction) { + default T map(Function 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}. */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java index 78ffc9774..86f8790b4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java @@ -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. *

* {@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 *

* 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; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java index 3907acbb7..bf4ca29f5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java @@ -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}. - *
+ * {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory}.
* Binds a {@link ClientSession} from the specified * {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory} to the subscriber - * {@link reactor.util.context.Context}. - *
+ * {@link reactor.util.context.Context}.
* {@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. - *
+ * {@link ClientSession#abortTransaction() abort} a transaction.
* 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. - *
+ * Create a new {@link ReactiveMongoTransactionManager} for bean-style usage.
* Note: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()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java index 9cd2146ce..f9e1d874f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java @@ -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 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( diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java index fd01d180a..bd06bdac5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java @@ -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; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java index fc8432690..6ab6902b9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java @@ -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 { + /** + * 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); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java index 18b2e4d4f..ed6078b8b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java @@ -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(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SimpleMongoTransactionOptionsResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolverUnitTests.java similarity index 83% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SimpleMongoTransactionOptionsResolverUnitTests.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolverUnitTests.java index cfe18a579..d833208b3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SimpleMongoTransactionOptionsResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolverUnitTests.java @@ -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)) // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java index 688bfbbe4..9836a704b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java @@ -29,6 +29,8 @@ import com.mongodb.TransactionOptions; import com.mongodb.WriteConcern; /** + * Unit tests for {@link MongoTransactionOptions}. + * * @author Christoph Strobl */ class MongoTransactionOptionsUnitTests { diff --git a/src/main/antora/modules/ROOT/pages/mongodb/client-session-transactions.adoc b/src/main/antora/modules/ROOT/pages/mongodb/client-session-transactions.adoc index f623067c7..f825690d7 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/client-session-transactions.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/client-session-transactions.adoc @@ -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 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 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 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: