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 extends ClientSession> 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 extends ClientSession> 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 extends ClientSession> 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 extends ClientSession> 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`.