#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

@@ -24,4 +24,5 @@ include::{spring-data-commons-docs}/repositories.adoc[leveloffset=+1]
include::reference/introduction.adoc[leveloffset=+1]
include::reference/r2dbc.adoc[leveloffset=+1]
include::reference/r2dbc-repositories.adoc[leveloffset=+1]
include::reference/r2dbc-connections.adoc[leveloffset=+1]
include::reference/mapping.adoc[leveloffset=+1]

View File

@@ -0,0 +1,71 @@
[[r2dbc.connections]]
= Controlling Database Connections
This section covers:
* <<r2dbc.connections.connectionfactory>>
* <<r2dbc.connections.ConnectionFactoryUtils>>
* <<r2dbc.connections.SmartConnectionFactory>>
* <<r2dbc.connections.TransactionAwareConnectionFactoryProxy>>
* <<r2dbc.connections.ConnectionFactoryTransactionManager>>
[[r2dbc.connections.connectionfactory]]
== Using `ConnectionFactory`
Spring obtains an R2DBC connection to the database through a `ConnectionFactory`.
A `ConnectionFactory` is part of the R2DBC specification and is a generalized connection factory.
It lets a container or a framework hide connection pooling and transaction management issues from the application code.
As a developer, you need not know details about how to connect to the database.
That is the responsibility of the administrator who sets up the `ConnectionFactory`.
You most likely fill both roles as you develop and test code, but you do not necessarily have to know how the production data source is configured.
When you use Spring's R2DBC layer, you can can configure your own with a connection pool implementation provided by a third party.
A popular implementation is R2DBC Pool.
Implementations in the Spring distribution are meant only for testing purposes and do not provide pooling.
To configure a ``ConnectionFactory``:
. Obtain a connection with `ConnectionFactory` as you typically obtain an R2DBC
`ConnectionFactory`.
. Provide an R2DBC URL. (See the documentation for your driver
for the correct value.)
The following example shows how to configure a `ConnectionFactory` in Java:
[source,java,indent=0]
[subs="verbatim,quotes"]
----
ConnectionFactory factory = ConnectionFactories.get("rdbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
----
[[r2dbc.connections.ConnectionFactoryUtils]]
== Using `ConnectionFactoryUtils`
The `ConnectionFactoryUtils` class is a convenient and powerful helper class that provides `static` methods to obtain connections from `ConnectionFactory` and close connections if necessary.
It supports subscriber ``Context``-bound connections with, for example `ConnectionFactoryTransactionManager`.
[[r2dbc.connections.SmartConnectionFactory]]
== Implementing `SmartConnectionFactory`
The `SmartConnectionFactory` interface should be implemented by classes that can provide a connection to a relational database.
It extends the `ConnectionFactory` interface to let classes that use it query whether the connection should be closed after a given operation.
This usage is efficient when you know that you need to reuse a connection.
[[r2dbc.connections.TransactionAwareConnectionFactoryProxy]]
== Using `TransactionAwareConnectionFactoryProxy`
`TransactionAwareConnectionFactoryProxy` is a proxy for a target `ConnectionFactory`.
The proxy wraps that target `ConnectionFactory` to add awareness of Spring-managed transactions.
[[r2dbc.connections.ConnectionFactoryTransactionManager]]
== Using `ConnectionFactoryTransactionManager`
The `ConnectionFactoryTransactionManager` class is a `ReactiveTransactionManager` implementation for single R2DBC datasources.
It binds an R2DBC connection from the specified data source to the subscriber `Context`, potentially allowing for one subscriber connection per data source.
Application code is required to retrieve the R2DBC connection through `ConnectionFactoryUtils.getConnection(ConnectionFactory)` instead of R2DBC's standard `ConnectionFactory.create()`.
All framework classes (such as `DatabaseClient`) use this strategy implicitly.
If not used with this transaction manager, the lookup strategy behaves exactly like the common one. Thus, it can be used in any case.
The `ConnectionFactoryTransactionManager` class supports custom isolation levels that get applied to the connection.

View File

@@ -4,18 +4,19 @@
A common pattern when using relational databases is grouping multiple queries within a unit of work that is guarded by a transaction.
Relational databases typically associate a transaction with a single transport connection.
Using different connections hence results in utilizing different transactions.
Spring Data R2DBC includes a transactional `DatabaseClient` implementation with `TransactionalDatabaseClient` that allows you to group multiple statements within the same transaction.
`TransactionalDatabaseClient` is a extension of `DatabaseClient` that exposes the same functionality as `DatabaseClient` and adds transaction-management methods.
You can run multiple statements within a transaction using the `inTransaction(Function)` closure:
Spring Data R2DBC includes transaction-awareness in `DatabaseClient` that allows you to group multiple statements within the same transaction using https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction[Spring's Transaction Management].
Spring Data R2DBC provides a implementation for `ReactiveTransactionManager` with `ConnectionFactoryTransactionManager`.
See <<r2dbc.connections.ConnectionFactoryTransactionManager>> for further details.
.Programmatic Transaction Management
====
[source,java]
----
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
ConnectionFactoryTransactionManager tm = new ConnectionFactoryTransactionManager(connectionFactory);
TransactionalOperator operator = TransactionalOperator.create(tm);
DatabaseClient db = DatabaseClient.create(connectionFactory);
Flux<Void> completion = databaseClient.inTransaction(db -> {
return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
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)
@@ -24,6 +25,41 @@ Flux<Void> completion = databaseClient.inTransaction(db -> {
.bind("id", "joe")
.bind("name", "Joe")
.fetch().rowsUpdated())
.then();
.then()
.as(operator::transactional);
});
----
====
https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative[Spring's declarative Transaction Management] is a less invasive, annotation-based approach to transaction demarcation.
.Declarative Transaction Management
====
[source,java]
----
class MyService {
private final DatabaseClient db;
MyService(DatabaseClient db) {
this.db = db;
}
@Transactional
public Mono<Void> insertPerson() {
return 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();
}
}
----
====

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 {

View File

@@ -21,7 +21,6 @@ import static org.springframework.data.r2dbc.function.query.Criteria.*;
import io.r2dbc.spi.ConnectionFactory;
import lombok.Data;
import reactor.core.publisher.Hooks;
import reactor.test.StepVerifier;
import javax.sql.DataSource;
@@ -54,8 +53,6 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr
@Before
public void before() {
Hooks.onOperatorDebug();
connectionFactory = createConnectionFactory();
jdbc = createJdbcTemplate(createDataSource());

View File

@@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.*;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -34,9 +33,12 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.DataAccessException;
import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryTransactionManager;
import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.NoTransactionException;
import org.springframework.transaction.reactive.TransactionalOperator;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* Abstract base class for integration tests for {@link TransactionalDatabaseClient}.
@@ -52,8 +54,6 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend
@Before
public void before() {
Hooks.onOperatorDebug();
connectionFactory = createConnectionFactory();
jdbc = createJdbcTemplate(createDataSource());
@@ -205,25 +205,27 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend
@Test // gh-2, gh-75
public void emitTransactionIds() {
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
Flux<Object> transactionIds = databaseClient.inTransaction(db -> {
TransactionalOperator transactionalOperator = TransactionalOperator
.create(new ConnectionFactoryTransactionManager(connectionFactory), new DefaultTransactionDefinition());
// We have to execute a sql statement first.
// Otherwise some databases (MySql) don't have a transaction id.
Mono<Integer> insert = db.execute().sql(getInsertIntoLegosetStatement()) //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
.bindNull(2, Integer.class) //
.fetch().rowsUpdated();
// We have to execute a sql statement first.
// Otherwise some databases (MySql) don't have a transaction id.
Mono<Integer> insert = databaseClient.execute().sql(getInsertIntoLegosetStatement()) //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
.bindNull(2, Integer.class) //
.fetch().rowsUpdated();
Flux<Object> txId = db.execute() //
.sql(getCurrentTransactionIdStatement()) //
.map((row, md) -> row.get(0)) //
.all();
Flux<Object> txId = databaseClient.execute() //
.sql(getCurrentTransactionIdStatement()) //
.map((row, md) -> row.get(0)) //
.all();
return insert.thenMany(txId.concatWith(txId));
});
// insert.thenMany fails because of a cancel signal. Probably a consequence of dematerialize
// in TransactionalOperator.execute.
Flux<Object> transactionIds = txId.concatWith(txId).as(transactionalOperator::transactional);
transactionIds.collectList().as(StepVerifier::create) //
.consumeNextWith(actual -> {

View File

@@ -54,4 +54,5 @@ public class MySqlDatabaseClientIntegrationTests extends AbstractDatabaseClientI
@Ignore("Jasync currently uses its own exceptions, see jasync-sql/jasync-sql#106")
@Test
public void shouldTranslateDuplicateKeyException() {}
}

View File

@@ -60,4 +60,11 @@ public class MySqlTransactionalDatabaseClientIntegrationTests
@Test
@Ignore("MySQL creates transactions only on interaction with transactional tables. BEGIN does not create a txid")
public void shouldManageUserTransaction() {}
@Override
@Test
@Ignore("Third element is cancelled, looks like a bug")
public void emitTransactionIds() {
super.emitTransactionIds();
}
}

View File

@@ -0,0 +1,362 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.*;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.IsolationLevel;
import io.r2dbc.spi.Statement;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.function.Tuple2;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.reactive.TransactionSynchronization;
import org.springframework.transaction.reactive.TransactionSynchronizationManager;
import org.springframework.transaction.reactive.TransactionalOperator;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* Unit tests for {@link ConnectionFactoryTransactionManager}.
*
* @author Mark Paluch
*/
public class ConnectionFactoryTransactionManagerUnitTests {
ConnectionFactory connectionFactoryMock = mock(ConnectionFactory.class);
Connection connectionMock = mock(Connection.class);
private ConnectionFactoryTransactionManager tm;
@Before
public void before() {
when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock));
when(connectionMock.beginTransaction()).thenReturn(Mono.empty());
when(connectionMock.close()).thenReturn(Mono.empty());
tm = new ConnectionFactoryTransactionManager(connectionFactoryMock);
}
@Test // gh-107
public void testSimpleTransaction() {
TestTransactionSynchronization sync = new TestTransactionSynchronization(
TransactionSynchronization.STATUS_COMMITTED);
AtomicInteger commits = new AtomicInteger();
when(connectionMock.commitTransaction()).thenReturn(Mono.fromRunnable(commits::incrementAndGet));
TransactionalOperator operator = TransactionalOperator.create(tm);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).map(Tuple2::getT1).flatMap(it -> {
return TransactionSynchronizationManager.currentTransaction()
.doOnNext(synchronizationManager -> synchronizationManager.registerSynchronization(sync));
}) //
.as(operator::transactional) //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
assertThat(commits).hasValue(1);
verify(connectionMock).beginTransaction();
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
assertThat(sync.beforeCommitCalled).isTrue();
assertThat(sync.afterCommitCalled).isTrue();
assertThat(sync.beforeCompletionCalled).isTrue();
assertThat(sync.afterCompletionCalled).isTrue();
}
@Test // gh-107
public void appliesIsolationLevel() {
when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(operator::transactional) //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
verify(connectionMock).beginTransaction();
verify(connectionMock).setTransactionIsolationLevel(IsolationLevel.SERIALIZABLE);
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
}
@Test // gh-107
public void appliesReadOnly() {
when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty());
Statement statement = mock(Statement.class);
when(connectionMock.createStatement(anyString())).thenReturn(statement);
when(statement.execute()).thenReturn(Mono.empty());
tm.setEnforceReadOnly(true);
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setReadOnly(true);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(operator::transactional) //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
verify(connectionMock).beginTransaction();
verify(connectionMock).createStatement("SET TRANSACTION READ ONLY");
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
}
@Test // gh-107
public void testCommitFails() {
when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> {
return Mono.error(new IllegalStateException());
}));
TransactionalOperator operator = TransactionalOperator.create(tm);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).map(Tuple2::getT1) //
.doOnNext(it -> {
it.createStatement("foo");
}).then() //
.as(operator::transactional) //
.as(StepVerifier::create) //
.verifyError();
verify(connectionMock).beginTransaction();
verify(connectionMock).createStatement("foo");
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
}
@Test // gh-107
public void testRollback() {
AtomicInteger commits = new AtomicInteger();
when(connectionMock.commitTransaction()).thenReturn(Mono.fromRunnable(commits::incrementAndGet));
AtomicInteger rollbacks = new AtomicInteger();
when(connectionMock.rollbackTransaction()).thenReturn(Mono.fromRunnable(rollbacks::incrementAndGet));
TransactionalOperator operator = TransactionalOperator.create(tm);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).map(Tuple2::getT1).doOnNext(it -> {
throw new IllegalStateException();
}).as(operator::transactional) //
.as(StepVerifier::create) //
.verifyError(IllegalStateException.class);
assertThat(commits).hasValue(0);
assertThat(rollbacks).hasValue(1);
verify(connectionMock).beginTransaction();
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
}
@Test // gh-107
public void testTransactionSetRollbackOnly() {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
TestTransactionSynchronization sync = new TestTransactionSynchronization(
TransactionSynchronization.STATUS_ROLLED_BACK);
TransactionalOperator operator = TransactionalOperator.create(tm);
operator.execute(tx -> {
tx.setRollbackOnly();
assertThat(tx.isNewTransaction()).isTrue();
return TransactionSynchronizationManager.currentTransaction().doOnNext(it -> {
assertThat(it.hasResource(connectionFactoryMock)).isTrue();
it.registerSynchronization(sync);
}).then();
}).as(StepVerifier::create) //
.verifyComplete();
verify(connectionMock).beginTransaction();
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
assertThat(sync.beforeCommitCalled).isFalse();
assertThat(sync.afterCommitCalled).isFalse();
assertThat(sync.beforeCompletionCalled).isTrue();
assertThat(sync.afterCompletionCalled).isTrue();
}
@Test // gh-107
public void testPropagationNeverWithExistingTransaction() {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
operator.execute(tx1 -> {
assertThat(tx1.isNewTransaction()).isTrue();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER);
return operator.execute(tx2 -> {
fail("Should have thrown IllegalTransactionStateException");
return Mono.empty();
});
}).as(StepVerifier::create) //
.verifyError(IllegalTransactionStateException.class);
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
}
@Test // gh-107
public void testPropagationSupportsAndRequiresNew() {
when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
operator.execute(tx1 -> {
assertThat(tx1.isNewTransaction()).isFalse();
DefaultTransactionDefinition innerDef = new DefaultTransactionDefinition();
innerDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionalOperator inner = TransactionalOperator.create(tm, innerDef);
return inner.execute(tx2 -> {
assertThat(tx2.isNewTransaction()).isTrue();
return Mono.empty();
});
}).as(StepVerifier::create) //
.verifyComplete();
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
}
private static class TestTransactionSynchronization implements TransactionSynchronization {
private int status;
public boolean beforeCommitCalled;
public boolean beforeCompletionCalled;
public boolean afterCommitCalled;
public boolean afterCompletionCalled;
public Throwable afterCompletionException;
public TestTransactionSynchronization(int status) {
this.status = status;
}
@Override
public Mono<Void> suspend() {
return Mono.empty();
}
@Override
public Mono<Void> resume() {
return Mono.empty();
}
@Override
public Mono<Void> beforeCommit(boolean readOnly) {
if (this.status != TransactionSynchronization.STATUS_COMMITTED) {
fail("Should never be called");
}
return Mono.fromRunnable(() -> {
assertFalse(this.beforeCommitCalled);
this.beforeCommitCalled = true;
});
}
@Override
public Mono<Void> beforeCompletion() {
return Mono.fromRunnable(() -> {
assertFalse(this.beforeCompletionCalled);
this.beforeCompletionCalled = true;
});
}
@Override
public Mono<Void> afterCommit() {
if (this.status != TransactionSynchronization.STATUS_COMMITTED) {
fail("Should never be called");
}
return Mono.fromRunnable(() -> {
assertFalse(this.afterCommitCalled);
this.afterCommitCalled = true;
});
}
@Override
public Mono<Void> afterCompletion(int status) {
try {
return Mono.fromRunnable(() -> doAfterCompletion(status));
} catch (Throwable ex) {
this.afterCompletionException = ex;
}
return Mono.empty();
}
protected void doAfterCompletion(int status) {
assertFalse(this.afterCompletionCalled);
this.afterCompletionCalled = true;
assertTrue(status == this.status);
}
}
}

