#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
@@ -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]
|
||||
|
||||
71
src/main/asciidoc/reference/r2dbc-connections.adoc
Normal file
71
src/main/asciidoc/reference/r2dbc-connections.adoc
Normal 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.
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user