DATAMONGO-1920 - Add support for MongoDB 4.0 transactions (synchronous driver).

MongoTransactionManager is the gateway to the well known Spring transaction support. It allows applications to use managed transaction features of Spring.
The MongoTransactionManager binds a ClientSession to the thread. MongoTemplate automatically detects those and operates on them accordingly.

static class Config extends AbstractMongoConfiguration {

	// ...

	@Bean
	MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
		return new MongoTransactionManager(dbFactory);
	}
}

@Component
public class StateService {

	@Transactional
	void someBusinessFunction(Step step) {

		template.insert(step);

		process(step);

		template.update(Step.class).apply(update.set("state", // ...
	};
});

Original pull request: #554.
This commit is contained in:
Christoph Strobl
2018-04-03 14:11:30 +02:00
committed by Mark Paluch
parent 6fbb7cec22
commit 4cd2935087
39 changed files with 2411 additions and 163 deletions

View File

@@ -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:

View File

@@ -28,8 +28,8 @@
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>2.1.0.BUILD-SNAPSHOT</springdata.commons>
<mongo>3.6.3</mongo>
<mongo.reactivestreams>1.7.1</mongo.reactivestreams>
<mongo>3.8.0-beta1</mongo>
<mongo.reactivestreams>1.8.0</mongo.reactivestreams>
<jmh.version>1.19</jmh.version>
</properties>

View File

@@ -253,6 +253,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
<!-- Kotlin extension -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>

View File

@@ -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.
* <p />
* <strong>Note:</strong> 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}.
* <p />
* 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}.
* <p />
* 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}.
* <p />
* 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}.
* <p />
* 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<MongoResourceHolder, Object> {
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();
}
}
}

View File

@@ -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.

View File

@@ -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.
* <p />
* <strong>Note:</strong> 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;
}
}

View File

@@ -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);
}

View File

@@ -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}.
* <p />
* Binds a {@link ClientSession} from the specified {@link MongoDbFactory} to the thread.
* <p />
* {@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 <a href="https://www.mongodb.com/transactions">MongoDB Transaction Documentation</a>
*/
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.
* <p />
* <strong>Note:</strong>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}.
* <p />
* 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();
}
}
}

View File

@@ -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.

View File

@@ -57,6 +57,7 @@ public class SessionAwareMethodInterceptor<D, C> 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<D, C> implements MethodInterceptor {
* {@code MongoCollection}.
* @param <T> target object type.
*/
public <T> SessionAwareMethodInterceptor(ClientSession session, T target, Class<D> databaseType,
ClientSessionOperator<D> databaseDecorator, Class<C> collectionType,
public <T> SessionAwareMethodInterceptor(ClientSession session, T target, Class<? extends ClientSession> sessionType,
Class<D> databaseType, ClientSessionOperator<D> databaseDecorator, Class<C> collectionType,
ClientSessionOperator<C> 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<D, C> implements MethodInterceptor {
this.databaseDecorator = databaseDecorator;
this.targetType = ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseType : collectionType;
this.sessionType = sessionType;
}
/*
@@ -114,7 +117,7 @@ public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
return methodInvocation.proceed();
}
Optional<Method> targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType);
Optional<Method> 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<D, C> implements MethodInterceptor {
* @param targetClass
* @return
*/
Optional<Method> lookup(Method method, Class<?> targetClass) {
Optional<Method> 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);

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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. <br/>
* <strong>NOTE</strong> The entire list of documents will be fetched before sending the actual delete commands.

View File

@@ -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);
}
/*

View File

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

View File

@@ -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}.<br />
* <strong>NOTE:</strong> 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<Document> 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 <T> DeleteResult doRemove(final String collectionName, final Query query,
@Nullable final Class<T> entityClass) {
@Nullable final Class<T> 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<Document> collectionToUse = writeConcernToUse != null
? collection.withWriteConcern(writeConcernToUse) : collection;
DeleteResult result = multi ? collectionToUse.deleteMany(removeQuery, options)
: collection.deleteOne(removeQuery, options);
maybeEmitEvent(new AfterDeleteEvent<T>(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

View File

@@ -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);

View File

@@ -25,6 +25,7 @@ import org.springframework.lang.Nullable;
* @since 2.1
* @see com.mongodb.session.ClientSession
*/
@FunctionalInterface
public interface SessionCallback<T> {
/**

View File

@@ -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}.

View File

@@ -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> T createProxyInstance(ClientSession session, T target, Class<T> targetType) {
private <T> T createProxyInstance(com.mongodb.session.ClientSession session, T target, Class<T> 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());

View File

@@ -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());

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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> T createProxyInstance(ClientSession session, T target, Class<T> targetType) {
private <T> T createProxyInstance(com.mongodb.session.ClientSession session, T target, Class<T> 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());
}

View File

@@ -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;
}
}

View File

@@ -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<AfterTransactionAssertion<Persistable<String>>> 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<Document> 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<String> {
@Id String id;
String name;
@Override
public boolean isNew() {
return id == null;
}
}
}

View File

@@ -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<DeleteOptions> 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<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
ArgumentCaptor<ReplaceOptions> options = ArgumentCaptor.forClass(ReplaceOptions.class);
verify(collection).replaceOne(any(), any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));

View File

@@ -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<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
ArgumentCaptor<ReplaceOptions> options = ArgumentCaptor.forClass(ReplaceOptions.class);
verify(collection).replaceOne(any(Bson.class), any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));

View File

@@ -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}.

View File

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

View File

@@ -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}.

View File

@@ -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<Person> all;
List<org.springframework.data.mongodb.test.util.AfterTransactionAssertion<Persistable<String>>> 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<Document> 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;
}
}

View File

@@ -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<T extends Persistable> {
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;
}
}

View File

@@ -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<Document> 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());
}
}

View File

@@ -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;
}
}
}

View File

@@ -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[]

View File

@@ -10,7 +10,8 @@
* <<mongo.jsonSchema,`$jsonSchema` support>> for queries and collection creation.
* <<change-streams, Change Stream support>> for imperative and reactive drivers.
* Tailable cursors for imperative driver.
* <<mongo.sessions, MongoDB Session>> support for the imperative and reactive Template API.
* <<mongo.sessions, MongoDB 3.6 Session>> support for the imperative and reactive Template API.
* <<mongo.transactions, MongoDB 4.0 Transaction>> support and MongoDB specific transaction manager implementation.
[[new-features.2-0-0]]
== What's new in Spring Data MongoDB 2.0

View File

@@ -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<ClientSession> 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 <<mongo.sessions,Sessions>> 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.

View File

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