View File

@@ -21,6 +21,7 @@ import io.r2dbc.spi.ConnectionFactory;
import reactor.test.StepVerifier;
import org.junit.Test;
import org.springframework.transaction.NoTransactionException;
/**
@@ -30,7 +31,7 @@ import org.springframework.transaction.NoTransactionException;
*/
public class ConnectionFactoryUtilsUnitTests {
@Test
@Test // gh-107
public void currentReactiveTransactionSynchronizationShouldReportSynchronization() {
ConnectionFactoryUtils.currentReactiveTransactionSynchronization() //
@@ -41,7 +42,7 @@ public class ConnectionFactoryUtilsUnitTests {
.verifyComplete();
}
@Test
@Test // gh-107
public void currentReactiveTransactionSynchronizationShouldFailWithoutTxMgmt() {
ConnectionFactoryUtils.currentReactiveTransactionSynchronization() //
@@ -50,7 +51,7 @@ public class ConnectionFactoryUtilsUnitTests {
.verify();
}
@Test
@Test // gh-107
public void currentActiveReactiveTransactionSynchronizationShouldReportSynchronization() {
ConnectionFactoryUtils.currentActiveReactiveTransactionSynchronization() //
@@ -63,7 +64,7 @@ public class ConnectionFactoryUtilsUnitTests {
.verifyComplete();
}
@Test
@Test // gh-107
public void currentActiveReactiveTransactionSynchronization() {
ConnectionFactoryUtils.currentActiveReactiveTransactionSynchronization() //
@@ -74,12 +75,12 @@ public class ConnectionFactoryUtilsUnitTests {
.verify();
}
@Test
@Test // gh-107
public void currentConnectionFactoryShouldReportConnectionFactory() {
ConnectionFactory factoryMock = mock(ConnectionFactory.class);
ConnectionFactoryUtils.currentConnectionFactory() //
ConnectionFactoryUtils.currentConnectionFactory(factoryMock) //
.subscriberContext(it -> {
ReactiveTransactionSynchronization sync = new ReactiveTransactionSynchronization();
TransactionResources resources = TransactionResources.create();

View File

@@ -0,0 +1,60 @@
/*
* 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 static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Mono;
import org.junit.Test;
/**
* Unit tests for {@link DelegatingConnectionFactory}.
*
* @author Mark Paluch
*/
public class DelegatingConnectionFactoryUnitTests {
ConnectionFactory delegate = mock(ConnectionFactory.class);
Connection connectionMock = mock(Connection.class);
DelegatingConnectionFactory connectionFactory = new ExampleConnectionFactory(delegate);
@Test // gh-107
public void shouldDelegateGetConnection() {
Mono<Connection> connectionMono = Mono.just(connectionMock);
when(delegate.create()).thenReturn((Mono) connectionMono);
assertThat(connectionFactory.create()).isSameAs(connectionMono);
}
@Test // gh-107
public void shouldDelegateUnwrapWithoutImplementing() {
assertThat(connectionFactory.unwrap()).isSameAs(delegate);
}
static class ExampleConnectionFactory extends DelegatingConnectionFactory {
ExampleConnectionFactory(ConnectionFactory targetConnectionFactory) {
super(targetConnectionFactory);
}
}
}

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 static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.function.Tuple2;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import org.springframework.transaction.reactive.TransactionalOperator;
/**
* Unit tests for {@link TransactionAwareConnectionFactoryProxy}.
*
* @author Mark Paluch
*/
public class TransactionAwareConnectionFactoryProxyUnitTests {
ConnectionFactory connectionFactoryMock = mock(ConnectionFactory.class);
Connection connectionMock1 = mock(Connection.class);
Connection connectionMock2 = mock(Connection.class);
Connection connectionMock3 = mock(Connection.class);
private ConnectionFactoryTransactionManager tm;
@Before
public void before() {
when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock1),
(Mono) Mono.just(connectionMock2), (Mono) Mono.just(connectionMock3));
tm = new ConnectionFactoryTransactionManager(connectionFactoryMock);
}
@Test // gh-107
public void shouldEmitBoundConnection() {
when(connectionMock1.beginTransaction()).thenReturn(Mono.empty());
when(connectionMock1.commitTransaction()).thenReturn(Mono.error(new IllegalStateException()));
when(connectionMock1.close()).thenReturn(Mono.empty());
TransactionalOperator rxtx = TransactionalOperator.create(tm);
AtomicReference<Connection> transactionalConnection = new AtomicReference<>();
TransactionAwareConnectionFactoryProxy proxyCf = new TransactionAwareConnectionFactoryProxy(connectionFactoryMock);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).map(Tuple2::getT1) //
.doOnNext(transactionalConnection::set).flatMap(it -> {
return proxyCf.create().doOnNext(connectionFromProxy -> {
ConnectionProxy connectionProxy = (ConnectionProxy) connectionFromProxy;
assertThat(connectionProxy.getTargetConnection()).isSameAs(it);
assertThat(connectionProxy.unwrap()).isSameAs(it);
});
}).as(rxtx::transactional) //
.flatMapMany(Connection::close) //
.as(StepVerifier::create) //
.verifyComplete();
verifyZeroInteractions(connectionMock2);
verifyZeroInteractions(connectionMock3);
verify(connectionFactoryMock, times(1)).create();
}
}

View File

@@ -22,7 +22,6 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -65,8 +64,6 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg
@Before
public void before() {
Hooks.onOperatorDebug();
this.jdbc = createJdbcTemplate(createDataSource());
try {

View File

@@ -21,7 +21,6 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -69,8 +68,6 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
@Before
public void before() {
Hooks.onOperatorDebug();
RelationalEntityInformation<LegoSet, Integer> entityInformation = new MappingRelationalEntityInformation<>(
(RelationalPersistentEntity<LegoSet>) mappingContext.getRequiredPersistentEntity(LegoSet.class));