From 4cd29350877c54e3002a60e7171e0fb96a96e7b1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 3 Apr 2018 14:11:30 +0200 Subject: [PATCH] DATAMONGO-1920 - Add support for MongoDB 4.0 transactions (synchronous driver). MongoTransactionManager is the gateway to the well known Spring transaction support. It allows applications to use managed transaction features of Spring. The MongoTransactionManager binds a ClientSession to the thread. MongoTemplate automatically detects those and operates on them accordingly. static class Config extends AbstractMongoConfiguration { // ... @Bean MongoTransactionManager transactionManager(MongoDbFactory dbFactory) { return new MongoTransactionManager(dbFactory); } } @Component public class StateService { @Transactional void someBusinessFunction(Step step) { template.insert(step); process(step); template.update(Step.class).apply(update.set("state", // ... }; }); Original pull request: #554. --- README.md | 36 ++ pom.xml | 4 +- spring-data-mongodb/pom.xml | 7 + .../data/mongodb/MongoDatabaseUtils.java | 233 +++++++++ .../data/mongodb/MongoDbFactory.java | 4 +- .../data/mongodb/MongoResourceHolder.java | 122 +++++ .../data/mongodb/MongoSessionProvider.java | 41 ++ .../data/mongodb/MongoTransactionManager.java | 461 ++++++++++++++++++ .../mongodb/ReactiveMongoDatabaseFactory.java | 2 +- .../SessionAwareMethodInterceptor.java | 18 +- .../data/mongodb/SessionSynchronization.java | 37 ++ .../config/AbstractMongoConfiguration.java | 4 +- .../core/ExecutableRemoveOperation.java | 7 + .../ExecutableRemoveOperationSupport.java | 12 +- .../data/mongodb/core/MongoOperations.java | 2 +- .../data/mongodb/core/MongoTemplate.java | 76 +-- .../mongodb/core/ReactiveMongoTemplate.java | 12 +- .../data/mongodb/core/SessionCallback.java | 1 + .../data/mongodb/core/SessionScoped.java | 2 +- .../mongodb/core/SimpleMongoDbFactory.java | 10 +- .../SimpleReactiveMongoDatabaseFactory.java | 2 +- .../mongodb/MongoDatabaseUtilsUnitTests.java | 239 +++++++++ .../MongoTransactionManagerUnitTests.java | 333 +++++++++++++ ...essionAwareMethodInterceptorUnitTests.java | 14 +- .../data/mongodb/core/ClientSessionTests.java | 87 +++- .../core/MongoTemplateTransactionTests.java | 183 +++++++ .../mongodb/core/MongoTemplateUnitTests.java | 5 +- .../core/ReactiveMongoTemplateUnitTests.java | 5 +- .../core/SessionBoundMongoTemplateTests.java | 2 +- .../SessionBoundMongoTemplateUnitTests.java | 2 +- .../core/SimpleMongoDbFactoryUnitTests.java | 2 +- .../PersonRepositoryTransactionalTests.java | 192 ++++++++ .../test/util/AfterTransactionAssertion.java | 47 ++ .../mongodb/test/util/MongoTestUtils.java | 72 +++ .../mongodb/test/util/MongoVersionRule.java | 14 +- src/main/asciidoc/index.adoc | 2 +- src/main/asciidoc/new-features.adoc | 3 +- .../client-session-transactions.adoc | 200 ++++++++ .../asciidoc/reference/client-session.adoc | 79 --- 39 files changed, 2411 insertions(+), 163 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoSessionProvider.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionSynchronization.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoDatabaseUtilsUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionManagerUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTransactionTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/AfterTransactionAssertion.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java create mode 100644 src/main/asciidoc/reference/client-session-transactions.adoc delete mode 100644 src/main/asciidoc/reference/client-session.adoc diff --git a/README.md b/README.md index 1425c0827..c7b64f93f 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,42 @@ public class MyService { } ``` +### MongoDB 4.0 Transactions + +As of version 4 MongoDB supports [Transactions](https://www.mongodb.com/transactions). Transactions are built on top of + `ClientSessions` and therefore require an active session. + +`MongoTransactionManager` is the gateway to the well known Spring transaction support. It allows applications to use +[managed transaction features of Spring](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html). +The `MongoTransactionManager` binds a `ClientSession` to the thread. `MongoTemplate` automatically detects those and operates on them accordingly. + +```java +@Configuration +static class Config extends AbstractMongoConfiguration { + + @Bean + MongoTransactionManager transactionManager(MongoDbFactory dbFactory) { + return new MongoTransactionManager(dbFactory); + } + + // ... +} + +@Component +public class StateService { + + @Transactional + void someBusinessFunction(Step step) { + + template.insert(step); + + process(step); + + template.update(Step.class).apply(Update.set("state", // ... + }; +}); +``` + ## Contributing to Spring Data Here are some ways for you to get involved in the community: diff --git a/pom.xml b/pom.xml index 6ccfbc2f3..f87fb5d28 100644 --- a/pom.xml +++ b/pom.xml @@ -28,8 +28,8 @@ multi spring-data-mongodb 2.1.0.BUILD-SNAPSHOT - 3.6.3 - 1.7.1 + 3.8.0-beta1 + 1.8.0 1.19 diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 274ca0484..b86dc2808 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -253,6 +253,13 @@ test + + javax.transaction + jta + 1.1 + test + + org.jetbrains.kotlin diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java new file mode 100644 index 000000000..37155a314 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java @@ -0,0 +1,233 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb; + +import com.mongodb.ReadPreference; +import com.mongodb.TransactionOptions; +import com.mongodb.WriteConcern; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.mongodb.ClientSessionOptions; +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; + +/** + * Helper class for managing a {@link MongoDatabase} instances via {@link MongoDbFactory}. Used for obtaining + * {@link ClientSession session bound} resources, such as {@link MongoDatabase} and + * {@link com.mongodb.client.MongoCollection} suitable for transactional usage. + *

+ * Note: Intended for internal usage only. + * + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + * @since 2.1 + */ +public class MongoDatabaseUtils { + + /** + * Obtain the default {@link MongoDatabase database} form the given {@link MongoDbFactory factory} using + * {@link SessionSynchronization#NATIVE native session synchronization}. + *

+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current + * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() snychronization is active}. + * + * @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from. + * @return must not be {@literal null}. + */ + public static MongoDatabase getDatabase(MongoDbFactory factory) { + return doGetMongoDatabase(null, factory, SessionSynchronization.NATIVE); + } + + /** + * Obtain the default {@link MongoDatabase database} form the given {@link MongoDbFactory factory}. + *

+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current + * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() snychronization is active}. + * + * @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from. + * @param sessionSynchronization the synchronization to use. Must not be {@literal null}. + * @return must not be {@literal null}. + */ + public static MongoDatabase getDatabase(MongoDbFactory factory, SessionSynchronization sessionSynchronization) { + return doGetMongoDatabase(null, factory, sessionSynchronization); + } + + /** + * Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDbFactory factory} using + * {@link SessionSynchronization#NATIVE native session synchronization}. + *

+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current + * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() snychronization is active}. + * + * @param dbName the name of the {@link MongoDatabase} to get. + * @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from. + * @return must not be {@literal null}. + */ + public static MongoDatabase getDatabase(String dbName, MongoDbFactory factory) { + return doGetMongoDatabase(dbName, factory, SessionSynchronization.NATIVE); + } + + /** + * Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDbFactory factory}. + *

+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current + * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() snychronization is active}. + * + * @param dbName the name of the {@link MongoDatabase} to get. + * @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from. + * @param sessionSynchronization the synchronization to use. Must not be {@literal null}. + * @return must not be {@literal null}. + */ + public static MongoDatabase getDatabase(String dbName, MongoDbFactory factory, + SessionSynchronization sessionSynchronization) { + return doGetMongoDatabase(dbName, factory, sessionSynchronization); + } + + private static MongoDatabase doGetMongoDatabase(@Nullable String dbName, MongoDbFactory factory, + SessionSynchronization sessionSynchronization) { + + Assert.notNull(factory, "Factory must not be null!"); + + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + return StringUtils.hasText(dbName) ? factory.getDb(dbName) : factory.getDb(); + } + + ClientSession session = doGetSession(factory, sessionSynchronization); + + if(session == null) { + return StringUtils.hasText(dbName) ? factory.getDb(dbName) : factory.getDb(); + } + + MongoDbFactory factoryToUse = factory.withSession(session); + return StringUtils.hasText(dbName) ? factoryToUse.getDb(dbName) : factoryToUse.getDb(); + } + + @Nullable + private static ClientSession doGetSession(MongoDbFactory dbFactory, SessionSynchronization sessionSynchronization) { + + MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager.getResource(dbFactory); + + // check for native MongoDB transaction + if (resourceHolder != null && (resourceHolder.hasSession() || resourceHolder.isSynchronizedWithTransaction())) { + + resourceHolder.requested(); + if (!resourceHolder.hasSession()) { + resourceHolder.setSession(createClientSession(dbFactory)); + } + return resourceHolder.getSession(); + } + + if (SessionSynchronization.NATIVE.equals(sessionSynchronization)) { + return null; + } + + // init a non native MongoDB transaction by registering a MongoSessionSynchronization + + resourceHolder = new MongoResourceHolder(createClientSession(dbFactory), dbFactory); + resourceHolder.requested(); + resourceHolder.getSession().startTransaction(); + + TransactionSynchronizationManager + .registerSynchronization(new MongoSessionSynchronization(resourceHolder, dbFactory)); + resourceHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.bindResource(dbFactory, resourceHolder); + + return resourceHolder.getSession(); + } + + private static ClientSession createClientSession(MongoDbFactory dbFactory) { + return dbFactory.getSession(ClientSessionOptions.builder().causallyConsistent(true).build()); + } + + /** + * MongoDB specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when + * participating in a non-native MongoDB transaction, such as a Jta or JDBC transaction. + * + * @author Christoph Strobl + * @since 2.1 + */ + private static class MongoSessionSynchronization extends ResourceHolderSynchronization { + + private final MongoResourceHolder resourceHolder; + + MongoSessionSynchronization(MongoResourceHolder resourceHolder, MongoDbFactory dbFactory) { + + super(resourceHolder, dbFactory); + this.resourceHolder = resourceHolder; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceHolderSynchronization#shouldReleaseBeforeCompletion() + */ + @Override + protected boolean shouldReleaseBeforeCompletion() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceHolderSynchronization#processResourceAfterCommit(java.lang.Object) + */ + @Override + protected void processResourceAfterCommit(MongoResourceHolder resourceHolder) { + + if (isTransactionActive(resourceHolder)) { + resourceHolder.getSession().commitTransaction(); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceHolderSynchronization#afterCompletion(int) + */ + @Override + public void afterCompletion(int status) { + + if (status == TransactionSynchronization.STATUS_ROLLED_BACK && isTransactionActive(this.resourceHolder)) { + resourceHolder.getSession().abortTransaction(); + } + + super.afterCompletion(status); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceHolderSynchronization#releaseResource(java.lang.Object, java.lang.Object) + */ + @Override + protected void releaseResource(MongoResourceHolder resourceHolder, Object resourceKey) { + + if (resourceHolder.hasActiveSession()) { + resourceHolder.getSession().close(); + } + } + + private boolean isTransactionActive(MongoResourceHolder resourceHolder) { + + if (!resourceHolder.hasSession()) { + return false; + } + + return resourceHolder.getSession().hasActiveTransaction(); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java index 21cef8a8d..19ca9fe79 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java @@ -22,8 +22,8 @@ import org.springframework.data.mongodb.core.MongoExceptionTranslator; import com.mongodb.ClientSessionOptions; import com.mongodb.DB; +import com.mongodb.client.ClientSession; import com.mongodb.client.MongoDatabase; -import com.mongodb.session.ClientSession; /** * Interface for factories creating {@link MongoDatabase} instances. @@ -32,7 +32,7 @@ import com.mongodb.session.ClientSession; * @author Thomas Darimont * @author Christoph Strobl */ -public interface MongoDbFactory extends CodecRegistryProvider { +public interface MongoDbFactory extends CodecRegistryProvider, MongoSessionProvider { /** * Creates a default {@link MongoDatabase} instance. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java new file mode 100644 index 000000000..f77d0ea6c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java @@ -0,0 +1,122 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb; + +import org.springframework.lang.Nullable; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.ResourceHolderSupport; + +import com.mongodb.client.ClientSession; + +/** + * MongoDB specific {@link ResourceHolderSupport resource holder}, wrapping a {@link ClientSession}. + * {@link MongoTransactionManager} binds instances of this class to the thread. + *

+ * Note: Intended for internal usage only. + * + * @author Christoph Strobl + * @since 2.1 + * @see MongoTransactionManager + * @see org.springframework.data.mongodb.core.MongoTemplate + */ +class MongoResourceHolder extends ResourceHolderSupport { + + private @Nullable ClientSession session; + private MongoDbFactory dbFactory; + + /** + * Create a new {@link MongoResourceHolder} for a given {@link ClientSession session}. + * + * @param session the associated {@link ClientSession}. Can be {@literal null}. + * @param dbFactory the associated {@link MongoDbFactory}. must not be {@literal null}. + */ + MongoResourceHolder(@Nullable ClientSession session, MongoDbFactory dbFactory) { + + this.session = session; + this.dbFactory = dbFactory; + } + + /** + * @return the associated {@link ClientSession}. Can be {@literal null}. + */ + @Nullable + ClientSession getSession() { + return session; + } + + /** + * @return the associated {@link MongoDbFactory}. + */ + public MongoDbFactory getDbFactory() { + return dbFactory; + } + + /** + * Set the {@link ClientSession} to guard. + * + * @param session can be {@literal null}. + */ + public void setSession(@Nullable ClientSession session) { + this.session = session; + } + + /** + * Only set the timeout if it does not match the {@link TransactionDefinition#TIMEOUT_DEFAULT default timeout}. + * + * @param seconds + */ + void setTimeoutIfNotDefaulted(int seconds) { + + if (seconds != TransactionDefinition.TIMEOUT_DEFAULT) { + setTimeoutInSeconds(seconds); + } + } + + /** + * @return {@literal true} if session is not {@literal null}. + */ + boolean hasSession() { + return session != null; + } + + /** + * @return {@literal true} if the session is active and has not been closed. + */ + boolean hasActiveSession() { + + if (!hasSession()) { + return false; + } + + return hasServerSession() && !getSession().getServerSession().isClosed(); + } + + /** + * @return {@literal true} if the {@link ClientSession} has a {@link com.mongodb.session.ServerSession} associated + * that is accessible via {@link ClientSession#getServerSession()}. + */ + boolean hasServerSession() { + + try { + return getSession().getServerSession() != null; + } catch (IllegalStateException serverSessionClosed) { + // ignore + } + + return false; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoSessionProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoSessionProvider.java new file mode 100644 index 000000000..45ca6c2ab --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoSessionProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb; + +import com.mongodb.ClientSessionOptions; +import com.mongodb.client.ClientSession; + +/** + * A simple interface for obtaining a {@link ClientSession} to be consumed by + * {@link org.springframework.data.mongodb.core.MongoOperations} and MongoDB native operations that support causal + * consistency and transactions. + * + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + * @since 2.1 + */ +@FunctionalInterface +public interface MongoSessionProvider { + + /** + * Obtain a {@link ClientSession} with with given options. + * + * @param options must not be {@literal null}. + * @return never {@literal null}. + * @throws org.springframework.dao.DataAccessException + */ + ClientSession getSession(ClientSessionOptions options); +} 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 new file mode 100644 index 000000000..dbe612aa9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java @@ -0,0 +1,461 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.mongodb.ClientSessionOptions; +import com.mongodb.MongoException; +import com.mongodb.TransactionOptions; +import com.mongodb.client.ClientSession; + +/** + * A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages + * {@link ClientSession} based transactions for a single {@link MongoDbFactory}. + *

+ * Binds a {@link ClientSession} from the specified {@link MongoDbFactory} to the thread. + *

+ * {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link ClientSession} and enable causal + * consistency, and also {@link ClientSession#startTransaction() start}, {@link ClientSession#commitTransaction() + * commit} or {@link ClientSession#abortTransaction() abort} a transaction. + * + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + * @since 2.1 + * @see MongoDB Transaction Documentation + */ +public class MongoTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, InitializingBean { + + private @Nullable MongoDbFactory dbFactory; + private @Nullable TransactionOptions options; + + /** + * Create a new {@link MongoTransactionManager} for bean-style usage. + *

+ * Note:The {@link MongoDbFactory db factory} has to be {@link #setDbFactory(MongoDbFactory) 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 eg. + * {@link com.mongodb.ReadConcern} and {@link com.mongodb.WriteConcern}. + * + * @see #setDbFactory(MongoDbFactory) + * @see #setTransactionSynchronization(int) + */ + public MongoTransactionManager() {} + + /** + * Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDbFactory}. + * + * @param dbFactory must not be {@literal null}. + */ + public MongoTransactionManager(MongoDbFactory dbFactory) { + this(dbFactory, null); + } + + /** + * Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDbFactory} applying the + * given {@link TransactionOptions options}, if present, when starting a new transaction. + * + * @param dbFactory must not be {@literal null}. + * @param options can be {@literal null}. + */ + public MongoTransactionManager(MongoDbFactory dbFactory, @Nullable TransactionOptions options) { + + Assert.notNull(dbFactory, "DbFactory must not be null!"); + + this.dbFactory = dbFactory; + this.options = options; + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doGetTransaction() + */ + @Override + protected Object doGetTransaction() throws TransactionException { + + MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager + .getResource(getRequiredDbFactory()); + return new MongoTransactionObject(resourceHolder); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#isExistingTransaction(java.lang.Object) + */ + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + return extractMongoTransaction(transaction).hasResourceHolder(); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doBegin(java.lang.Object, org.springframework.transaction.TransactionDefinition) + */ + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + + MongoTransactionObject mongoTransactionObject = extractMongoTransaction(transaction); + + MongoResourceHolder resourceHolder = newResourceHolder(definition, + ClientSessionOptions.builder().causallyConsistent(true).build()); + mongoTransactionObject.setResourceHolder(resourceHolder); + + if (logger.isDebugEnabled()) { + logger + .debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getSession()))); + } + + try { + mongoTransactionObject.startTransaction(options); + } catch (MongoException ex) { + throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.", + debugString(mongoTransactionObject.getSession())), ex); + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession()))); + } + + resourceHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.bindResource(dbFactory, resourceHolder); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doSuspend(java.lang.Object) + */ + @Override + protected Object doSuspend(Object transaction) throws TransactionException { + + MongoTransactionObject mongoTransactionObject = extractMongoTransaction(transaction); + mongoTransactionObject.setResourceHolder(null); + + return TransactionSynchronizationManager.unbindResource(getRequiredDbFactory()); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doResume(java.lang.Object, java.lang.Object) + */ + @Override + protected void doResume(@Nullable Object transaction, Object suspendedResources) { + TransactionSynchronizationManager.bindResource(getRequiredDbFactory(), suspendedResources); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus) + */ + @Override + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + + MongoTransactionObject mongoTransactionObject = extractMongoTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to commit transaction for session %s.", + debugString(mongoTransactionObject.getSession()))); + } + + try { + mongoTransactionObject.commitTransaction(); + } catch (MongoException ex) { + + throw new TransactionSystemException(String.format("Could not commit Mongo transaction for session %s.", + debugString(mongoTransactionObject.getSession())), ex); + } + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doRollback(org.springframework.transaction.support.DefaultTransactionStatus) + */ + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + + MongoTransactionObject mongoTransactionObject = extractMongoTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to abort transaction for session %s.", + debugString(mongoTransactionObject.getSession()))); + } + + try { + mongoTransactionObject.abortTransaction(); + } catch (MongoException ex) { + + throw new TransactionSystemException(String.format("Could not abort Mongo transaction for session %s.", + debugString(mongoTransactionObject.getSession())), ex); + } + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doSetRollbackOnly(org.springframework.transaction.support.DefaultTransactionStatus) + */ + @Override + protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException { + + MongoTransactionObject transactionObject = extractMongoTransaction(status); + transactionObject.getRequiredResourceHolder().setRollbackOnly(); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doCleanupAfterCompletion(java.lang.Object) + */ + @Override + protected void doCleanupAfterCompletion(Object transaction) { + + Assert.isInstanceOf(MongoTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", MongoTransactionObject.class, + transaction.getClass())); + + MongoTransactionObject mongoTransactionObject = (MongoTransactionObject) transaction; + + // Remove the connection holder from the thread. + TransactionSynchronizationManager.unbindResource(getRequiredDbFactory()); + mongoTransactionObject.getRequiredResourceHolder().clear(); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to release Session %s after transaction.", + debugString(mongoTransactionObject.getSession()))); + } + + mongoTransactionObject.closeSession(); + } + + /** + * Set the {@link MongoDbFactory} that this instance should manage transactions for. + * + * @param dbFactory must not be {@literal null}. + */ + public void setDbFactory(MongoDbFactory dbFactory) { + + Assert.notNull(dbFactory, "DbFactory must not be null!"); + this.dbFactory = dbFactory; + } + + /** + * Set the {@link TransactionOptions} to be applied when starting transactions. + * + * @param options can be {@literal null}. + */ + public void setOptions(@Nullable TransactionOptions options) { + this.options = options; + } + + /** + * Get the {@link MongoDbFactory} that this instance manages transactions for. + * + * @return can be {@literal null}. + */ + @Nullable + public MongoDbFactory getDbFactory() { + return dbFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceTransactionManager#getResourceFactory() + */ + @Override + public MongoDbFactory getResourceFactory() { + return getRequiredDbFactory(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() throws Exception { + getRequiredDbFactory(); + } + + private MongoResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options) { + + MongoDbFactory dbFactory = getResourceFactory(); + + MongoResourceHolder resourceHolder = new MongoResourceHolder(dbFactory.getSession(options), dbFactory); + resourceHolder.setTimeoutIfNotDefaulted(determineTimeout(definition)); + + return resourceHolder; + } + + /** + * @throws IllegalStateException if {@link #dbFactory} is {@literal null}. + */ + private MongoDbFactory getRequiredDbFactory() { + + Assert.state(dbFactory != null, + "MongoTransactionManager operates upon a MongoDbFactory. Did you forget to provide one? It's required."); + + return dbFactory; + } + + private static MongoTransactionObject extractMongoTransaction(Object transaction) { + + Assert.isInstanceOf(MongoTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", MongoTransactionObject.class, + transaction.getClass())); + + return (MongoTransactionObject) transaction; + } + + private static MongoTransactionObject extractMongoTransaction(DefaultTransactionStatus status) { + + Assert.isInstanceOf(MongoTransactionObject.class, status.getTransaction(), + () -> String.format("Expected to find a %s but it turned out to be %s.", MongoTransactionObject.class, + status.getTransaction().getClass())); + + return (MongoTransactionObject) status.getTransaction(); + } + + private static String debugString(@Nullable ClientSession session) { + + if (session == null) { + return "null"; + } + + String debugString = "[" + ClassUtils.getShortName(session.getClass()) + "@" + + Integer.toHexString(session.hashCode()) + " "; + + try { + if (session.getServerSession() != null) { + debugString += "id = " + session.getServerSession().getIdentifier() + ", "; + debugString += "causallyConsistent = " + session.isCausallyConsistent() + ", "; + debugString += "txActive = " + session.hasActiveTransaction() + ", "; + debugString += "txNumber = " + session.getServerSession().getTransactionNumber() + ", "; + debugString += "statementId = " + session.getServerSession().getStatementId() + ", "; + debugString += "clusterTime = " + session.getClusterTime(); + } else { + debugString += "id = n/a"; + debugString += "causallyConsistent = " + session.isCausallyConsistent() + ", "; + debugString += "txActive = " + session.hasActiveTransaction() + ", "; + debugString += "clusterTime = " + session.getClusterTime(); + } + } catch (RuntimeException e) { + debugString += "error = " + e.getMessage(); + } + + debugString += "]"; + + return debugString; + } + + /** + * MongoDB specific transaction object, representing a {@link MongoResourceHolder}. Used as transaction object by + * {@link MongoTransactionManager}. + * + * @author Christoph Strobl + * @since 2.1 + * @see MongoResourceHolder + */ + static class MongoTransactionObject implements SmartTransactionObject { + + private @Nullable MongoResourceHolder resourceHolder; + + MongoTransactionObject(@Nullable MongoResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + void setResourceHolder(@Nullable MongoResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + boolean hasResourceHolder() { + return resourceHolder != null; + } + + void commitTransaction() { + getRequiredSession().commitTransaction(); + } + + void abortTransaction() { + getRequiredSession().abortTransaction(); + } + + void startTransaction(@Nullable TransactionOptions options) { + + ClientSession session = getRequiredSession(); + if (options != null) { + session.startTransaction(options); + } else { + session.startTransaction(); + } + } + + void closeSession() { + + ClientSession session = getRequiredSession(); + if (session.getServerSession() != null && !session.getServerSession().isClosed()) { + session.close(); + } + } + + @Nullable + ClientSession getSession() { + return resourceHolder != null ? resourceHolder.getSession() : null; + } + + private MongoResourceHolder getRequiredResourceHolder() { + + Assert.state(resourceHolder != null, "MongoResourceHolder is required but not present. o_O"); + return resourceHolder; + } + + private ClientSession getRequiredSession() { + + ClientSession session = getSession(); + Assert.state(session != null, "A Session is required but it turned out to be null."); + return session; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#isRollbackOnly() + */ + @Override + public boolean isRollbackOnly() { + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#flush() + */ + @Override + public void flush() { + TransactionSynchronizationUtils.triggerFlush(); + } + + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseFactory.java index 889fae8ba..1b6dbf73b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseFactory.java @@ -24,8 +24,8 @@ import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.core.MongoExceptionTranslator; import com.mongodb.ClientSessionOptions; -import com.mongodb.reactivestreams.client.MongoDatabase; import com.mongodb.session.ClientSession; +import com.mongodb.reactivestreams.client.MongoDatabase; /** * Interface for factories creating reactive {@link MongoDatabase} instances. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java index 03123ee2e..ffa75657e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java @@ -57,6 +57,7 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { private final Class targetType; private final Class collectionType; private final Class databaseType; + private final Class sessionType; /** * Create a new SessionAwareMethodInterceptor for given target. @@ -71,12 +72,13 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { * {@code MongoCollection}. * @param target object type. */ - public SessionAwareMethodInterceptor(ClientSession session, T target, Class databaseType, - ClientSessionOperator databaseDecorator, Class collectionType, + public SessionAwareMethodInterceptor(ClientSession session, T target, Class sessionType, + Class databaseType, ClientSessionOperator databaseDecorator, Class collectionType, ClientSessionOperator collectionDecorator) { Assert.notNull(session, "ClientSession must not be null!"); Assert.notNull(target, "Target must not be null!"); + Assert.notNull(sessionType, "SessionType must not be null!"); Assert.notNull(databaseType, "Database type must not be null!"); Assert.notNull(databaseDecorator, "Database ClientSessionOperator must not be null!"); Assert.notNull(collectionType, "Collection type must not be null!"); @@ -90,6 +92,7 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { this.databaseDecorator = databaseDecorator; this.targetType = ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseType : collectionType; + this.sessionType = sessionType; } /* @@ -114,7 +117,7 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { return methodInvocation.proceed(); } - Optional targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType); + Optional targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType, sessionType); return !targetMethod.isPresent() ? methodInvocation.proceed() : ReflectionUtils.invokeMethod(targetMethod.get(), target, @@ -171,18 +174,19 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { * @param targetClass * @return */ - Optional lookup(Method method, Class targetClass) { + Optional lookup(Method method, Class targetClass, Class sessionType) { return cache.computeIfAbsent(new MethodClassKey(method, targetClass), - val -> Optional.ofNullable(findTargetWithSession(method, targetClass))); + val -> Optional.ofNullable(findTargetWithSession(method, targetClass, sessionType))); } @Nullable - private Method findTargetWithSession(Method sourceMethod, Class targetType) { + private Method findTargetWithSession(Method sourceMethod, Class targetType, + Class sessionType) { Class[] argTypes = sourceMethod.getParameterTypes(); Class[] args = new Class[argTypes.length + 1]; - args[0] = ClientSession.class; + args[0] = sessionType; System.arraycopy(argTypes, 0, args, 1, argTypes.length); return ReflectionUtils.findMethod(targetType, sourceMethod.getName(), args); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionSynchronization.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionSynchronization.java new file mode 100644 index 000000000..9b0cb7bfa --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionSynchronization.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb; + +/** + * {@link SessionSynchronization} is used along with {@link org.springframework.data.mongodb.core.MongoTemplate} to + * define in which type of transactions to participate if any. + * + * @author Christoph Strobl + * @since 2.1 + */ +public enum SessionSynchronization { + + /** + * Synchronize with native MongoDB transactions as those initiated via {@link MongoTransactionManager}. + */ + NATIVE, + + /** + * Synchronize with any ongoing transaction and initiate a MongoDB transaction when doing so by registering a MongoDB + * specific {@link org.springframework.transaction.support.ResourceHolderSynchronization}. + */ + ANY; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java index d7a3ad2e6..b53a16c5c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java @@ -40,8 +40,7 @@ import com.mongodb.MongoClient; * @see MongoConfigurationSupport */ @Configuration -public abstract class -AbstractMongoConfiguration extends MongoConfigurationSupport { +public abstract class AbstractMongoConfiguration extends MongoConfigurationSupport { /** * Return the {@link MongoClient} instance to connect to. Annotate with {@link Bean} in case you want to expose a @@ -111,4 +110,5 @@ AbstractMongoConfiguration extends MongoConfigurationSupport { return converter; } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java index 2c55bc982..5ad61d6b2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java @@ -86,6 +86,13 @@ public interface ExecutableRemoveOperation { */ DeleteResult all(); + /** + * Remove the first matching document. + * + * @return the {@link DeleteResult}. Never {@literal null}. + */ + DeleteResult one(); + /** * Remove and return all matching documents.
* NOTE The entire list of documents will be fetched before sending the actual delete commands. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java index a27108eee..50b75ef55 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java @@ -98,10 +98,16 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation { */ @Override public DeleteResult all() { + return template.doRemove(getCollectionName(), query, domainType, true); + } - String collectionName = getCollectionName(); - - return template.doRemove(collectionName, query, domainType); + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove#one() + */ + @Override + public DeleteResult one() { + return template.doRemove(getCollectionName(), query, domainType, false); } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index b5f5a9a2e..bcc815fe6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -46,10 +46,10 @@ import org.springframework.util.Assert; import com.mongodb.ClientSessionOptions; import com.mongodb.Cursor; import com.mongodb.ReadPreference; +import com.mongodb.client.ClientSession; import com.mongodb.client.MongoCollection; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; -import com.mongodb.session.ClientSession; /** * Interface that specifies a basic set of MongoDB operations. Implemented by {@link MongoTemplate}. Not often used but diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 0932c2409..bf3f41af4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -61,7 +61,9 @@ import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.SessionSynchronization; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext; import org.springframework.data.mongodb.core.aggregation.Aggregation; @@ -134,6 +136,7 @@ import com.mongodb.MongoException; import com.mongodb.ReadPreference; import com.mongodb.WriteConcern; import com.mongodb.client.AggregateIterable; +import com.mongodb.client.ClientSession; import com.mongodb.client.DistinctIterable; import com.mongodb.client.FindIterable; import com.mongodb.client.MapReduceIterable; @@ -141,19 +144,9 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.CountOptions; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.DeleteOptions; -import com.mongodb.client.model.Filters; -import com.mongodb.client.model.FindOneAndDeleteOptions; -import com.mongodb.client.model.FindOneAndUpdateOptions; -import com.mongodb.client.model.ReturnDocument; -import com.mongodb.client.model.UpdateOptions; -import com.mongodb.client.model.ValidationAction; -import com.mongodb.client.model.ValidationLevel; +import com.mongodb.client.model.*; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; -import com.mongodb.session.ClientSession; import com.mongodb.util.JSONParseException; /** @@ -214,6 +207,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private @Nullable ResourceLoader resourceLoader; private @Nullable MongoPersistentEntityIndexCreator indexCreator; + private SessionSynchronization sessionSynchronization = SessionSynchronization.NATIVE; + /** * Constructor used for a basic template configuration * @@ -267,6 +262,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.mongoDbFactory = dbFactory; this.exceptionTranslator = that.exceptionTranslator; + this.sessionSynchronization = that.sessionSynchronization; this.mongoConverter = that.mongoConverter instanceof MappingMongoConverter ? getDefaultMongoConverter(dbFactory) : that.mongoConverter; this.queryMapper = that.queryMapper; @@ -582,6 +578,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return new SessionBoundMongoTemplate(session, MongoTemplate.this); } + /** + * Define if {@link MongoTemplate} should participate in transactions. Default is set to + * {@link SessionSynchronization#NATIVE}.
+ * NOTE: MongoDB transactions require at least MongoDB 4.0. + * + * @since 2.1 + */ + public void setSessionSynchronization(SessionSynchronization sessionSynchronization) { + this.sessionSynchronization = sessionSynchronization; + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.MongoOperations#createCollection(java.lang.Class) @@ -687,7 +694,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, public Void doInCollection(MongoCollection collection) throws MongoException, DataAccessException { collection.drop(); if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Dropped collection [{}]", collection.getNamespace().getCollectionName()); + LOGGER.debug("Dropped collection [{}]", + collection.getNamespace() != null ? collection.getNamespace().getCollectionName() : collectionName); } return null; } @@ -1158,7 +1166,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * In case of using MongoDB Java driver version 3 the returned {@link WriteConcern} will be defaulted to * {@link WriteConcern#ACKNOWLEDGED} when {@link WriteResultChecking} is set to {@link WriteResultChecking#EXCEPTION}. * - * @param writeConcern any WriteConcern already configured or null + * @param mongoAction any MongoAction already configured or null * @return The prepared WriteConcern or null */ @Nullable @@ -1474,10 +1482,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, collection.withWriteConcern(writeConcernToUse).insertOne(dbDoc); } } else if (writeConcernToUse == null) { - collection.replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc, new UpdateOptions().upsert(true)); + collection.replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc, new ReplaceOptions().upsert(true)); } else { collection.withWriteConcern(writeConcernToUse).replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc, - new UpdateOptions().upsert(true)); + new ReplaceOptions().upsert(true)); } return dbDoc.get(ID_FIELD); } @@ -1583,7 +1591,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, collection = writeConcernToUse != null ? collection.withWriteConcern(writeConcernToUse) : collection; if (!UpdateMapper.isUpdateObject(updateObj)) { - return collection.replaceOne(queryObj, updateObj, opts); + + ReplaceOptions replaceOptions = new ReplaceOptions(); + replaceOptions.collation(opts.getCollation()); + replaceOptions.upsert(opts.isUpsert()); + + return collection.replaceOne(queryObj, updateObj, replaceOptions); } else { if (multi) { return collection.updateMany(queryObj, updateObj, opts); @@ -1619,7 +1632,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(object, "Object must not be null!"); Assert.hasText(collectionName, "Collection name must not be null or empty!"); - return doRemove(collectionName, getIdQueryFor(object), object.getClass()); + return doRemove(collectionName, getIdQueryFor(object), object.getClass(), false); } /** @@ -1708,7 +1721,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public DeleteResult remove(Query query, String collectionName) { - return doRemove(collectionName, query, null); + return doRemove(collectionName, query, null, true); } @Override @@ -1720,11 +1733,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, public DeleteResult remove(Query query, Class entityClass, String collectionName) { Assert.notNull(entityClass, "EntityClass must not be null!"); - return doRemove(collectionName, query, entityClass); + return doRemove(collectionName, query, entityClass, true); } protected DeleteResult doRemove(final String collectionName, final Query query, - @Nullable final Class entityClass) { + @Nullable final Class entityClass, boolean multi) { Assert.notNull(query, "Query must not be null!"); Assert.hasText(collectionName, "Collection name must not be null or empty!"); @@ -1749,7 +1762,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - DeleteResult dr = null; if (LOGGER.isDebugEnabled()) { LOGGER.debug("Remove using query: {} in collection: {}.", new Object[] { serializeToJsonSafely(removeQuery), collectionName }); @@ -1768,15 +1780,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, removeQuery = new Document(ID_FIELD, new Document("$in", ids)); } - if (writeConcernToUse == null) { - dr = collection.deleteMany(removeQuery, options); - } else { - dr = collection.withWriteConcern(writeConcernToUse).deleteMany(removeQuery, options); - } + MongoCollection collectionToUse = writeConcernToUse != null + ? collection.withWriteConcern(writeConcernToUse) : collection; + + DeleteResult result = multi ? collectionToUse.deleteMany(removeQuery, options) + : collection.deleteOne(removeQuery, options); maybeEmitEvent(new AfterDeleteEvent(queryObject, entityClass, collectionName)); - return dr; + return result; } }); } @@ -2288,7 +2300,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } protected MongoDatabase doGetDatabase() { - return mongoDbFactory.getDb(); + return MongoDatabaseUtils.getDatabase(mongoDbFactory, sessionSynchronization); } protected MongoDatabase prepareDatabase(MongoDatabase database) { @@ -2350,7 +2362,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, // TODO: Emit a collection created event if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Created collection [{}]", coll.getNamespace().getCollectionName()); + LOGGER.debug("Created collection [{}]", + coll.getNamespace() != null ? coll.getNamespace().getCollectionName() : collectionName); } return coll; } @@ -2861,7 +2874,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, if (LOGGER.isDebugEnabled()) { LOGGER.debug("findOne using query: {} fields: {} in db.collection: {}", serializeToJsonSafely(query), - serializeToJsonSafely(fields.orElseGet(Document::new)), collection.getNamespace().getFullName()); + serializeToJsonSafely(fields.orElseGet(Document::new)), + collection.getNamespace() != null ? collection.getNamespace().getFullName() : "n/a"); } if (fields.isPresent()) { @@ -3004,7 +3018,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, /** * Simple {@link DocumentCallback} that will transform {@link Document} into the given target type using the given - * {@link MongoReader}. + * {@link EntityReader}. * * @author Oliver Gierke * @author Christoph Strobl diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 6a64a0476..b974d2440 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -127,6 +127,7 @@ import com.mongodb.client.model.DeleteOptions; import com.mongodb.client.model.Filters; import com.mongodb.client.model.FindOneAndDeleteOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; +import com.mongodb.client.model.ReplaceOptions; import com.mongodb.client.model.ReturnDocument; import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.ValidationOptions; @@ -1470,10 +1471,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } } else if (writeConcernToUse == null) { publisher = collection.replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, - new UpdateOptions().upsert(true)); + new ReplaceOptions().upsert(true)); } else { publisher = collection.withWriteConcern(writeConcernToUse) - .replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, new UpdateOptions().upsert(true)); + .replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, new ReplaceOptions().upsert(true)); } return Mono.from(publisher).map(o -> document.get(ID_FIELD)); @@ -1580,7 +1581,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati query.getCollation().map(Collation::toMongoCollation).ifPresent(updateOptions::collation); if (!UpdateMapper.isUpdateObject(updateObj)) { - return collectionToUse.replaceOne(queryObj, updateObj, updateOptions); + + ReplaceOptions replaceOptions = new ReplaceOptions(); + replaceOptions.upsert(updateOptions.isUpsert()); + replaceOptions.collation(updateOptions.getCollation()); + + return collectionToUse.replaceOne(queryObj, updateObj, replaceOptions); } if (multi) { return collectionToUse.updateMany(queryObj, updateObj, updateOptions); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java index dcc65f7f2..2f389219d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java @@ -25,6 +25,7 @@ import org.springframework.lang.Nullable; * @since 2.1 * @see com.mongodb.session.ClientSession */ +@FunctionalInterface public interface SessionCallback { /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java index 939dbf678..0a1c218ca 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java @@ -19,7 +19,7 @@ import java.util.function.Consumer; import org.springframework.lang.Nullable; -import com.mongodb.session.ClientSession; +import com.mongodb.client.ClientSession; /** * Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a {@link SessionCallback}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java index 3706db665..67626cbcb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java @@ -31,9 +31,9 @@ import com.mongodb.DB; import com.mongodb.MongoClient; import com.mongodb.MongoClientURI; import com.mongodb.WriteConcern; +import com.mongodb.client.ClientSession; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; -import com.mongodb.session.ClientSession; /** * Factory to create {@link MongoDatabase} instances from a {@link MongoClient} instance. @@ -243,22 +243,22 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory { return createProxyInstance(session, database, MongoDatabase.class); } - private MongoDatabase proxyDatabase(ClientSession session, MongoDatabase database) { + private MongoDatabase proxyDatabase(com.mongodb.session.ClientSession session, MongoDatabase database) { return createProxyInstance(session, database, MongoDatabase.class); } - private MongoCollection proxyCollection(ClientSession session, MongoCollection collection) { + private MongoCollection proxyCollection(com.mongodb.session.ClientSession session, MongoCollection collection) { return createProxyInstance(session, collection, MongoCollection.class); } - private T createProxyInstance(ClientSession session, T target, Class targetType) { + private T createProxyInstance(com.mongodb.session.ClientSession session, T target, Class targetType) { ProxyFactory factory = new ProxyFactory(); factory.setTarget(target); factory.setInterfaces(targetType); factory.setOpaque(true); - factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase, + factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, MongoDatabase.class, this::proxyDatabase, MongoCollection.class, this::proxyCollection)); return targetType.cast(factory.getProxy()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java index 876923edd..726fb4e9b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java @@ -230,7 +230,7 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React factory.setInterfaces(targetType); factory.setOpaque(true); - factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase, + factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, MongoDatabase.class, this::proxyDatabase, MongoCollection.class, this::proxyCollection)); return targetType.cast(factory.getProxy()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoDatabaseUtilsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoDatabaseUtilsUnitTests.java new file mode 100644 index 000000000..47aadedd7 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoDatabaseUtilsUnitTests.java @@ -0,0 +1,239 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; +import com.mongodb.session.ServerSession; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class MongoDatabaseUtilsUnitTests { + + @Mock ClientSession session; + @Mock ServerSession serverSession; + @Mock MongoDbFactory dbFactory; + @Mock MongoDatabase db; + + @Mock UserTransaction userTransaction; + + @Before + public void setUp() { + + when(dbFactory.getSession(any())).thenReturn(session); + + when(dbFactory.withSession(session)).thenReturn(dbFactory); + + when(dbFactory.getDb()).thenReturn(db); + + when(session.getServerSession()).thenReturn(serverSession); + when(session.hasActiveTransaction()).thenReturn(true); + + when(serverSession.isClosed()).thenReturn(false); + } + + @After + public void verifyTransactionSynchronizationManagerState() { + + assertTrue(TransactionSynchronizationManager.getResourceMap().isEmpty()); + assertFalse(TransactionSynchronizationManager.isSynchronizationActive()); + assertNull(TransactionSynchronizationManager.getCurrentTransactionName()); + assertFalse(TransactionSynchronizationManager.isCurrentTransactionReadOnly()); + assertNull(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel()); + assertFalse(TransactionSynchronizationManager.isActualTransactionActive()); + } + + @Test // DATAMONGO-1920 + public void shouldNotStartSessionWhenNoTransactionOngoing() { + + MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.NATIVE); + + verify(dbFactory, never()).getSession(any()); + verify(dbFactory, never()).withSession(any(ClientSession.class)); + } + + @Test // DATAMONGO-1920 + public void shouldParticipateInOngoingJtaTransactionWithCommitWhenSessionSychronizationIsAny() throws Exception { + + when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + + JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isFalse(); + + MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.ANY); + + assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isTrue(); + } + }); + + verify(userTransaction).begin(); + + verify(session).startTransaction(); + verify(session).commitTransaction(); + verify(session).close(); + } + + @Test // DATAMONGO-1920 + public void shouldParticipateInOngoingJtaTransactionWithRollbackWhenSessionSychronizationIsAny() throws Exception { + + when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + + JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isFalse(); + + MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.ANY); + + assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isTrue(); + + transactionStatus.setRollbackOnly(); + } + }); + + verify(userTransaction).rollback(); + + verify(session).startTransaction(); + verify(session).abortTransaction(); + verify(session).close(); + } + + @Test // DATAMONGO-1920 + public void shouldNotParticipateInOngoingJtaTransactionWithRollbackWhenSessionSychronizationIsNative() + throws Exception { + + when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + + JtaTransactionManager txManager = new JtaTransactionManager(userTransaction); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isFalse(); + + MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.NATIVE); + + assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isFalse(); + + transactionStatus.setRollbackOnly(); + } + }); + + verify(userTransaction).rollback(); + + verify(session, never()).startTransaction(); + verify(session, never()).abortTransaction(); + verify(session, never()).close(); + } + + @Test // DATAMONGO-1920 + public void shouldParticipateInOngoingMongoTransactionWhenSessionSychronizationIsNative() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isTrue(); + + MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.NATIVE); + + transactionStatus.setRollbackOnly(); + } + }); + + verify(session).startTransaction(); + verify(session).abortTransaction(); + verify(session).close(); + } + + @Test // DATAMONGO-1920 + public void shouldParticipateInOngoingMongoTransactionWhenSessionSychronizationIsAny() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(transactionStatus.isNewTransaction()).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isTrue(); + + MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.ANY); + + transactionStatus.setRollbackOnly(); + } + }); + + verify(session).startTransaction(); + verify(session).abortTransaction(); + verify(session).close(); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionManagerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionManagerUnitTests.java new file mode 100644 index 000000000..620a6e542 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionManagerUnitTests.java @@ -0,0 +1,333 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.UnexpectedRollbackException; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; +import com.mongodb.session.ServerSession; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class MongoTransactionManagerUnitTests { + + @Mock ClientSession session; + @Mock ClientSession session2; + @Mock ServerSession serverSession; + @Mock MongoDbFactory dbFactory; + @Mock MongoDbFactory dbFactory2; + @Mock MongoDatabase db; + @Mock MongoDatabase db2; + + @Before + public void setUp() { + + when(dbFactory.getSession(any())).thenReturn(session, session2); + + when(dbFactory.withSession(session)).thenReturn(dbFactory); + when(dbFactory.withSession(session2)).thenReturn(dbFactory2); + + when(dbFactory.getDb()).thenReturn(db); + when(dbFactory2.getDb()).thenReturn(db2); + + when(session.getServerSession()).thenReturn(serverSession); + when(session2.getServerSession()).thenReturn(serverSession); + + when(serverSession.isClosed()).thenReturn(false); + } + + @After + public void verifyTransactionSynchronizationManager() { + + assertTrue(TransactionSynchronizationManager.getResourceMap().isEmpty()); + assertFalse(TransactionSynchronizationManager.isSynchronizationActive()); + } + + @Test // DATAMONGO-1920 + public void triggerCommitCorrectly() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); + + MongoTemplate template = new MongoTemplate(dbFactory); + + template.execute(db -> { + db.drop(); + return null; + }); + + verify(dbFactory).withSession(eq(session)); + + txManager.commit(txStatus); + + verify(session).startTransaction(); + verify(session).commitTransaction(); + verify(session).close(); + } + + @Test // DATAMONGO-1920 + public void participateInOnGoingTransactionWithCommit() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); + + MongoTemplate template = new MongoTemplate(dbFactory); + + template.execute(db -> { + db.drop(); + return null; + }); + + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + + template.execute(db -> { + db.drop(); + return null; + }); + } + }); + + verify(dbFactory, times(2)).withSession(eq(session)); + + txManager.commit(txStatus); + + verify(session).startTransaction(); + verify(session).commitTransaction(); + verify(session).close(); + } + + @Test // DATAMONGO-1920 + public void participateInOnGoingTransactionWithRollbackOnly() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); + + MongoTemplate template = new MongoTemplate(dbFactory); + + template.execute(db -> { + db.drop(); + return null; + }); + + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + + template.execute(db -> { + db.drop(); + return null; + }); + + status.setRollbackOnly(); + } + }); + + verify(dbFactory, times(2)).withSession(eq(session)); + + assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> txManager.commit(txStatus)); + + verify(session).startTransaction(); + verify(session).abortTransaction(); + verify(session).close(); + } + + @Test // DATAMONGO-1920 + public void triggerRollbackCorrectly() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); + + MongoTemplate template = new MongoTemplate(dbFactory); + + template.execute(db -> { + db.drop(); + return null; + }); + + verify(dbFactory).withSession(eq(session)); + + txManager.rollback(txStatus); + + verify(session).startTransaction(); + verify(session).abortTransaction(); + verify(session).close(); + } + + @Test // DATAMONGO-1920 + public void suspendTransactionWhilePropagationNotSupported() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); + + MongoTemplate template = new MongoTemplate(dbFactory); + + template.execute(db -> { + db.drop(); + return null; + }); + + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED); + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + + template.execute(db -> { + db.drop(); + return null; + }); + } + }); + + template.execute(MongoDatabase::listCollections); + txManager.commit(txStatus); + + verify(session).startTransaction(); + verify(session2, never()).startTransaction(); + + verify(dbFactory, times(2)).withSession(eq(session)); + verify(dbFactory, never()).withSession(eq(session2)); + + verify(db, times(2)).drop(); + verify(db).listCollections(); + + verify(session).close(); + verify(session2, never()).close(); + } + + @Test // DATAMONGO-1920 + public void suspendTransactionWhilePropagationRequiresNew() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition()); + + MongoTemplate template = new MongoTemplate(dbFactory); + + template.execute(db -> { + db.drop(); + return null; + }); + + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + + template.execute(db -> { + db.drop(); + return null; + }); + } + }); + + template.execute(MongoDatabase::listCollections); + txManager.commit(txStatus); + + verify(session).startTransaction(); + verify(session2).startTransaction(); + + verify(dbFactory, times(2)).withSession(eq(session)); + verify(dbFactory).withSession(eq(session2)); + + verify(db).drop(); + verify(db2).drop(); + verify(db).listCollections(); + + verify(session).close(); + verify(session2).close(); + } + + @Test // DATAMONGO-1920 + public void readonlyShouldInitiateASessionStartAndCommitTransaction() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + + DefaultTransactionDefinition readonlyTxDefinition = new DefaultTransactionDefinition(); + readonlyTxDefinition.setReadOnly(true); + + TransactionStatus txStatus = txManager.getTransaction(readonlyTxDefinition); + + MongoTemplate template = new MongoTemplate(dbFactory); + + template.execute(db -> { + db.drop(); + return null; + }); + + verify(dbFactory).withSession(eq(session)); + + txManager.commit(txStatus); + + verify(session).startTransaction(); + verify(session).commitTransaction(); + verify(session).close(); + } + + @Test // DATAMONGO-1920 + public void readonlyShouldInitiateASessionStartAndRollbackTransaction() { + + MongoTransactionManager txManager = new MongoTransactionManager(dbFactory); + + DefaultTransactionDefinition readonlyTxDefinition = new DefaultTransactionDefinition(); + readonlyTxDefinition.setReadOnly(true); + + TransactionStatus txStatus = txManager.getTransaction(readonlyTxDefinition); + + MongoTemplate template = new MongoTemplate(dbFactory); + + template.execute(db -> { + db.drop(); + return null; + }); + + verify(dbFactory).withSession(eq(session)); + + txManager.rollback(txStatus); + + verify(session).startTransaction(); + verify(session).abortTransaction(); + verify(session).close(); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SessionAwareMethodInterceptorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SessionAwareMethodInterceptorUnitTests.java index 923035fcc..2df96b867 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SessionAwareMethodInterceptorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SessionAwareMethodInterceptorUnitTests.java @@ -35,9 +35,9 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ClassUtils; import com.mongodb.MongoClient; +import com.mongodb.client.ClientSession; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; -import com.mongodb.session.ClientSession; /** * Unit tests for {@link SessionAwareMethodInterceptor}. @@ -129,7 +129,7 @@ public class SessionAwareMethodInterceptorUnitTests { collection.getReadConcern(); assertThat(cache.contains(readConcernMethod, MongoCollection.class)).isTrue(); - assertThat(cache.lookup(readConcernMethod, MongoCollection.class)).isEmpty(); + assertThat(cache.lookup(readConcernMethod, MongoCollection.class, ClientSession.class)).isEmpty(); } @Test // DATAMONGO-1880 @@ -160,23 +160,23 @@ public class SessionAwareMethodInterceptorUnitTests { verify(otherCollection).drop(eq(session)); } - private MongoDatabase proxyDatabase(ClientSession session, MongoDatabase database) { + private MongoDatabase proxyDatabase(com.mongodb.session.ClientSession session, MongoDatabase database) { return createProxyInstance(session, database, MongoDatabase.class); } - private MongoCollection proxyCollection(ClientSession session, MongoCollection collection) { + private MongoCollection proxyCollection(com.mongodb.session.ClientSession session, MongoCollection collection) { return createProxyInstance(session, collection, MongoCollection.class); } - private T createProxyInstance(ClientSession session, T target, Class targetType) { + private T createProxyInstance(com.mongodb.session.ClientSession session, T target, Class targetType) { ProxyFactory factory = new ProxyFactory(); factory.setTarget(target); factory.setInterfaces(targetType); factory.setOpaque(true); - factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase, - MongoCollection.class, this::proxyCollection)); + factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, MongoDatabase.class, + this::proxyDatabase, MongoCollection.class, this::proxyCollection)); return targetType.cast(factory.getProxy()); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ClientSessionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ClientSessionTests.java index cf207b219..c74697f21 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ClientSessionTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ClientSessionTests.java @@ -16,20 +16,29 @@ package org.springframework.data.mongodb.core; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; + +import lombok.AllArgsConstructor; +import lombok.Data; import org.bson.Document; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; +import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.test.util.MongoTestUtils; +import org.springframework.data.mongodb.test.util.MongoVersion; import org.springframework.data.mongodb.test.util.MongoVersionRule; import org.springframework.data.mongodb.test.util.ReplicaSet; import org.springframework.data.util.Version; import com.mongodb.ClientSessionOptions; import com.mongodb.MongoClient; -import com.mongodb.session.ClientSession; +import com.mongodb.client.ClientSession; /** * @author Christoph Strobl @@ -37,8 +46,11 @@ import com.mongodb.session.ClientSession; */ public class ClientSessionTests { - public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_6_0 = MongoVersionRule.atLeast(Version.parse("3.6.0")); public static @ClassRule TestRule replSet = ReplicaSet.required(); + public @Rule MongoVersionRule REQUIRES_AT_LEAST_3_6_0 = MongoVersionRule.atLeast(Version.parse("3.6.0")); + + private static final String DB_NAME = "client-session-tests"; + private static final String COLLECTION_NAME = "test"; MongoTemplate template; MongoClient client; @@ -46,11 +58,12 @@ public class ClientSessionTests { @Before public void setUp() { - client = new MongoClient(); - template = new MongoTemplate(client, "reflective-client-session-tests"); - template.getDb().getCollection("test").drop(); + client = MongoTestUtils.replSetClient(); - template.getDb().getCollection("test").insertOne(new Document("_id", "id-1").append("value", "spring")); + MongoTestUtils.createOrReplaceCollection(DB_NAME, COLLECTION_NAME, client); + + template = new MongoTemplate(client, DB_NAME); + template.getDb().getCollection(COLLECTION_NAME).insertOne(new Document("_id", "id-1").append("value", "spring")); } @Test // DATAMONGO-1880 @@ -69,4 +82,66 @@ public class ClientSessionTests { session.close(); } + + @Test // DATAMONGO-1920 + @MongoVersion(asOf = "3.7.3") + public void withCommittedTransaction() { + + ClientSession session = client.startSession(ClientSessionOptions.builder().causallyConsistent(true).build()); + + assertThat(session.getOperationTime()).isNull(); + + session.startTransaction(); + + SomeDoc saved = template.withSession(() -> session).execute(action -> { + + SomeDoc doc = new SomeDoc("id-2", "value2"); + action.insert(doc); + return doc; + }); + + session.commitTransaction(); + session.close(); + + assertThat(saved).isNotNull(); + assertThat(session.getOperationTime()).isNotNull(); + + assertThat(template.exists(query(where("id").is(saved.getId())), SomeDoc.class)).isTrue(); + } + + @Test // DATAMONGO-1920 + @MongoVersion(asOf = "3.7.3") + public void withAbortedTransaction() { + + ClientSession session = client.startSession(ClientSessionOptions.builder().causallyConsistent(true).build()); + + assertThat(session.getOperationTime()).isNull(); + + session.startTransaction(); + + SomeDoc saved = template.withSession(() -> session).execute(action -> { + + SomeDoc doc = new SomeDoc("id-2", "value2"); + action.insert(doc); + return doc; + }); + + session.abortTransaction(); + session.close(); + + assertThat(saved).isNotNull(); + assertThat(session.getOperationTime()).isNotNull(); + + assertThat(template.exists(query(where("id").is(saved.getId())), SomeDoc.class)).isFalse(); + } + + @Data + @AllArgsConstructor + @org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME) + static class SomeDoc { + + @Id String id; + String value; + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTransactionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTransactionTests.java new file mode 100644 index 000000000..2a455c4eb --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTransactionTests.java @@ -0,0 +1,183 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; +import static org.springframework.data.mongodb.test.util.MongoTestUtils.*; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.bson.Document; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Persistable; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.MongoTransactionManager; +import org.springframework.data.mongodb.config.AbstractMongoConfiguration; +import org.springframework.data.mongodb.test.util.AfterTransactionAssertion; +import org.springframework.data.mongodb.test.util.MongoTestUtils; +import org.springframework.data.mongodb.test.util.MongoVersionRule; +import org.springframework.data.mongodb.test.util.ReplicaSet; +import org.springframework.data.util.Version; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +import com.mongodb.MongoClient; +import com.mongodb.ReadPreference; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; + +/** + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional(transactionManager = "txManager") +public class MongoTemplateTransactionTests { + + public static @ClassRule RuleChain TEST_RULES = RuleChain.outerRule(MongoVersionRule.atLeast(Version.parse("3.7.3"))) + .around(ReplicaSet.required()); + + static final String DB_NAME = "template-tx-tests"; + static final String COLLECTION_NAME = "assassins"; + + @Configuration + static class Config extends AbstractMongoConfiguration { + + @Bean + public MongoClient mongoClient() { + return MongoTestUtils.replSetClient(); + } + + @Override + protected String getDatabaseName() { + return DB_NAME; + } + + @Bean + MongoTransactionManager txManager(MongoDbFactory dbFactory) { + return new MongoTransactionManager(dbFactory); + } + } + + @Autowired MongoTemplate template; + @Autowired MongoClient client; + + List>> assertionList; + + @Before + public void setUp() { + + template.setReadPreference(ReadPreference.primary()); + assertionList = new CopyOnWriteArrayList<>(); + } + + @BeforeTransaction + public void xxx() { + + createOrReplaceCollection(DB_NAME, COLLECTION_NAME, client); + } + + @AfterTransaction + public void verifyDbState() throws InterruptedException { + + MongoCollection collection = client.getDatabase(DB_NAME).withReadPreference(ReadPreference.primary()) + .getCollection(COLLECTION_NAME); + + assertionList.forEach(it -> { + + boolean isPresent = collection.count(Filters.eq("_id", it.getId())) != 0; + + assertThat(isPresent).isEqualTo(it.shouldBePresent()) + .withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(), + it.shouldBePresent() ? "be present" : "NOT be present")); + }); + } + + @Rollback(false) + @Test // DATAMONGO-1920 + public void shouldOperateCommitCorrectly() { + + Assassin hu = new Assassin("hu", "Hu Gibbet"); + template.save(hu); + + assertAfterTransaction(hu).isPresent(); + } + + @Test // DATAMONGO-1920 + public void shouldOperateRollbackCorrectly() { + + Assassin vi = new Assassin("vi", "Viridiana Sovari"); + template.save(vi); + + assertAfterTransaction(vi).isNotPresent(); + } + + @Test // DATAMONGO-1920 + public void shouldBeAbleToViewChangesDuringTransaction() throws InterruptedException { + + Assassin durzo = new Assassin("durzo", "Durzo Blint"); + template.save(durzo); + + Thread.sleep(100); + Assassin retrieved = template.findOne(query(where("id").is(durzo.getId())), Assassin.class); + + assertThat(retrieved).isEqualTo(durzo); + + assertAfterTransaction(durzo).isNotPresent(); + } + + // --- Just some helpers and tests entities + + private AfterTransactionAssertion assertAfterTransaction(Assassin assassin) { + + AfterTransactionAssertion assertion = new AfterTransactionAssertion(assassin); + assertionList.add(assertion); + return assertion; + } + + @Data + @AllArgsConstructor + @org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME) + static class Assassin implements Persistable { + + @Id String id; + String name; + + @Override + public boolean isNew() { + return id == null; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 8451d8c52..ccee9a1bc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.any; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; +import com.mongodb.client.model.ReplaceOptions; import lombok.Data; import java.math.BigInteger; @@ -716,7 +717,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Test // DATAMONGO-1518 public void findAndRemoveManyShouldUseCollationWhenPresent() { - template.doRemove("collection-1", new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class); + template.doRemove("collection-1", new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class, true); ArgumentCaptor options = ArgumentCaptor.forClass(DeleteOptions.class); verify(collection).deleteMany(any(), options.capture()); @@ -754,7 +755,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), new Update(), AutogenerateableId.class); - ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + ArgumentCaptor options = ArgumentCaptor.forClass(ReplaceOptions.class); verify(collection).replaceOne(any(), any(), options.capture()); assertThat(options.getValue().getCollation().getLocale(), is("fr")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index da73018cc..fd44ad563 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.*; import static org.mockito.Mockito.any; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; +import com.mongodb.client.model.ReplaceOptions; import lombok.Data; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -233,12 +234,12 @@ public class ReactiveMongoTemplateUnitTests { @Test // DATAMONGO-1518 public void replaceOneShouldUseCollationWhenPresent() { - when(collection.replaceOne(any(Bson.class), any(), any())).thenReturn(Mono.empty()); + when(collection.replaceOne(any(Bson.class), any(), any(ReplaceOptions.class))).thenReturn(Mono.empty()); template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), new Update(), AutogenerateableId.class) .subscribe(); - ArgumentCaptor options = ArgumentCaptor.forClass(UpdateOptions.class); + ArgumentCaptor options = ArgumentCaptor.forClass(ReplaceOptions.class); verify(collection).replaceOne(any(Bson.class), any(), options.capture()); assertThat(options.getValue().getCollation().getLocale(), is("fr")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java index 3d1a369ee..5376d9e0f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java @@ -61,9 +61,9 @@ import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.ClientSessionOptions; import com.mongodb.MongoClient; +import com.mongodb.client.ClientSession; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; -import com.mongodb.session.ClientSession; /** * Integration tests for {@link SessionBoundMongoTemplate} operating up an active {@link ClientSession}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java index c510a002b..0087bad36 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java @@ -56,7 +56,7 @@ import com.mongodb.client.model.CountOptions; import com.mongodb.client.model.DeleteOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.UpdateOptions; -import com.mongodb.session.ClientSession; +import com.mongodb.client.ClientSession; /** * Unit test for {@link SessionBoundMongoTemplate} making sure a proxied {@link MongoCollection} and diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleMongoDbFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleMongoDbFactoryUnitTests.java index fa5a9249d..641522d0a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleMongoDbFactoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleMongoDbFactoryUnitTests.java @@ -37,7 +37,7 @@ import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.MongoClient; import com.mongodb.MongoClientURI; import com.mongodb.client.MongoDatabase; -import com.mongodb.session.ClientSession; +import com.mongodb.client.ClientSession; /** * Unit tests for {@link SimpleMongoDbFactory}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java new file mode 100644 index 000000000..44b99ea18 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.test.util.MongoTestUtils.*; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.bson.Document; +import org.bson.types.ObjectId; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Persistable; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.MongoTransactionManager; +import org.springframework.data.mongodb.config.AbstractMongoConfiguration; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; +import org.springframework.data.mongodb.test.util.AfterTransactionAssertion; +import org.springframework.data.mongodb.test.util.MongoTestUtils; +import org.springframework.data.mongodb.test.util.MongoVersionRule; +import org.springframework.data.mongodb.test.util.ReplicaSet; +import org.springframework.data.util.Version; +import org.springframework.lang.Nullable; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.ReadPreference; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; + +/** + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional(transactionManager = "txManager") +public class PersonRepositoryTransactionalTests { + + public static @ClassRule RuleChain TEST_RULES = RuleChain.outerRule(MongoVersionRule.atLeast(Version.parse("3.7.3"))) + .around(ReplicaSet.required()); + + static final String DB_NAME = "repository-tx-tests"; + + @Configuration + @EnableMongoRepositories + static class Config extends AbstractMongoConfiguration { + + @Bean + public MongoClient mongoClient() { + return MongoTestUtils.replSetClient(); + } + + @Override + protected String getDatabaseName() { + return DB_NAME; + } + + @Bean + MongoTransactionManager txManager(MongoDbFactory dbFactory) { + return new MongoTransactionManager(dbFactory); + } + } + + public @Rule ExpectedException expectedException = ExpectedException.none(); + + @Autowired MongoClient client; + @Autowired PersonRepository repository; + @Autowired MongoTemplate template; + + Person durzo, kylar, vi; + + List all; + + List>> assertionList; + + @Before + public void setUp() throws InterruptedException { + assertionList = new CopyOnWriteArrayList<>(); + } + + @BeforeTransaction + public void beforeTransaction() throws InterruptedException { + + createOrReplaceCollection(DB_NAME, template.getCollectionName(Person.class), client); + + durzo = new Person("Durzo", "Blint", 700); + kylar = new Person("Kylar", "Stern", 21); + vi = new Person("Viridiana", "Sovari", 20); + + all = repository.saveAll(Arrays.asList(durzo, kylar, vi)); + } + + @AfterTransaction + public void verifyDbState() throws InterruptedException { + + MongoCollection collection = client.getDatabase(DB_NAME) + .getCollection(template.getCollectionName(Person.class)); + + assertionList.forEach(it -> { + + boolean isPresent = collection.find(Filters.eq("_id", new ObjectId(it.getId().toString()))).limit(1).iterator() + .hasNext(); + + assertThat(isPresent).isEqualTo(it.shouldBePresent()) + .withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(), + it.shouldBePresent() ? "be present" : "NOT be present")); + }); + } + + @Rollback(false) + @Test // DATAMONGO-1920 + public void shouldHonorCommitForDerivedQuery() { + + repository.removePersonByLastnameUsingAnnotatedQuery(durzo.getLastname()); + + assertAfterTransaction(durzo).isNotPresent(); + } + + @Rollback(false) + @Test // DATAMONGO-1920 + public void shouldHonorCommit() { + + Person hu = new Person("Hu", "Gibbet", 43); + + repository.save(hu); + + assertAfterTransaction(hu).isPresent(); + } + + @Test // DATAMONGO-1920 + public void shouldHonorRollback() { + + Person hu = new Person("Hu", "Gibbet", 43); + + repository.save(hu); + + assertAfterTransaction(hu).isNotPresent(); + } + + private AfterTransactionAssertion assertAfterTransaction(Person person) { + + AfterTransactionAssertion assertion = new AfterTransactionAssertion(new Persistable() { + + @Nullable + @Override + public Object getId() { + return person.id; + } + + @Override + public boolean isNew() { + return person.id != null; + } + }); + + assertionList.add(assertion); + return assertion; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/AfterTransactionAssertion.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/AfterTransactionAssertion.java new file mode 100644 index 000000000..f043ddfd7 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/AfterTransactionAssertion.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.test.util; + +import lombok.Data; + +import org.springframework.data.domain.Persistable; + +/** + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + */ +@Data +public class AfterTransactionAssertion { + + private final T persistable; + private boolean presentAfterTransaction; + + public void isPresent() { + presentAfterTransaction = true; + } + + public void isNotPresent() { + presentAfterTransaction = false; + } + + public Object getId() { + return persistable.getId(); + } + + public boolean shouldBePresent() { + return presentAfterTransaction; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java new file mode 100644 index 000000000..023665b41 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.test.util; + +import org.bson.Document; + +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; + +/** + * @author Christoph Strobl + */ +public class MongoTestUtils { + + /** + * Create a {@link com.mongodb.client.MongoCollection} if it does not exist, or drop and recreate it if it does. + * + * @param dbName must not be {@literal null}. + * @param collectionName must not be {@literal null}. + * @param client must not be {@literal null}. + */ + public static MongoCollection createOrReplaceCollection(String dbName, String collectionName, + MongoClient client) { + + MongoDatabase database = client.getDatabase(dbName); + + boolean collectionExists = database.listCollections().filter(new Document("name", collectionName)).first() != null; + + if (collectionExists) { + database.getCollection(collectionName).drop(); + } + + database.createCollection(collectionName); + + try { + Thread.sleep(10); // server replication time + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return database.getCollection(collectionName); + } + + /** + * Create a new {@link MongoClient} with defaults suitable for replica set usage. + * + * @return new instance of {@link MongoClient}. + */ + public static MongoClient replSetClient() { + + return new MongoClient("localhost", + MongoClientOptions.builder().requiredReplicaSetName("rs0").build()); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoVersionRule.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoVersionRule.java index cd124bc62..697c82561 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoVersionRule.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoVersionRule.java @@ -128,12 +128,20 @@ public class MongoVersionRule implements TestRule { Version maxVersion = MongoVersionRule.this.maxVersion.equals(ANY) ? DEFAULT_HIGH : MongoVersionRule.this.maxVersion; - if (MongoVersionRule.this.minVersion.equals(ANY) && MongoVersionRule.this.maxVersion.equals(ANY)) { + if (description.getAnnotation(MongoVersion.class) != null) { MongoVersion version = description.getAnnotation(MongoVersion.class); if (version != null) { - minVersion = Version.parse(version.asOf()); - maxVersion = Version.parse(version.until()); + + Version tmpMinVersion = Version.parse(version.asOf()); + if (!tmpMinVersion.equals(ANY) && !tmpMinVersion.equals(DEFAULT_LOW)) { + minVersion = tmpMinVersion; + } + + Version tmpMaxVersion = Version.parse(version.until()); + if (!tmpMaxVersion.equals(ANY) && !tmpMaxVersion.equals(DEFAULT_HIGH)) { + maxVersion = tmpMaxVersion; + } } } diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 155dccdf3..5e8f2cc79 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -27,7 +27,7 @@ include::{spring-data-commons-docs}/repositories.adoc[] include::reference/introduction.adoc[] include::reference/mongodb.adoc[] include::reference/reactive-mongodb.adoc[] -include::reference/client-session.adoc[] +include::reference/client-session-transactions.adoc[] include::reference/mongo-repositories.adoc[] include::reference/reactive-mongo-repositories.adoc[] include::{spring-data-commons-docs}/auditing.adoc[] diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 47edadc08..dc6ffd35a 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -10,7 +10,8 @@ * <> for queries and collection creation. * <> for imperative and reactive drivers. * Tailable cursors for imperative driver. -* <> support for the imperative and reactive Template API. +* <> support for the imperative and reactive Template API. +* <> support and MongoDB specific transaction manager implementation. [[new-features.2-0-0]] == What's new in Spring Data MongoDB 2.0 diff --git a/src/main/asciidoc/reference/client-session-transactions.adoc b/src/main/asciidoc/reference/client-session-transactions.adoc new file mode 100644 index 000000000..0112712c0 --- /dev/null +++ b/src/main/asciidoc/reference/client-session-transactions.adoc @@ -0,0 +1,200 @@ +[[mongo.sessions]] += MongoDB Sessions + +As of version 3.6 MongoDB supports a concept of Sessions. The use of sessions enables MongoDBs https://docs.mongodb.com/manual/core/read-isolation-consistency-recency/#causal-consistency[Causal Consistency] model guaranteeing to execute operations in an order that respect their causal relationships. Those are split into ``ServerSession``s and ``ClientSession``s. In the following when we speak of session we refer to `ClientSession`. + +WARNING: Operations within a client session are not isolated from operations outside the session. + +Both `MongoOperations` and `ReactiveMongoOperations` provide gateway methods for tying a `ClientSession` to the operations themselves. `MongoCollection` and `MongoDatabase` use session proxy objects implementing MongoDB's collection and and database interfaces so there's no need to add a session on each call. This means that a potential call to `MongoCollection#find()` is delegated to `MongoCollection#find(ClientSession)`. + +NOTE: Methods like `(Reactive)MongoOperations#getCollection` returning native MongoDB Java Driver gateway objects, such as `MongoCollection`, that themselves offer dedicated methods for `ClientSession` are *NOT* be session-proxied. So make sure to provide the `ClientSession` where needed when interacting directly with a `MongoCollection` or `MongoDatabase` and not via one of the `#execute` callbacks on `MongoOperations`. + +.ClientSession with `MongoOperations` +==== +[source,java] +---- +ClientSessionOptions sessionOptions = ClientSessionOptions.builder() + .causallyConsistent(true) + .build(); + +ClientSession session = client.startSession(sessionOptions); <1> + +template.withSession(() -> session) + .execute(action -> { + + Query query = query(where("name").is("Durzo Blint")); + Person durzo = action.findOne(query, Person.class); <2> + + Person azoth = new Person("Kylar Stern"); + azoth.setMaster(durzo); + + action.insert(azoth); <2> + + return azoth; + }); + +session.close() <4> +---- +<1> Obtain a new session from the server. +<2> Use `MongoOperation` methods as before. The `ClientSession` gets applied automatically. +<3> Make sure to close the `ClientSession`. +==== + +WARNING: When dealing with ``DBRef``s, especially lazily loaded ones, it is essential to **not** close the `ClientSession` before all data is loaded. Otherwise, lazy fetch fails. + +The reactive counterpart uses the very same building blocks as the imperative one. + +.ClientSession with `ReactiveMongoOperations` +==== +[source,java] +---- +ClientSessionOptions sessionOptions = ClientSessionOptions.builder() + .causallyConsistent(true) + .build(); + +Publisher session = client.startSession(sessionOptions); <1> + +template.withSession(session) + .execute(action -> { + + Query query = query(where("name").is("Durzo Blint")); + return action.findOne(query, Person.class) + .flatMap(durzo -> { + + Person azoth = new Person("Kylar Stern"); + azoth.setMaster(durzo); + + return action.insert(azoth); <2> + }); + }, ClientSession::close) <3> + .subscribe(); +---- +<1> Obtain a `Publisher` for new session retrieval. +<2> Use `ReactiveMongoOperation` methods as before. The `ClientSession` is obtained and applied automatically. +<3> Make sure to close the `ClientSession`. +==== + +By using a `Publisher` providing the actual session you can defer session acquisition to the point of actual subscription. +Still you need to close the session when done in order to not pollute the server with stale sessions. Use the `doFinally` hook on `execute` to call `ClientSession#close()` when you don't need the session any more. +In case you prefer having more control over the session itself, you can always obtain the `ClientSession` via the driver and provide it via a `Supplier`. + + +[[mongo.transactions]] += MongoDB Transactions + +As of version 4 MongoDB supports https://www.mongodb.com/transactions[Transactions]. Transactions are built on top of <> and therefore require an active `ClientSession`. + +NOTE: By default, unless you specify a `MongoTransactionManager` within your application context, transaction support is **DISABLED**. You may use `setSessionSynchronization(ANY)` 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`. + +.Programmatic transactions +==== +[source,java] +---- +ClientSession session = client.startSession(options); <1> + +template.withSession(session) + .execute(action -> { + + session.startTransaction(); <2> + + try { + + Step step = // ...; + action.insert(step); + + process(step); + + action.update(Step.class).apply(Update.set("state", // ... + + session.commitTransaction(); <3> + + } catch (RuntimeException e) { + session.abortTransaction(); <4> + } + }, ClientSession::close) <5> + .subscribe(); +---- +<1> Obtain a new `ClientSession`. +<2> Start the transaction. +<3> If everything works out as expected, go on and commit the changes. +<4> Something broke, just roll back everything. +<5> Do not forget to close the session when done. +==== + +The above example allows you to have full control over transactional behavior while using the session scoped `MongoOperations` instance within the callback to ensure the session is passed on to each and every server call. +To avoid some of the overhead that comes with this approach usage of a `TransactionTemplate` can take away some of the noise of manual transaction flow. + +== Transactions with TransactionTemplate + +.Transactions with TransactionTemplate +==== +[source,java] +---- +template.setSessionSynchronization(ANY); <1> + +// ... + +TransactionTemplate txTemplate = new TransactionTemplate(anyTxManager); <2> + +txTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { <3> + + Step step = // ...; + template.insert(step); + + process(step); + + template.update(Step.class).apply(Update.set("state", // ... + }; +}); +---- +<1> Manually enable transaction synchronization. +<2> Create the `TransactionTemplate` using the provided `PlatformTransactionManager`. +<3> Within the callback the `ClientSession` and transaction are already registered. +==== + +== Transactions with MongoTransactionManager + +`MongoTransactionManager` is the gateway to the well known Spring transaction support. It allows applications to use http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/html/transaction.html[managed transaction features of Spring]. +The `MongoTransactionManager` binds a `ClientSession` to the thread. `MongoTemplate` automatically detects those and operates on them accordingly. `MongoTemplate` can also participate in other, ongoing transactions. + +.Transactions with MongoTransactionManager +==== +[source,java] +---- +@Configuration +static class Config extends AbstractMongoConfiguration { + + @Bean + MongoTransactionManager transactionManager(MongoDbFactory dbFactory) { <1> + return new MongoTransactionManager(dbFactory); + } + + // ... +} + +@Component +public class StateService { + + @Transactional + void someBusinessFunction(Step step) { <2> + + template.insert(step); + + process(step); + + template.update(Step.class).apply(Update.set("state", // ... + }; +}); + +---- +<1> Register `MongoTransactionManager` in the application context. +<2> Mark methods as transactional. +==== + +NOTE: `@Transactional(readOnly = true)` advises the `MongoTransactionManager` to also start a transaction adding the + `ClientSession` to outgoing requests. \ No newline at end of file diff --git a/src/main/asciidoc/reference/client-session.adoc b/src/main/asciidoc/reference/client-session.adoc deleted file mode 100644 index bbd9ce784..000000000 --- a/src/main/asciidoc/reference/client-session.adoc +++ /dev/null @@ -1,79 +0,0 @@ -[[mongo.sessions]] -= MongoDB Sessions - -As of version 3.6 MongoDB supports a concept of Sessions. The use of sessions enables MongoDBs https://docs.mongodb.com/manual/core/read-isolation-consistency-recency/#causal-consistency[Causal Consistency] model guaranteeing to execute operations in an order that respect their causal relationships. Those are split into ``ServerSession``s and ``ClientSession``s. In the following when we speak of session we refer to `ClientSession`. - -WARNING: Operations within a client session are not isolated from operations outside the session. - -Both `MongoOperations` and `ReactiveMongoOperations` provide gateway methods for tying a `ClientSession` to the operations themselves. `MongoCollection` and `MongoDatabase` use session proxy objects implementing MongoDB's collection and and database interfaces so there's no need to add a session on each call. This means that a potential call to `MongoCollection#find()` is delegated to `MongoCollection#find(ClientSession)`. - -NOTE: Methods like `(Reactive)MongoOperations#getCollection` returning native MongoDB Java Driver gateway objects, such as `MongoCollection`, that themselves offer dedicated methods for `ClientSession` are *NOT* be session-proxied. So make sure to provide the `ClientSession` where needed when interacting directly with a `MongoCollection` or `MongoDatabase` and not via one of the `#execute` callbacks on `MongoOperations`. - -.ClientSession with `MongoOperations` -==== -[source,java] ----- -ClientSessionOptions sessionOptions = ClientSessionOptions.builder() - .causallyConsistent(true) - .build(); - -ClientSession session = client.startSession(sessionOptions); <1> - -template.withSession(() -> session) - .execute(action -> { - - Query query = query(where("name").is("Durzo Blint")); - Person durzo = action.findOne(query, Person.class); <2> - - Person azoth = new Person("Kylar Stern"); - azoth.setMaster(durzo); - - action.insert(azoth); <2> - - return azoth; - }); - -session.close() <4> ----- -<1> Obtain a new session from the server. -<2> Use `MongoOperation` methods as before. The `ClientSession` gets applied automatically. -<3> Make sure to close the `ClientSession`. -==== - -WARNING: When dealing with ``DBRef``s, especially lazily loaded ones, it is essential to **not** close the `ClientSession` before all data is loaded. Otherwise, lazy fetch fails. - -The reactive counterpart uses the very same building blocks as the imperative one. - -.ClientSession with `ReactiveMongoOperations` -==== -[source,java] ----- -ClientSessionOptions sessionOptions = ClientSessionOptions.builder() - .causallyConsistent(true) - .build(); - -Publisher session = client.startSession(sessionOptions); <1> - -template.withSession(session) - .execute(action -> { - - Query query = query(where("name").is("Durzo Blint")); - return action.findOne(query, Person.class) - .flatMap(durzo -> { - - Person azoth = new Person("Kylar Stern"); - azoth.setMaster(durzo); - - return action.insert(azoth); <2> - }); - }, ClientSession::close) <3> - .subscribe(); ----- -<1> Obtain a `Publisher` for new session retrieval. -<2> Use `ReactiveMongoOperation` methods as before. The `ClientSession` is obtained and applied automatically. -<3> Make sure to close the `ClientSession`. -==== - -By using a `Publisher` providing the actual session you can defer session acquisition to the point of actual subscription. -Still you need to close the session when done in order to not pollute the server with stale sessions. Use the `doFinally` hook on `execute` to call `ClientSession#close()` when you don't need the session any more. -In case you prefer having more control over the session itself, you can always obtain the `ClientSession` via the driver and provide it via a `Supplier`.