#107 - Add support for ConnectionFactoryTransactionManager.ConnectionFactoryTransactionManager.

We now support R2DBC transaction management through ConnectionFactoryTransactionManager which is a ReactiveTransactionManager implementation to be used with TransactionalOperator and Spring's declarative transaction management.

ConnectionFactoryTransactionManager tm = new ConnectionFactoryTransactionManager(connectionFactory);
TransactionalOperator operator = TransactionalOperator.create(tm);
DatabaseClient db = DatabaseClient.create(connectionFactory);

Mono<Void> atomicOperation = db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
            .bind("id", "joe")
            .bind("name", "Joe")
            .bind("age", 34)
            .fetch().rowsUpdated()
            .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)")
                    .bind("id", "joe")
                    .bind("name", "Joe")
                    .fetch().rowsUpdated())
            .then()
            .as(operator::transactional);

Original Pull Request: #107
This commit is contained in:
Mark Paluch
2019-03-18 08:19:05 +01:00
committed by Christoph Strobl
parent da53a9a934
commit 79e32941b5
28 changed files with 2078 additions and 96 deletions

View File

@@ -25,6 +25,7 @@ import io.r2dbc.spi.Statement;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
@@ -55,6 +56,7 @@ import org.springframework.data.r2dbc.domain.BindableOperation;
import org.springframework.data.r2dbc.domain.OutboundRow;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils;
import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy;
import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper;
import org.springframework.data.r2dbc.function.query.Criteria;
@@ -186,7 +188,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
* @return a {@link Mono} able to emit a {@link Connection}.
*/
protected Mono<Connection> getConnection() {
return Mono.from(obtainConnectionFactory().create());
return ConnectionFactoryUtils.getConnection(obtainConnectionFactory()).map(Tuple2::getT1);
}
/**
@@ -196,7 +198,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
* @return a {@link Publisher} that completes successfully when the connection is closed.
*/
protected Publisher<Void> closeConnection(Connection connection) {
return connection.close();
return ConnectionFactoryUtils.currentConnectionFactory(obtainConnectionFactory()).then()
.onErrorResume(Exception.class, e -> Mono.from(connection.close()));
}
/**

View File

@@ -25,6 +25,7 @@ import reactor.util.function.Tuple2;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils;
import org.springframework.data.r2dbc.function.connectionfactory.ReactiveTransactionSynchronization;
import org.springframework.data.r2dbc.function.connectionfactory.TransactionResources;
@@ -106,24 +107,6 @@ class DefaultTransactionalDatabaseClient extends DefaultDatabaseClient implement
return ConnectionFactoryUtils.getConnection(obtainConnectionFactory()).map(Tuple2::getT1);
}
/* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.DefaultDatabaseClient#closeConnection(io.r2dbc.spi.Connection)
*/
@Override
protected Publisher<Void> closeConnection(Connection connection) {
return Mono.subscriberContext().flatMap(context -> {
if (context.hasKey(ReactiveTransactionSynchronization.class)) {
return ConnectionFactoryUtils.currentConnectionFactory()
.flatMap(it -> ConnectionFactoryUtils.releaseConnection(connection, it));
}
return Mono.from(connection.close());
});
}
/**
* Execute a transactional cleanup. Also, deregister the current {@link TransactionResources synchronization} element.
*/

View File

