#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:
committed by
Christoph Strobl
parent
da53a9a934
commit
79e32941b5
@@ -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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
*/
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user