Browse Source

Polishing.

Tweak naming. Add Javadoc and documentation.

See #1628
Original pull request: #4552
pull/4666/head
Mark Paluch 2 years ago
parent
commit
77a205c229
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 9
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
  2. 60
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java
  3. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
  4. 14
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
  5. 29
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java
  6. 14
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java
  7. 8
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java
  8. 13
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java
  9. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java
  10. 23
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolverUnitTests.java
  11. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java
  12. 125
      src/main/antora/modules/ROOT/pages/mongodb/client-session-transactions.adoc

9
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java

@ -18,7 +18,6 @@ package org.springframework.data.mongodb; @@ -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; @@ -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 @@ -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));

60
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java

@ -64,59 +64,61 @@ import com.mongodb.client.ClientSession; @@ -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 @@ -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 @@ -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 @@ -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) {

15
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java

@ -17,13 +17,13 @@ package org.springframework.data.mongodb; @@ -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 @@ -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 @@ -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}.
*/

14
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java

@ -28,7 +28,7 @@ import org.springframework.util.StringUtils; @@ -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 @@ -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 @@ -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;
}

29
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java

@ -37,17 +37,14 @@ import com.mongodb.reactivestreams.client.ClientSession; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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()) {

14
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java

@ -82,6 +82,13 @@ class SimpleMongoTransactionOptions implements MongoTransactionOptions { @@ -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 { @@ -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(

8
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java

@ -20,14 +20,22 @@ import java.time.Duration; @@ -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;
}

13
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java

@ -17,13 +17,22 @@ package org.springframework.data.mongodb; @@ -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);
}

15
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java

@ -17,25 +17,26 @@ package org.springframework.data.mongodb.core; @@ -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();
}

23
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SimpleMongoTransactionOptionsResolverUnitTests.java → spring-data-mongodb/src/test/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolverUnitTests.java

@ -31,19 +31,22 @@ import com.mongodb.ReadPreference; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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)) //

2
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java

@ -29,6 +29,8 @@ import com.mongodb.TransactionOptions; @@ -29,6 +29,8 @@ import com.mongodb.TransactionOptions;
import com.mongodb.WriteConcern;
/**
* Unit tests for {@link MongoTransactionOptions}.
*
* @author Christoph Strobl
*/
class MongoTransactionOptionsUnitTests {

125
src/main/antora/modules/ROOT/pages/mongodb/client-session-transactions.adoc

@ -12,7 +12,9 @@ Both `MongoOperations` and `ReactiveMongoOperations` provide gateway methods for @@ -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) @@ -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) @@ -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) @@ -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 @@ -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() { @@ -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) @@ -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) @@ -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 { @@ -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 { @@ -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 @@ -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. @@ -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:

Loading…
Cancel
Save