@@ -23,8 +23,10 @@ import java.util.function.Consumer;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.springframework.data.r2dbc.function.connectionfactory.TransactionResources;
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
import org.springframework.transaction.reactive.TransactionalOperator;
import org.springframework.util.Assert;
/**
@@ -77,7 +79,9 @@ import org.springframework.util.Assert;
* @see org.springframework.data.r2dbc.function.connectionfactory.ReactiveTransactionSynchronization
* @see TransactionResources
* @see org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils
* @deprecated Use {@link DatabaseClient} in combination with {@link TransactionalOperator}.
*/
@Deprecated
public interface TransactionalDatabaseClient extends DatabaseClient {
/**

View File

@@ -0,0 +1,493 @@
/*
* Copyright 2019 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.r2dbc.function.connectionfactory;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.IsolationLevel;
import io.r2dbc.spi.Result;
import reactor.core.publisher.Mono;
import java.time.Duration;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.r2dbc.function.DatabaseClient;
import org.springframework.lang.Nullable;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.reactive.AbstractReactiveTransactionManager;
import org.springframework.transaction.reactive.GenericReactiveTransaction;
import org.springframework.transaction.reactive.TransactionSynchronizationManager;
import org.springframework.util.Assert;
/**
* {@link org.springframework.transaction.ReactiveTransactionManager} implementation for a single R2DBC
* {@link ConnectionFactory}. This class is capable of working in any environment with any R2DBC driver, as long as the
* setup uses a {@link ConnectionFactory} as its {@link Connection} factory mechanism. Binds a R2DBC {@link Connection}
* from the specified {@link ConnectionFactory} to the current subscriber context, potentially allowing for one
* context-bound {@link Connection} per {@link ConnectionFactory}.
* <p>
* <b>Note: The {@link ConnectionFactory} that this transaction manager operates on needs to return independent
* {@link Connection}s.</b> The {@link Connection}s may come from a pool (the typical case), but the
* {@link ConnectionFactory} must not return scoped scoped {@link Connection}s or the like. This transaction manager
* will associate {@link Connection} with context-bound transactions itself, according to the specified propagation
* behavior. It assumes that a separate, independent {@link Connection} can be obtained even during an ongoing
* transaction.
* <p>
* Application code is required to retrieve the R2DBC Connection via
* {@link ConnectionFactoryUtils#getConnection(ConnectionFactory)} instead of a standard R2DBC-style
* {@link ConnectionFactory#create()} call. Spring classes such as {@link DatabaseClient} use this strategy implicitly.
* If not used in combination with this transaction manager, the {@link ConnectionFactoryUtils} lookup strategy behaves
* exactly like the native {@link ConnectionFactory} lookup; it can thus be used in a portable fashion.
* <p>
* Alternatively, you can allow application code to work with the standard R2DBC lookup pattern
* {@link ConnectionFactory#create()}, for example for code that is not aware of Spring at all. In that case, define a
* {@link TransactionAwareConnectionFactoryProxy} for your target {@link ConnectionFactory}, and pass that proxy
* {@link ConnectionFactory} to your DAOs, which will automatically participate in Spring-managed transactions when
* accessing it.
* <p>
* This transaction manager triggers flush callbacks on registered transaction synchronizations (if synchronization is
* generally active), assuming resources operating on the underlying R2DBC {@link Connection}.
*
* @author Mark Paluch
* @see ConnectionFactoryUtils#getConnection(ConnectionFactory)
* @see ConnectionFactoryUtils#releaseConnection
* @see TransactionAwareConnectionFactoryProxy
* @see DatabaseClient
*/
public class ConnectionFactoryTransactionManager extends AbstractReactiveTransactionManager
implements InitializingBean {
private ConnectionFactory connectionFactory;
private boolean enforceReadOnly = false;
/**
* Create a new @link ConnectionFactoryTransactionManager} instance. A ConnectionFactory has to be set to be able to
* use it.
*
* @see #setConnectionFactory
*/
public ConnectionFactoryTransactionManager() {}
/**
* Create a new {@link ConnectionFactoryTransactionManager} instance.
*
* @param connectionFactory the R2DBC ConnectionFactory to manage transactions for
*/
public ConnectionFactoryTransactionManager(ConnectionFactory connectionFactory) {
this();
setConnectionFactory(connectionFactory);
afterPropertiesSet();
}
/**
* Set the R2DBC {@link ConnectionFactory} that this instance should manage transactions for.
* <p>
* This will typically be a locally defined {@link ConnectionFactory}, for example an connection pool.
* <p>
* The {@link ConnectionFactory} specified here should be the target {@link ConnectionFactory} to manage transactions
* for, not a TransactionAwareConnectionFactoryProxy. Only data access code may work with
* TransactionAwareConnectionFactoryProxy, while the transaction manager needs to work on the underlying target
* {@link ConnectionFactory}. If there's nevertheless a TransactionAwareConnectionFactoryProxy passed in, it will be
* unwrapped to extract its target {@link ConnectionFactory}.
* <p>
* <b>The {@link ConnectionFactory} passed in here needs to return independent {@link Connection}s.</b> The
* {@link Connection}s may come from a pool (the typical case), but the {@link ConnectionFactory} must not return
* scoped {@link Connection} or the like.
*
* @see TransactionAwareConnectionFactoryProxy
*/
public void setConnectionFactory(@Nullable ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
/**
* Return the R2DBC {@link ConnectionFactory} that this instance manages transactions for.
*/
@Nullable
public ConnectionFactory getConnectionFactory() {
return this.connectionFactory;
}
/**
* Obtain the {@link ConnectionFactory} for actual use.
*
* @return the {@link ConnectionFactory} (never {@code null})
* @throws IllegalStateException in case of no ConnectionFactory set
*/
protected ConnectionFactory obtainConnectionFactory() {
ConnectionFactory connectionFactory = getConnectionFactory();
Assert.state(connectionFactory != null, "No ConnectionFactory set");
return connectionFactory;
}
/**
* Specify whether to enforce the read-only nature of a transaction (as indicated by
* {@link TransactionDefinition#isReadOnly()} through an explicit statement on the transactional connection: "SET
* TRANSACTION READ ONLY" as understood by Oracle, MySQL and Postgres.
* <p>
* The exact treatment, including any SQL statement executed on the connection, can be customized through through
* {@link #prepareTransactionalConnection}.
*
* @see #prepareTransactionalConnection
*/
public void setEnforceReadOnly(boolean enforceReadOnly) {
this.enforceReadOnly = enforceReadOnly;
}
/**
* Return whether to enforce the read-only nature of a transaction through an explicit statement on the transactional
* connection.
*
* @see #setEnforceReadOnly
*/
public boolean isEnforceReadOnly() {
return this.enforceReadOnly;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
if (getConnectionFactory() == null) {
throw new IllegalArgumentException("Property 'connectionFactory' is required");
}
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doGetTransaction(org.springframework.transaction.reactive.TransactionSynchronizationManager)
*/
@Override
protected Object doGetTransaction(TransactionSynchronizationManager synchronizationManager)
throws TransactionException {
ConnectionFactoryTransactionObject txObject = new ConnectionFactoryTransactionObject();
ConnectionHolder conHolder = (ConnectionHolder) synchronizationManager.getResource(obtainConnectionFactory());
txObject.setConnectionHolder(conHolder, false);
return txObject;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#isExistingTransaction(java.lang.Object)
*/
@Override
protected boolean isExistingTransaction(Object transaction) {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doBegin(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object, org.springframework.transaction.TransactionDefinition)
*/
@Override
protected Mono<Void> doBegin(TransactionSynchronizationManager synchronizationManager, Object transaction,
TransactionDefinition definition) throws TransactionException {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
return Mono.defer(() -> {
Mono<Connection> connection = null;
if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Mono<Connection> newCon = Mono.from(obtainConnectionFactory().create());
connection = newCon.doOnNext(it -> {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Acquired Connection [" + newCon + "] for R2DBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(it), true);
});
} else {
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
connection = Mono.just(txObject.getConnectionHolder().getConnection());
}
return connection.flatMap(con -> {
return prepareTransactionalConnection(con, definition).then(doBegin(con, definition)).then().doOnSuccess(v -> {
txObject.getConnectionHolder().setTransactionActive(true);
Duration timeout = determineTimeout(definition);
if (!timeout.isNegative() && !timeout.isZero()) {
txObject.getConnectionHolder().setTimeoutInMillis(timeout.toMillis());
}
// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
synchronizationManager.bindResource(obtainConnectionFactory(), txObject.getConnectionHolder());
}
}).thenReturn(con).onErrorResume(e -> {
CannotCreateTransactionException ex = new CannotCreateTransactionException(
"Could not open R2DBC Connection for transaction", e);
if (txObject.isNewConnectionHolder()) {
return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()).doOnTerminate(() -> {
txObject.setConnectionHolder(null, false);
}).then(Mono.error(ex));
}
return Mono.error(ex);
});
});
}).then();
}
private Mono<Void> doBegin(Connection con, TransactionDefinition definition) {
Mono<Void> doBegin = Mono.from(con.beginTransaction());
if (definition != null && definition.getIsolationLevel() != -1) {
IsolationLevel isolationLevel = resolveIsolationLevel(definition.getIsolationLevel());
if (isolationLevel != null) {
if (this.logger.isDebugEnabled()) {
this.logger
.debug("Changing isolation level of R2DBC Connection [" + con + "] to " + definition.getIsolationLevel());
}
doBegin = doBegin.then(Mono.from(con.setTransactionIsolationLevel(isolationLevel)));
}
}
return doBegin;
}
/**
* Determine the actual timeout to use for the given definition. Will fall back to this manager's default timeout if
* the transaction definition doesn't specify a non-default value.
*
* @param definition the transaction definition
* @return the actual timeout to use
* @see org.springframework.transaction.TransactionDefinition#getTimeout()
*/
protected Duration determineTimeout(TransactionDefinition definition) {
if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) {
return Duration.ofSeconds(definition.getTimeout());
}
return Duration.ZERO;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSuspend(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object)
*/
@Override
protected Mono<Object> doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction)
throws TransactionException {
return Mono.defer(() -> {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
txObject.setConnectionHolder(null);
return Mono.justOrEmpty(synchronizationManager.unbindResource(obtainConnectionFactory()));
});
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doResume(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object, java.lang.Object)
*/
@Override
protected Mono<Void> doResume(TransactionSynchronizationManager synchronizationManager, Object transaction,
Object suspendedResources) throws TransactionException {
return Mono.defer(() -> {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
txObject.setConnectionHolder(null);
synchronizationManager.bindResource(obtainConnectionFactory(), suspendedResources);
return Mono.empty();
});
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doCommit(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction)
*/
@Override
protected Mono<Void> doCommit(TransactionSynchronizationManager TransactionSynchronizationManager,
GenericReactiveTransaction status) throws TransactionException {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction();
Connection connection = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
this.logger.debug("Committing R2DBC transaction on Connection [" + connection + "]");
}
return Mono.from(connection.commitTransaction())
.onErrorMap(ex -> new TransactionSystemException("Could not commit R2DBC transaction", ex));
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doRollback(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction)
*/
@Override
protected Mono<Void> doRollback(TransactionSynchronizationManager TransactionSynchronizationManager,
GenericReactiveTransaction status) throws TransactionException {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction();
Connection connection = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
this.logger.debug("Rolling back R2DBC transaction on Connection [" + connection + "]");
}
return Mono.from(connection.rollbackTransaction())
.onErrorMap(ex -> new TransactionSystemException("Could not roll back R2DBC transaction", ex));
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSetRollbackOnly(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction)
*/
@Override
protected Mono<Void> doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager,
GenericReactiveTransaction status) throws TransactionException {
return Mono.fromRunnable(() -> {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction();
if (status.isDebug()) {
this.logger
.debug("Setting R2DBC transaction [" + txObject.getConnectionHolder().getConnection() + "] rollback-only");
}
txObject.setRollbackOnly();
});
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doCleanupAfterCompletion(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object)
*/
@Override
protected Mono<Void> doCleanupAfterCompletion(TransactionSynchronizationManager synchronizationManager,
Object transaction) {
return Mono.defer(() -> {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
// Remove the connection holder from the context, if exposed.
if (txObject.isNewConnectionHolder()) {
synchronizationManager.unbindResource(obtainConnectionFactory());
}
// Reset connection.
Connection con = txObject.getConnectionHolder().getConnection();
try {
if (txObject.isNewConnectionHolder()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Releasing R2DBC Connection [" + con + "] after transaction");
}
return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory());
}
} finally {
txObject.getConnectionHolder().clear();
}
return Mono.empty();
});
}
/**
* Prepare the transactional {@link Connection} right after transaction begin.
* <p>
* The default implementation executes a "SET TRANSACTION READ ONLY" statement if the {@link #setEnforceReadOnly
* "enforceReadOnly"} flag is set to {@code true} and the transaction definition indicates a read-only transaction.
* <p>
* The "SET TRANSACTION READ ONLY" is understood by Oracle, MySQL and Postgres and may work with other databases as
* well. If you'd like to adapt this treatment, override this method accordingly.
*
* @param con the transactional R2DBC Connection
* @param definition the current transaction definition
* @see #setEnforceReadOnly
*/
protected Mono<Void> prepareTransactionalConnection(Connection con, TransactionDefinition definition) {
if (isEnforceReadOnly() && definition.isReadOnly()) {
return Mono.from(con.createStatement("SET TRANSACTION READ ONLY").execute()) //
.flatMapMany(Result::getRowsUpdated) //
.then();
}
return Mono.empty();
}
/**
* Resolve the {@link TransactionDefinition#getIsolationLevel() isolation level constant} to a R2DBC
* {@link IsolationLevel}. If you'd like to extend isolation level translation for vendor-specific
* {@link IsolationLevel}s, override this method accordingly.
*
* @param isolationLevel the isolation level to translate.
* @return the resolved isolation level. Can be {@literal null} if not resolvable or the isolation level should remain
* {@link TransactionDefinition#ISOLATION_DEFAULT default}.
* @see TransactionDefinition#getIsolationLevel()
*/
@Nullable
protected IsolationLevel resolveIsolationLevel(int isolationLevel) {
switch (isolationLevel) {
case TransactionDefinition.ISOLATION_READ_COMMITTED:
return IsolationLevel.READ_COMMITTED;
case TransactionDefinition.ISOLATION_READ_UNCOMMITTED:
return IsolationLevel.READ_UNCOMMITTED;
case TransactionDefinition.ISOLATION_REPEATABLE_READ:
return IsolationLevel.REPEATABLE_READ;
case TransactionDefinition.ISOLATION_SERIALIZABLE:
return IsolationLevel.SERIALIZABLE;
}
return null;
}
/**
* ConnectionFactory transaction object, representing a ConnectionHolder. Used as transaction object by
* ConnectionFactoryTransactionManager.
*/
private static class ConnectionFactoryTransactionObject extends R2dbcTransactionObjectSupport {
private boolean newConnectionHolder;
void setConnectionHolder(@Nullable ConnectionHolder connectionHolder, boolean newConnectionHolder) {
super.setConnectionHolder(connectionHolder);
this.newConnectionHolder = newConnectionHolder;
}
boolean isNewConnectionHolder() {
return this.newConnectionHolder;
}
void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
}
}

View File

@@ -23,9 +23,14 @@ import reactor.util.function.Tuples;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import org.springframework.core.Ordered;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.lang.Nullable;
import org.springframework.transaction.NoTransactionException;
import org.springframework.transaction.reactive.TransactionSynchronization;
import org.springframework.transaction.reactive.TransactionSynchronizationManager;
import org.springframework.util.Assert;
/**
@@ -37,10 +42,19 @@ import org.springframework.util.Assert;
*
* @author Mark Paluch
*/
public class ConnectionFactoryUtils {
public abstract class ConnectionFactoryUtils {
/**
* Order value for ReactiveTransactionSynchronization objects that clean up R2DBC Connections.
*/
public static final int CONNECTION_SYNCHRONIZATION_ORDER = 1000;
private static final Log logger = LogFactory.getLog(ConnectionFactoryUtils.class);
private ConnectionFactoryUtils() {
}
/**
* Obtain a {@link io.r2dbc.spi.Connection} from the given {@link io.r2dbc.spi.ConnectionFactory}. Translates
* exceptions into the Spring hierarchy of unchecked generic data access exceptions, simplifying calling code and
@@ -73,18 +87,70 @@ public class ConnectionFactoryUtils {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
return Mono.subscriberContext().flatMap(it -> {
return TransactionSynchronizationManager.currentTransaction().flatMap(synchronizationManager -> {
if (it.hasKey(ReactiveTransactionSynchronization.class)) {
ReactiveTransactionSynchronization synchronization = it.get(ReactiveTransactionSynchronization.class);
return obtainConnection(synchronization, connectionFactory);
ConnectionHolder conHolder = (ConnectionHolder) synchronizationManager.getResource(connectionFactory);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed R2DBC Connection from ConnectionFactory");
return fetchConnection(connectionFactory).doOnNext(conHolder::setConnection);
}
return Mono.just(conHolder.getConnection());
}
return Mono.empty();
}).switchIfEmpty(Mono.defer(() -> {
return Mono.from(connectionFactory.create()).map(it -> Tuples.of(it, connectionFactory));
}));
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching R2DBC Connection from ConnectionFactory");
Mono<Connection> con = fetchConnection(connectionFactory);
if (synchronizationManager.isSynchronizationActive()) {
return con.flatMap(it -> {
return Mono.just(it).doOnNext(conn -> {
// Use same Connection for further R2DBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(conn);
} else {
holderToUse.setConnection(conn);
}
holderToUse.requested();
synchronizationManager
.registerSynchronization(new ConnectionSynchronization(holderToUse, connectionFactory));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
synchronizationManager.bindResource(connectionFactory, holderToUse);
}
}).onErrorResume(e -> {
// Unexpected exception from external delegation call -> close Connection and rethrow.
return releaseConnection(it, connectionFactory).then(Mono.error(e));
});
});
}
return con;
}) //
.map(conn -> Tuples.of(conn, connectionFactory)) //
.onErrorResume(NoTransactionException.class, e -> {
return Mono.subscriberContext().flatMap(it -> {
if (it.hasKey(ReactiveTransactionSynchronization.class)) {
ReactiveTransactionSynchronization synchronization = it.get(ReactiveTransactionSynchronization.class);
return obtainConnection(synchronization, connectionFactory);
}
return Mono.empty();
}).switchIfEmpty(Mono.defer(() -> {
return Mono.from(connectionFactory.create()).map(it -> Tuples.of(it, connectionFactory));
}));
});
}
private static Mono<Tuple2<Connection, ConnectionFactory>> obtainConnection(
@@ -121,6 +187,24 @@ public class ConnectionFactoryUtils {
return Mono.empty();
}
/**
* Actually fetch a {@link Connection} from the given {@link ConnectionFactory}, defensively turning an unexpected
* {@code null} return value from {@link ConnectionFactory#create()} into an {@link IllegalStateException}.
*
* @param connectionFactory the {@link ConnectionFactory} to obtain {@link Connection}s from
* @return a R2DBC {@link Connection} from the given {@link ConnectionFactory} (never {@code null})
* @throws IllegalStateException if the {@link ConnectionFactory} returned a {@literal null} value.
* @see ConnectionFactory#create()
*/
private static Mono<Connection> fetchConnection(ConnectionFactory connectionFactory) {
Publisher<? extends Connection> con = connectionFactory.create();
if (con == null) {
throw new IllegalStateException("ConnectionFactory returned null from getConnection(): " + connectionFactory);
}
return Mono.from(con);
}
/**
* Close the given {@link io.r2dbc.spi.Connection}, obtained from the given {@link ConnectionFactory}, if it is not
* managed externally (that is, not bound to the thread).
@@ -149,18 +233,29 @@ public class ConnectionFactoryUtils {
public static Mono<Void> doReleaseConnection(@Nullable io.r2dbc.spi.Connection con,
@Nullable ConnectionFactory connectionFactory) {
if (connectionFactory instanceof SingletonConnectionFactory) {
return TransactionSynchronizationManager.currentTransaction().flatMap(it -> {
SingletonConnectionFactory factory = (SingletonConnectionFactory) connectionFactory;
ConnectionHolder conHolder = (ConnectionHolder) it.getResource(connectionFactory);
if (conHolder != null && connectionEquals(conHolder, con)) {
// It's the transactional Connection: Don't close it.
conHolder.released();
}
return Mono.from(con.close());
}).onErrorResume(NoTransactionException.class, e -> {
logger.debug("Releasing R2DBC Connection");
if (connectionFactory instanceof SingletonConnectionFactory) {
return factory.close(con);
}
SingletonConnectionFactory factory = (SingletonConnectionFactory) connectionFactory;
logger.debug("Closing R2DBC Connection");
logger.debug("Releasing R2DBC Connection");
return Mono.from(con.close());
return factory.close(con);
}
logger.debug("Closing R2DBC Connection");
return Mono.from(con.close());
});
}
/**
@@ -236,14 +331,196 @@ public class ConnectionFactoryUtils {
* @see ReactiveTransactionSynchronization
* @see TransactionResources
*/
public static Mono<ConnectionFactory> currentConnectionFactory() {
public static Mono<ConnectionFactory> currentConnectionFactory(ConnectionFactory connectionFactory) {
return currentActiveReactiveTransactionSynchronization() //
.map(synchronization -> {
return TransactionSynchronizationManager.currentTransaction()
.filter(TransactionSynchronizationManager::isSynchronizationActive).filter(it -> {
TransactionResources currentSynchronization = synchronization.getCurrentTransaction();
return currentSynchronization.getResource(ConnectionFactory.class);
}).switchIfEmpty(Mono.error(new DataAccessResourceFailureException(
"Cannot extract ConnectionFactory from current TransactionContext!")));
ConnectionHolder conHolder = (ConnectionHolder) it.getResource(connectionFactory);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
return true;
}
return false;
}).map(it -> connectionFactory).onErrorResume(NoTransactionException.class, e -> {
return currentActiveReactiveTransactionSynchronization().map(synchronization -> {
TransactionResources currentSynchronization = synchronization.getCurrentTransaction();
return currentSynchronization.getResource(ConnectionFactory.class);
}).switchIfEmpty(Mono.error(new DataAccessResourceFailureException(
"Cannot extract ConnectionFactory from current TransactionContext!")));
});
}
/**
* Determine whether the given two {@link Connection}s are equal, asking the target {@link Connection} in case of a
* proxy. Used to detect equality even if the user passed in a raw target Connection while the held one is a proxy.
*
* @param conHolder the {@link ConnectionHolder} for the held Connection (potentially a proxy)
* @param passedInCon the {@link Connection} passed-in by the user (potentially a target {@link Connection} without
* proxy)
* @return whether the given Connections are equal
* @see #getTargetConnection
*/
private static boolean connectionEquals(ConnectionHolder conHolder, Connection passedInCon) {
if (!conHolder.hasConnection()) {
return false;
}
Connection heldCon = conHolder.getConnection();
// Explicitly check for identity too: for Connection handles that do not implement
// "equals" properly).
return (heldCon == passedInCon || heldCon.equals(passedInCon) || getTargetConnection(heldCon).equals(passedInCon));
}
/**
* Return the innermost target {@link Connection} of the given {@link Connection}. If the given {@link Connection} is
* a proxy, it will be unwrapped until a non-proxy {@link Connection} is found. Otherwise, the passed-in Connection
* will be returned as-is.
*
* @param con the {@link Connection} proxy to unwrap
* @return the innermost target Connection, or the passed-in one if no proxy
* @see ConnectionProxy#getTargetConnection()
*/
public static Connection getTargetConnection(Connection con) {
Connection conToUse = con;
while (conToUse instanceof ConnectionProxy) {
conToUse = ((ConnectionProxy) conToUse).getTargetConnection();
}
return conToUse;
}
/**
* Determine the connection synchronization order to use for the given {@link ConnectionFactory}. Decreased for every
* level of nesting that a {@link ConnectionFactory} has, checked through the level of
* {@link DelegatingConnectionFactory} nesting.
*
* @param connectionFactory the {@link ConnectionFactory} to check.
* @return the connection synchronization order to use.
* @see #CONNECTION_SYNCHRONIZATION_ORDER
*/
private static int getConnectionSynchronizationOrder(ConnectionFactory connectionFactory) {
int order = CONNECTION_SYNCHRONIZATION_ORDER;
ConnectionFactory current = connectionFactory;
while (current instanceof DelegatingConnectionFactory) {
order--;
current = ((DelegatingConnectionFactory) current).getTargetConnectionFactory();
}
return order;
}
/**
* Callback for resource cleanup at the end of a non-native R2DBC transaction.
*/
private static class ConnectionSynchronization implements TransactionSynchronization, Ordered {
private final ConnectionHolder connectionHolder;
private final ConnectionFactory connectionFactory;
private int order;
private boolean holderActive = true;
ConnectionSynchronization(ConnectionHolder connectionHolder, ConnectionFactory connectionFactory) {
this.connectionHolder = connectionHolder;
this.connectionFactory = connectionFactory;
this.order = getConnectionSynchronizationOrder(connectionFactory);
}
@Override
public int getOrder() {
return this.order;
}
@Override
public Mono<Void> suspend() {
if (this.holderActive) {
return TransactionSynchronizationManager.currentTransaction().flatMap(it -> {
it.unbindResource(this.connectionFactory);
if (this.connectionHolder.hasConnection() && !this.connectionHolder.isOpen()) {
// Release Connection on suspend if the application doesn't keep
// a handle to it anymore. We will fetch a fresh Connection if the
// application accesses the ConnectionHolder again after resume,
// assuming that it will participate in the same transaction.
return releaseConnection(this.connectionHolder.getConnection(), this.connectionFactory)
.doOnTerminate(() -> this.connectionHolder.setConnection(null));
}
return Mono.empty();
});
}
return Mono.empty();
}
@Override
public Mono<Void> resume() {
if (this.holderActive) {
return TransactionSynchronizationManager.currentTransaction().doOnNext(it -> {
it.bindResource(this.connectionFactory, this.connectionHolder);
}).then();
}
return Mono.empty();
}
@Override
public Mono<Void> beforeCompletion() {
// Release Connection early if the holder is not open anymore
// (that is, not used by another resource
// that has its own cleanup via transaction synchronization),
// to avoid issues with strict transaction implementations that expect
// the close call before transaction completion.
if (!this.connectionHolder.isOpen()) {
return TransactionSynchronizationManager.currentTransaction().flatMap(it -> {
it.unbindResource(this.connectionFactory);
this.holderActive = false;
if (this.connectionHolder.hasConnection()) {
return releaseConnection(this.connectionHolder.getConnection(), this.connectionFactory);
}
return Mono.empty();
});
}
return Mono.empty();
}
@Override
public Mono<Void> afterCompletion(int status) {
// If we haven't closed the Connection in beforeCompletion,
// close it now. The holder might have been used for other
// cleanup in the meantime, for example by a Hibernate Session.
if (this.holderActive) {
// The thread-bound ConnectionHolder might not be available anymore,
// since afterCompletion might get called from a different thread.
return TransactionSynchronizationManager.currentTransaction().flatMap(it -> {
it.unbindResourceIfPossible(this.connectionFactory);
this.holderActive = false;
if (this.connectionHolder.hasConnection()) {
return releaseConnection(this.connectionHolder.getConnection(), this.connectionFactory)
.doOnTerminate(() -> {
// Reset the ConnectionHolder: It might remain bound to the context.
this.connectionHolder.setConnection(null);
});
}
return Mono.empty();
});
}
this.connectionHolder.reset();
return Mono.empty();
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2019 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.r2dbc.function.connectionfactory;
import io.r2dbc.spi.Connection;
/**
* Simple interface to be implemented by handles for a R2DBC Connection.
*
* @author Mark Paluch
* @see SimpleConnectionHandle
* @see ConnectionHolder
*/
@FunctionalInterface
public interface ConnectionHandle {
/**
* Fetch the R2DBC Connection that this handle refers to.
*/
Connection getConnection();
/**
* Release the R2DBC Connection that this handle refers to. Assumes a non-blocking implementation without
* synchronization.
* <p>
* The default implementation is empty, assuming that the lifecycle of the connection is managed externally.
*
* @param con the R2DBC Connection to release
*/
default void releaseConnection(Connection con) {}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2019 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.r2dbc.function.connectionfactory;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.ResourceHolderSupport;
import org.springframework.util.Assert;
/**
* Resource holder wrapping a R2DBC {@link Connection}. {@link ConnectionFactoryTransactionManager} binds instances of
* this class to the thread, for a specific {@link ConnectionFactory}.
* <p>
* Inherits rollback-only support for nested R2DBC transactions and reference count functionality from the base class.
* <p>
* Note: This is an SPI class, not intended to be used by applications.
*
* @see ConnectionFactoryTransactionManager
* @see ConnectionFactoryUtils
*/
public class ConnectionHolder extends ResourceHolderSupport {
@Nullable private ConnectionHandle connectionHandle;
@Nullable private Connection currentConnection;
private boolean transactionActive = false;
/**
* Create a new ConnectionHolder for the given R2DBC {@link Connection}, wrapping it with a
* {@link SimpleConnectionHandle}, assuming that there is no ongoing transaction.
*
* @param connection the R2DBC {@link Connection} to hold
* @see SimpleConnectionHandle
* @see #ConnectionHolder(Connection, boolean)
*/
public ConnectionHolder(Connection connection) {
this.connectionHandle = new SimpleConnectionHandle(connection);
}
/**
* Create a new ConnectionHolder for the given R2DBC {@link Connection}, wrapping it with a
* {@link SimpleConnectionHandle}.
*
* @param connection the R2DBC {@link Connection} to hold
* @param transactionActive whether the given {@link Connection} is involved in an ongoing transaction
* @see SimpleConnectionHandle
*/
public ConnectionHolder(Connection connection, boolean transactionActive) {
this(connection);
this.transactionActive = transactionActive;
}
/**
* Return the ConnectionHandle held by this ConnectionHolder.
*/
@Nullable
public ConnectionHandle getConnectionHandle() {
return this.connectionHandle;
}
/**
* Return whether this holder currently has a {@link Connection}.
*/
protected boolean hasConnection() {
return (this.connectionHandle != null);
}
/**
* Set whether this holder represents an active, R2DBC-managed transaction.
*
* @see ConnectionFactoryTransactionManager
*/
protected void setTransactionActive(boolean transactionActive) {
this.transactionActive = transactionActive;
}
/**
* Return whether this holder represents an active, R2DBC-managed transaction.
*/
protected boolean isTransactionActive() {
return this.transactionActive;
}
/**
* Override the existing Connection handle with the given {@link Connection}. Reset the handle if given {@code null}.
* <p>
* Used for releasing the {@link Connection} on suspend (with a {@code null} argument) and setting a fresh
* {@link Connection} on resume.
*/
protected void setConnection(@Nullable Connection connection) {
if (this.currentConnection != null) {
if (this.connectionHandle != null) {
this.connectionHandle.releaseConnection(this.currentConnection);
}
this.currentConnection = null;
}
if (connection != null) {
this.connectionHandle = new SimpleConnectionHandle(connection);
} else {
this.connectionHandle = null;
}
}
/**
* Return the current {@link Connection} held by this {@link ConnectionHolder}.
* <p>
* This will be the same {@link Connection} until {@code released} gets called on the {@link ConnectionHolder}, which
* will reset the held {@link Connection}, fetching a new {@link Connection} on demand.
*
* @see ConnectionHandle#getConnection()
* @see #released()
*/
public Connection getConnection() {
Assert.notNull(this.connectionHandle, "Active Connection is required");
if (this.currentConnection == null) {
this.currentConnection = this.connectionHandle.getConnection();
}
return this.currentConnection;
}
/**
* Releases the current {@link Connection} held by this {@link ConnectionHolder}.
* <p>
* This is necessary for {@link ConnectionHandle}s that expect "Connection borrowing", where each returned
* {@link Connection} is only temporarily leased and needs to be returned once the data operation is done, to make the
* Connection available for other operations within the same transaction.
*/
@Override
public void released() {
super.released();
if (!isOpen() && this.currentConnection != null) {
if (this.connectionHandle != null) {
this.connectionHandle.releaseConnection(this.currentConnection);
}
this.currentConnection = null;
}
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSupport#clear()
*/
@Override
public void clear() {
super.clear();
this.transactionActive = false;
}
}

View File

@@ -16,8 +16,7 @@
package org.springframework.data.r2dbc.function.connectionfactory;
import io.r2dbc.spi.Connection;
import java.sql.Wrapper;
import io.r2dbc.spi.Wrapped;
/**
* Subinterface of {@link Connection} to be implemented by Connection proxies. Allows access to the underlying target
@@ -27,12 +26,12 @@ import java.sql.Wrapper;
*
* @author Mark Paluch
*/
public interface ConnectionProxy extends Connection, Wrapper {
public interface ConnectionProxy extends Connection, Wrapped<Connection> {
/**
* Return the target Connection of this proxy.
* Return the target {@link Connection} of this proxy.
* <p/>
* This will typically be the native driver Connection or a wrapper from a connection pool.
* This will typically be the native driver {@link Connection} or a wrapper from a connection pool.
*
* @return the underlying Connection (never {@literal null})
*/

View File

@@ -29,7 +29,8 @@ class DefaultTransactionResources implements TransactionResources {
private Map<Class<?>, Object> items = new ConcurrentHashMap<>();
/* (non-Javadoc)
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.connectionfactory.TransactionResources#registerResource(java.lang.Class, java.lang.Object)
*/
@Override
@@ -40,7 +41,8 @@ class DefaultTransactionResources implements TransactionResources {
items.put(key, value);
}
/* (non-Javadoc)
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.connectionfactory.TransactionResources#getResource(java.lang.Class)
*/
@SuppressWarnings("unchecked")

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2019 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.r2dbc.function.connectionfactory;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryMetadata;
import io.r2dbc.spi.Wrapped;
import org.reactivestreams.Publisher;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* R2DBC {@link ConnectionFactory} implementation that delegates all calls to a given target {@link ConnectionFactory}.
* <p>
* This class is meant to be subclassed, with subclasses overriding only those methods (such as {@link #create()}) that
* should not simply delegate to the target {@link ConnectionFactory}.
*
* @author Mark Paluch
* @see #create
*/
public class DelegatingConnectionFactory implements ConnectionFactory, Wrapped<ConnectionFactory> {
private final ConnectionFactory targetConnectionFactory;
public DelegatingConnectionFactory(ConnectionFactory targetConnectionFactory) {
Assert.notNull(targetConnectionFactory, "ConnectionFactory must not be null");
this.targetConnectionFactory = targetConnectionFactory;
}
/*
* (non-Javadoc)
* @see io.r2dbc.spi.ConnectionFactory#create()
*/
@Override
public Publisher<? extends Connection> create() {
return targetConnectionFactory.create();
}
/**
* Obtain the target {@link ConnectionFactory} for actual use (never {@code null}).
*/
protected ConnectionFactory obtainTargetConnectionFactory() {
return getTargetConnectionFactory();
}
/**
* Return the target {@link ConnectionFactory} that this {@link ConnectionFactory} should delegate to.
*/
@Nullable
public ConnectionFactory getTargetConnectionFactory() {
return this.targetConnectionFactory;
}
/*
* (non-Javadoc)
* @see io.r2dbc.spi.ConnectionFactory#getMetadata()
*/
@Override
public ConnectionFactoryMetadata getMetadata() {
return obtainTargetConnectionFactory().getMetadata();
}
/*
* (non-Javadoc)
* @see io.r2dbc.spi.Wrapped#unwrap()
*/
@Override
public ConnectionFactory unwrap() {
return obtainTargetConnectionFactory();
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2019 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.r2dbc.function.connectionfactory;
import io.r2dbc.spi.IsolationLevel;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Convenient base class for R2DBC-aware transaction objects. Can contain a {@link ConnectionHolder} with a R2DBC
* {@link Connection}.
*
* @author Mark Paluch
* @see ConnectionFactoryTransactionManager
*/
public abstract class R2dbcTransactionObjectSupport {
@Nullable private ConnectionHolder connectionHolder;
@Nullable private IsolationLevel previousIsolationLevel;
private boolean savepointAllowed = false;
public void setConnectionHolder(@Nullable ConnectionHolder connectionHolder) {
this.connectionHolder = connectionHolder;
}
public ConnectionHolder getConnectionHolder() {
Assert.state(this.connectionHolder != null, "No ConnectionHolder available");
return this.connectionHolder;
}
public boolean hasConnectionHolder() {
return (this.connectionHolder != null);
}
public void setPreviousIsolationLevel(@Nullable IsolationLevel previousIsolationLevel) {
this.previousIsolationLevel = previousIsolationLevel;
}
@Nullable
public IsolationLevel getPreviousIsolationLevel() {
return this.previousIsolationLevel;
}
public void setSavepointAllowed(boolean savepointAllowed) {
this.savepointAllowed = savepointAllowed;
}
public boolean isSavepointAllowed() {
return this.savepointAllowed;
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2019 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.r2dbc.function.connectionfactory;
import io.r2dbc.spi.Connection;
import org.springframework.util.Assert;
/**
* Simple implementation of the {@link ConnectionHandle} interface, containing a given R2DBC Connection.
*
* @author Mark Paluch
*/
public class SimpleConnectionHandle implements ConnectionHandle {
private final Connection connection;
/**
* Create a new SimpleConnectionHandle for the given Connection.
*
* @param connection the R2DBC Connection
*/
public SimpleConnectionHandle(Connection connection) {
Assert.notNull(connection, "Connection must not be null");
this.connection = connection;
}
/**
* Return the specified Connection as-is.
*/
@Override
public Connection getConnection() {
return this.connection;
}
@Override
public String toString() {
return "SimpleConnectionHandle: " + this.connection;
}
}

View File

@@ -44,7 +44,8 @@ class SingletonConnectionFactory implements SmartConnectionFactory {
this.connectionMono = Mono.just(connection);
}
/* (non-Javadoc)
/*
* (non-Javadoc)
* @see io.r2dbc.spi.ConnectionFactory#create()
*/
@Override
@@ -57,7 +58,8 @@ class SingletonConnectionFactory implements SmartConnectionFactory {
return connectionMono.doOnSubscribe(s -> refCount.incrementAndGet());
}
/* (non-Javadoc)
/*
* (non-Javadoc)
* @see io.r2dbc.spi.ConnectionFactory#getMetadata()
*/
@Override
@@ -69,6 +71,10 @@ class SingletonConnectionFactory implements SmartConnectionFactory {
return this.connection == connection;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.connectionfactory.SmartConnectionFactory#shouldClose(io.r2dbc.spi.Connection)
*/
@Override
public boolean shouldClose(Connection connection) {
return refCount.get() == 1;

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2019 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.r2dbc.function.connectionfactory;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Wrapped;
import reactor.core.publisher.Mono;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.springframework.data.r2dbc.function.DatabaseClient;
import org.springframework.lang.Nullable;
/**
* Proxy for a target R2DBC {@link ConnectionFactory}, adding awareness of Spring-managed transactions.
* <p>
* Data access code that should remain unaware of Spring's data access support can work with this proxy to seamlessly
* participate in Spring-managed transactions. Note that the transaction manager, for example
* {@link ConnectionFactoryTransactionManager}, still needs to work with the underlying {@link ConnectionFactory},
* <i>not</i> with this proxy.
* <p>
* <b>Make sure that {@link TransactionAwareConnectionFactoryProxy} is the outermost {@link ConnectionFactory} of a
* chain of {@link ConnectionFactory} proxies/adapters.</b> {@link TransactionAwareConnectionFactoryProxy} can delegate
* either directly to the target connection pool or to some intermediary proxy/adapter.
* <p>
* Delegates to {@link ConnectionFactoryUtils} for automatically participating in thread-bound transactions, for example
* managed by {@link ConnectionFactoryTransactionManager}. {@link #create()} calls and {@code close} calls on returned
* {@link Connection} will behave properly within a transaction, i.e. always operate on the transactional Connection. If
* not within a transaction, normal {@link ConnectionFactory} behavior applies.
* <p>
* This proxy allows data access code to work with the plain R2DBC API. However, if possible, use Spring's
* {@link ConnectionFactoryUtils} or {@link DatabaseClient} to get transaction participation even without a proxy for
* the target {@link ConnectionFactory}, avoiding the need to define such a proxy in the first place.
* <p>
* <b>NOTE:</b> This {@link ConnectionFactory} proxy needs to return wrapped {@link Connection}s (which implement the
* {@link ConnectionProxy} interface) in order to handle close calls properly. Use {@link Wrapped#unwrap()} to retrieve
* the native R2DBC Connection.
*
* @author Mark Paluch
* @see ConnectionFactory#create
* @see Connection#close
* @see ConnectionFactoryUtils#doGetConnection
* @see ConnectionFactoryUtils#doReleaseConnection
*/
public class TransactionAwareConnectionFactoryProxy extends DelegatingConnectionFactory {
/**
* Create a new {@link TransactionAwareConnectionFactoryProxy}.
*
* @param targetConnectionFactory the target {@link ConnectionFactory}.
*/
public TransactionAwareConnectionFactoryProxy(ConnectionFactory targetConnectionFactory) {
super(targetConnectionFactory);
}
/**
* Delegates to {@link ConnectionFactoryUtils} for automatically participating in Spring-managed transactions.
* <p>
* The returned {@link ConnectionFactory} handle implements the {@link ConnectionProxy} interface, allowing to
* retrieve the underlying target {@link Connection}.
*
* @return a transactional {@link Connection} if any, a new one else.
* @see ConnectionFactoryUtils#doGetConnection
* @see ConnectionProxy#getTargetConnection
*/
@Override
public Mono<Connection> create() {
return getTransactionAwareConnectionProxy(obtainTargetConnectionFactory());
}
/**
* Wraps the given {@link Connection} with a proxy that delegates every method call to it but delegates
* {@code close()} calls to {@link ConnectionFactoryUtils}.
*
* @param targetConnectionFactory the {@link ConnectionFactory} that the {@link Connection} came from.
* @return the wrapped {@link Connection}.
* @see Connection#close()
* @see ConnectionFactoryUtils#doReleaseConnection
*/
protected Mono<Connection> getTransactionAwareConnectionProxy(ConnectionFactory targetConnectionFactory) {
return ConnectionFactoryUtils.getConnection(targetConnectionFactory).map(tuple -> {
return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(),
new Class<?>[] { ConnectionProxy.class },
new TransactionAwareInvocationHandler(tuple.getT1(), targetConnectionFactory));
});
}
/**
* Invocation handler that delegates close calls on R2DBC Connections to {@link ConnectionFactoryUtils} for being
* aware of context-bound transactions.
*/
private class TransactionAwareInvocationHandler implements InvocationHandler {
private final Connection connection;
private final ConnectionFactory targetConnectionFactory;
private boolean closed = false;
TransactionAwareInvocationHandler(Connection connection, ConnectionFactory targetConnectionFactory) {
this.connection = connection;
this.targetConnectionFactory = targetConnectionFactory;
}
/*
* (non-Javadoc)
* @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
*/
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Invocation on ConnectionProxy interface coming in...
switch (method.getName()) {
case "equals":
// Only considered as equal when proxies are identical.
return (proxy == args[0]);
case "hashCode":
// Use hashCode of Connection proxy.
return System.identityHashCode(proxy);
case "toString":
// Allow for differentiating between the proxy and the raw Connection.
StringBuilder sb = new StringBuilder("Transaction-aware proxy for target Connection ");
if (this.connection != null) {
sb.append("[").append(this.connection.toString()).append("]");
} else {
sb.append(" from ConnectionFactory [").append(this.targetConnectionFactory).append("]");
}
return sb.toString();
case "unwrap":
return this.connection;
case "close":
// Handle close method: only close if not within a transaction.
return ConnectionFactoryUtils.doReleaseConnection(this.connection, this.targetConnectionFactory)
.doOnSubscribe(n -> this.closed = true);
case "isClosed":
return this.closed;
}
if (this.closed) {
throw new IllegalStateException("Connection handle already closed");
}
if (method.getName().equals("getTargetConnection")) {
// Handle getTargetConnection method: return underlying Connection.
return this.connection;
}
// Invoke method on target Connection.
try {
return method.invoke(this.connection, args);
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
}

View File

@@ -17,6 +17,8 @@ package org.springframework.data.r2dbc.function.connectionfactory;
import reactor.core.publisher.Mono;
import org.springframework.data.r2dbc.function.TransactionalDatabaseClient;
/**
* Transaction context for an ongoing transaction synchronization allowing to register transactional resources.
* <p>
@@ -27,6 +29,7 @@ import reactor.core.publisher.Mono;
* should be bound to a transaction.
*
* @author Mark Paluch
* @see TransactionalDatabaseClient
*/
public interface TransactionResources {