Merge branch '6.0.x'

This commit is contained in:
Juergen Hoeller 2023-06-05 10:53:29 +02:00
commit 4fb4c95220
9 changed files with 305 additions and 146 deletions

View File

@ -39,7 +39,8 @@ public abstract class AotDetector {
*/ */
public static final String AOT_ENABLED = "spring.aot.enabled"; public static final String AOT_ENABLED = "spring.aot.enabled";
private static final boolean inNativeImage = NativeDetector.inNativeImage(Context.RUNTIME, Context.BUILD_TIME); private static final boolean inNativeImage = NativeDetector.inNativeImage(Context.RUN, Context.BUILD);
/** /**
* Determine whether AOT optimizations must be considered at runtime. This * Determine whether AOT optimizations must be considered at runtime. This

View File

@ -32,6 +32,7 @@ public abstract class NativeDetector {
private static final boolean inNativeImage = (imageCode != null); private static final boolean inNativeImage = (imageCode != null);
/** /**
* Returns {@code true} if running in a native image context (for example * Returns {@code true} if running in a native image context (for example
* {@code buildtime}, {@code runtime}, or {@code agent}) expressed by setting the * {@code buildtime}, {@code runtime}, or {@code agent}) expressed by setting the
@ -55,10 +56,10 @@ public abstract class NativeDetector {
return false; return false;
} }
/** /**
* Native image context as defined in GraalVM's * Native image context as defined in GraalVM's
* <a href="https://github.com/oracle/graal/blob/master/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/ImageInfo.java">ImageInfo</a>. * <a href="https://github.com/oracle/graal/blob/master/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/ImageInfo.java">ImageInfo</a>.
*
* @since 6.0.10 * @since 6.0.10
*/ */
public enum Context { public enum Context {
@ -66,12 +67,12 @@ public abstract class NativeDetector {
/** /**
* The code is executing in the context of image building. * The code is executing in the context of image building.
*/ */
BUILD_TIME("buildtime"), BUILD("buildtime"),
/** /**
* The code is executing at image runtime. * The code is executing at image runtime.
*/ */
RUNTIME("runtime"); RUN("runtime");
private final String key; private final String key;
@ -83,7 +84,6 @@ public abstract class NativeDetector {
public String toString() { public String toString() {
return this.key; return this.key;
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -35,49 +35,52 @@ import org.springframework.transaction.support.TransactionSynchronizationUtils;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* {@link org.springframework.transaction.PlatformTransactionManager} * {@link org.springframework.transaction.PlatformTransactionManager} implementation
* implementation for a single JDBC {@link javax.sql.DataSource}. This class is * for a single JDBC {@link javax.sql.DataSource}. This class is capable of working
* capable of working in any environment with any JDBC driver, as long as the setup * in any environment with any JDBC driver, as long as the setup uses a
* uses a {@code javax.sql.DataSource} as its {@code Connection} factory mechanism. * {@code javax.sql.DataSource} as its {@code Connection} factory mechanism.
* Binds a JDBC Connection from the specified DataSource to the current thread, * Binds a JDBC {@code Connection} from the specified {@code DataSource} to the
* potentially allowing for one thread-bound Connection per DataSource. * current thread, potentially allowing for one thread-bound {@code Connection}
* per {@code DataSource}.
* *
* <p><b>Note: The DataSource that this transaction manager operates on needs * <p><b>Note: The {@code DataSource} that this transaction manager operates on
* to return independent Connections.</b> The Connections may come from a pool * needs to return independent {@code Connection}s.</b> The {@code Connection}s
* (the typical case), but the DataSource must not return thread-scoped / * typically come from a connection pool but the {@code DataSource} must not return
* request-scoped Connections or the like. This transaction manager will * specifically scoped or constrained {@code Connection}s. This transaction manager
* associate Connections with thread-bound transactions itself, according * will associate {@code Connection}s with thread-bound transactions, according
* to the specified propagation behavior. It assumes that a separate, * to the specified propagation behavior. It assumes that a separate, independent
* independent Connection can be obtained even during an ongoing transaction. * {@code Connection} can be obtained even during an ongoing transaction.
* *
* <p>Application code is required to retrieve the JDBC Connection via * <p>Application code is required to retrieve the JDBC {@code Connection} via
* {@link DataSourceUtils#getConnection(DataSource)} instead of a standard * {@link DataSourceUtils#getConnection(DataSource)} instead of a standard
* Jakarta EE-style {@link DataSource#getConnection()} call. Spring classes such as * EE-style {@link DataSource#getConnection()} call. Spring classes such as
* {@link org.springframework.jdbc.core.JdbcTemplate} use this strategy implicitly. * {@link org.springframework.jdbc.core.JdbcTemplate} use this strategy implicitly.
* If not used in combination with this transaction manager, the * If not used in combination with this transaction manager, the
* {@link DataSourceUtils} lookup strategy behaves exactly like the native * {@link DataSourceUtils} lookup strategy behaves exactly like the native
* DataSource lookup; it can thus be used in a portable fashion. * {@code DataSource} lookup; it can thus be used in a portable fashion.
* *
* <p>Alternatively, you can allow application code to work with the standard * <p>Alternatively, you can allow application code to work with the standard
* Jakarta EE-style lookup pattern {@link DataSource#getConnection()}, for example for * EE-style lookup pattern {@link DataSource#getConnection()}, for example
* legacy code that is not aware of Spring at all. In that case, define a * for legacy code that is not aware of Spring at all. In that case, define a
* {@link TransactionAwareDataSourceProxy} for your target DataSource, and pass * {@link TransactionAwareDataSourceProxy} for your target {@code DataSource},
* that proxy DataSource to your DAOs, which will automatically participate in * and pass that proxy {@code DataSource} to your DAOs which will automatically
* Spring-managed transactions when accessing it. * participate in Spring-managed transactions when accessing it.
* *
* <p>Supports custom isolation levels, and timeouts which get applied as * <p>Supports custom isolation levels, and timeouts which get applied as
* appropriate JDBC statement timeouts. To support the latter, application code * appropriate JDBC statement timeouts. To support the latter, application code
* must either use {@link org.springframework.jdbc.core.JdbcTemplate}, call * must either use {@link org.springframework.jdbc.core.JdbcTemplate}, call
* {@link DataSourceUtils#applyTransactionTimeout} for each created JDBC Statement, * {@link DataSourceUtils#applyTransactionTimeout} for each created JDBC
* or go through a {@link TransactionAwareDataSourceProxy} which will create * {@code Statement}, or go through a {@link TransactionAwareDataSourceProxy}
* timeout-aware JDBC Connections and Statements automatically. * which will create timeout-aware JDBC {@code Connection}s and {@code Statement}s
* automatically.
* *
* <p>Consider defining a {@link LazyConnectionDataSourceProxy} for your target * <p>Consider defining a {@link LazyConnectionDataSourceProxy} for your target
* DataSource, pointing both this transaction manager and your DAOs to it. * {@code DataSource}, pointing both this transaction manager and your DAOs to it.
* This will lead to optimized handling of "empty" transactions, i.e. of transactions * This will lead to optimized handling of "empty" transactions, i.e. of transactions
* without any JDBC statements executed. A LazyConnectionDataSourceProxy will not fetch * without any JDBC statements executed. A {@code LazyConnectionDataSourceProxy} will
* an actual JDBC Connection from the target DataSource until a Statement gets executed, * not fetch an actual JDBC {@code Connection} from the target {@code DataSource}
* lazily applying the specified transaction settings to the target Connection. * until a {@code Statement} gets executed, lazily applying the specified transaction
* settings to the target {@code Connection}.
* *
* <p>This transaction manager supports nested transactions via the JDBC 3.0 * <p>This transaction manager supports nested transactions via the JDBC 3.0
* {@link java.sql.Savepoint} mechanism. The * {@link java.sql.Savepoint} mechanism. The
@ -88,9 +91,9 @@ import org.springframework.util.Assert;
* <p>This transaction manager can be used as a replacement for the * <p>This transaction manager can be used as a replacement for the
* {@link org.springframework.transaction.jta.JtaTransactionManager} in the single * {@link org.springframework.transaction.jta.JtaTransactionManager} in the single
* resource case, as it does not require a container that supports JTA, typically * resource case, as it does not require a container that supports JTA, typically
* in combination with a locally defined JDBC DataSource (e.g. an Apache Commons * in combination with a locally defined JDBC {@code DataSource} (e.g. a Hikari
* DBCP connection pool). Switching between this local strategy and a JTA * connection pool). Switching between this local strategy and a JTA environment
* environment is just a matter of configuration! * is just a matter of configuration!
* *
* <p>As of 4.3.4, this transaction manager triggers flush callbacks on registered * <p>As of 4.3.4, this transaction manager triggers flush callbacks on registered
* transaction synchronizations (if synchronization is generally active), assuming * transaction synchronizations (if synchronization is generally active), assuming
@ -112,6 +115,7 @@ import org.springframework.util.Assert;
* @see TransactionAwareDataSourceProxy * @see TransactionAwareDataSourceProxy
* @see LazyConnectionDataSourceProxy * @see LazyConnectionDataSourceProxy
* @see org.springframework.jdbc.core.JdbcTemplate * @see org.springframework.jdbc.core.JdbcTemplate
* @see org.springframework.jdbc.support.JdbcTransactionManager
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
@ -124,8 +128,8 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
/** /**
* Create a new DataSourceTransactionManager instance. * Create a new {@code DataSourceTransactionManager} instance.
* A DataSource has to be set to be able to use it. * A {@code DataSource} has to be set to be able to use it.
* @see #setDataSource * @see #setDataSource
*/ */
public DataSourceTransactionManager() { public DataSourceTransactionManager() {
@ -133,7 +137,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
} }
/** /**
* Create a new DataSourceTransactionManager instance. * Create a new {@code DataSourceTransactionManager} instance.
* @param dataSource the JDBC DataSource to manage transactions for * @param dataSource the JDBC DataSource to manage transactions for
*/ */
public DataSourceTransactionManager(DataSource dataSource) { public DataSourceTransactionManager(DataSource dataSource) {
@ -144,22 +148,22 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
/** /**
* Set the JDBC DataSource that this instance should manage transactions for. * Set the JDBC {@code DataSource} that this instance should manage transactions for.
* <p>This will typically be a locally defined DataSource, for example an * <p>This will typically be a locally defined {@code DataSource}, for example a
* Apache Commons DBCP connection pool. Alternatively, you can also drive * Hikari connection pool. Alternatively, you can also manage transactions for a
* transactions for a non-XA J2EE DataSource fetched from JNDI. For an XA * non-XA {@code DataSource} fetched from JNDI. For an XA {@code DataSource},
* DataSource, use JtaTransactionManager. * use {@link org.springframework.transaction.jta.JtaTransactionManager} instead.
* <p>The DataSource specified here should be the target DataSource to manage * <p>The {@code DataSource} specified here should be the target {@code DataSource}
* transactions for, not a TransactionAwareDataSourceProxy. Only data access * to manage transactions for, not a {@link TransactionAwareDataSourceProxy}.
* code may work with TransactionAwareDataSourceProxy, while the transaction * Only data access code may work with {@code TransactionAwareDataSourceProxy} while
* manager needs to work on the underlying target DataSource. If there's * the transaction manager needs to work on the underlying target {@code DataSource}.
* nevertheless a TransactionAwareDataSourceProxy passed in, it will be * If there is nevertheless a {@code TransactionAwareDataSourceProxy} passed in,
* unwrapped to extract its target DataSource. * it will be unwrapped to extract its target {@code DataSource}.
* <p><b>The DataSource passed in here needs to return independent Connections.</b> * <p><b>The {@code DataSource} passed in here needs to return independent
* The Connections may come from a pool (the typical case), but the DataSource * {@code Connection}s.</b> The {@code Connection}s typically come from a
* must not return thread-scoped / request-scoped Connections or the like. * connection pool but the {@code DataSource} must not return specifically
* @see TransactionAwareDataSourceProxy * scoped or constrained {@code Connection}s, just possibly lazily fetched.
* @see org.springframework.transaction.jta.JtaTransactionManager * @see LazyConnectionDataSourceProxy
*/ */
public void setDataSource(@Nullable DataSource dataSource) { public void setDataSource(@Nullable DataSource dataSource) {
if (dataSource instanceof TransactionAwareDataSourceProxy tadsp) { if (dataSource instanceof TransactionAwareDataSourceProxy tadsp) {
@ -174,7 +178,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
} }
/** /**
* Return the JDBC DataSource that this instance manages transactions for. * Return the JDBC {@code DataSource} that this instance manages transactions for.
*/ */
@Nullable @Nullable
public DataSource getDataSource() { public DataSource getDataSource() {
@ -182,7 +186,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
} }
/** /**
* Obtain the DataSource for actual use. * Obtain the {@code DataSource} for actual use.
* @return the DataSource (never {@code null}) * @return the DataSource (never {@code null})
* @throws IllegalStateException in case of no DataSource set * @throws IllegalStateException in case of no DataSource set
* @since 5.0 * @since 5.0

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -33,9 +33,9 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* Helper class that provides static methods for obtaining JDBC Connections from * Helper class that provides static methods for obtaining JDBC {@code Connection}s
* a {@link javax.sql.DataSource}. Includes special support for Spring-managed * from a {@link javax.sql.DataSource}. Includes special support for Spring-managed
* transactional Connections, e.g. managed by {@link DataSourceTransactionManager} * transactional {@code Connection}s, e.g. managed by {@link DataSourceTransactionManager}
* or {@link org.springframework.transaction.jta.JtaTransactionManager}. * or {@link org.springframework.transaction.jta.JtaTransactionManager}.
* *
* <p>Used internally by Spring's {@link org.springframework.jdbc.core.JdbcTemplate}, * <p>Used internally by Spring's {@link org.springframework.jdbc.core.JdbcTemplate},
@ -46,7 +46,8 @@ import org.springframework.util.Assert;
* @author Juergen Hoeller * @author Juergen Hoeller
* @see #getConnection * @see #getConnection
* @see #releaseConnection * @see #releaseConnection
* @see DataSourceTransactionManager * @see org.springframework.jdbc.core.JdbcTemplate
* @see org.springframework.jdbc.support.JdbcTransactionManager
* @see org.springframework.transaction.jta.JtaTransactionManager * @see org.springframework.transaction.jta.JtaTransactionManager
* @see org.springframework.transaction.support.TransactionSynchronizationManager * @see org.springframework.transaction.support.TransactionSynchronizationManager
*/ */
@ -310,8 +311,7 @@ public abstract class DataSourceUtils {
} }
/** /**
* Apply the current transaction timeout, if any, * Apply the current transaction timeout, if any, to the given JDBC Statement object.
* to the given JDBC Statement object.
* @param stmt the JDBC Statement object * @param stmt the JDBC Statement object
* @param dataSource the DataSource that the Connection was obtained from * @param dataSource the DataSource that the Connection was obtained from
* @throws SQLException if thrown by JDBC methods * @throws SQLException if thrown by JDBC methods

View File

@ -105,7 +105,7 @@ import org.springframework.util.Assert;
* @see DataSourceUtils#getConnection * @see DataSourceUtils#getConnection
* @see DataSourceUtils#releaseConnection * @see DataSourceUtils#releaseConnection
* @see org.springframework.jdbc.core.JdbcTemplate * @see org.springframework.jdbc.core.JdbcTemplate
* @see org.springframework.jdbc.datasource.DataSourceTransactionManager * @see org.springframework.jdbc.support.JdbcTransactionManager
* @see org.springframework.transaction.jta.JtaTransactionManager * @see org.springframework.transaction.jta.JtaTransactionManager
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")

View File

@ -110,7 +110,7 @@ import org.springframework.util.CollectionUtils;
* @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
* @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
* @see org.springframework.jdbc.core.JdbcTemplate * @see org.springframework.jdbc.core.JdbcTemplate
* @see org.springframework.jdbc.datasource.DataSourceTransactionManager * @see org.springframework.jdbc.support.JdbcTransactionManager
* @see org.springframework.transaction.jta.JtaTransactionManager * @see org.springframework.transaction.jta.JtaTransactionManager
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,11 +41,20 @@ import org.springframework.util.Assert;
*/ */
public class ConnectionHolder extends ResourceHolderSupport { public class ConnectionHolder extends ResourceHolderSupport {
/**
* Prefix for savepoint names.
* @since 6.0.10
*/
static final String SAVEPOINT_NAME_PREFIX = "SAVEPOINT_";
@Nullable @Nullable
private Connection currentConnection; private Connection currentConnection;
private boolean transactionActive; private boolean transactionActive;
private int savepointCounter = 0;
/** /**
* Create a new ConnectionHolder for the given R2DBC {@link Connection}, * Create a new ConnectionHolder for the given R2DBC {@link Connection},
@ -112,6 +121,17 @@ public class ConnectionHolder extends ResourceHolderSupport {
return this.currentConnection; return this.currentConnection;
} }
/**
* Create a new savepoint for the current {@link Connection},
* using generated savepoint names that are unique for the Connection.
* @return the name of the new savepoint
* @since 6.0.10
*/
String nextSavepoint() {
this.savepointCounter++;
return SAVEPOINT_NAME_PREFIX + this.savepointCounter;
}
/** /**
* Releases the current {@link Connection}. * Releases the current {@link Connection}.
*/ */

View File

@ -24,7 +24,6 @@ import io.r2dbc.spi.IsolationLevel;
import io.r2dbc.spi.Option; import io.r2dbc.spi.Option;
import io.r2dbc.spi.R2dbcException; import io.r2dbc.spi.R2dbcException;
import io.r2dbc.spi.Result; import io.r2dbc.spi.Result;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
@ -38,46 +37,42 @@ import org.springframework.transaction.reactive.TransactionSynchronizationManage
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* {@link org.springframework.transaction.ReactiveTransactionManager} * {@link org.springframework.transaction.ReactiveTransactionManager} implementation
* implementation for a single R2DBC {@link ConnectionFactory}. This class is * for a single R2DBC {@link ConnectionFactory}. This class is capable of working
* capable of working in any environment with any R2DBC driver, as long as the * in any environment with any R2DBC driver, as long as the setup uses a
* setup uses a {@code ConnectionFactory} as its {@link Connection} factory * {@code ConnectionFactory} as its {@link Connection} factory mechanism.
* mechanism. Binds a R2DBC {@code Connection} from the specified * Binds a R2DBC {@code Connection} from the specified {@code ConnectionFactory}
* {@code ConnectionFactory} to the current subscriber context, potentially * to the current subscriber context, potentially allowing for one context-bound
* allowing for one context-bound {@code Connection} per {@code ConnectionFactory}. * {@code Connection} per {@code ConnectionFactory}.
* *
* <p><b>Note: The {@code ConnectionFactory} that this transaction manager * <p><b>Note: The {@code ConnectionFactory} that this transaction manager operates
* operates on needs to return independent {@code Connection}s.</b> * on needs to return independent {@code Connection}s.</b> The {@code Connection}s
* The {@code Connection}s may come from a pool (the typical case), but the * typically come from a connection pool but the {@code ConnectionFactory} must not
* {@code ConnectionFactory} must not return scoped {@code Connection}s * return specifically scoped or constrained {@code Connection}s. This transaction
* or the like. This transaction manager will associate {@code Connection} * manager will associate {@code Connection} with context-bound transactions,
* with context-bound transactions itself, according to the specified propagation * according to the specified propagation behavior. It assumes that a separate,
* behavior. It assumes that a separate, independent {@code Connection} can * independent {@code Connection} can be obtained even during an ongoing transaction.
* be obtained even during an ongoing transaction.
* *
* <p>Application code is required to retrieve the R2DBC Connection via * <p>Application code is required to retrieve the R2DBC Connection via
* {@link ConnectionFactoryUtils#getConnection(ConnectionFactory)} * {@link ConnectionFactoryUtils#getConnection(ConnectionFactory)}
* instead of a standard R2DBC-style {@link ConnectionFactory#create()} call. * instead of a standard R2DBC-style {@link ConnectionFactory#create()} call.
* Spring classes such as {@code DatabaseClient} use this strategy implicitly. * Spring classes such as {@code DatabaseClient} use this strategy implicitly.
* If not used in combination with this transaction manager, the * If not used in combination with this transaction manager, the
* {@link ConnectionFactoryUtils} lookup strategy behaves exactly like the * {@link ConnectionFactoryUtils} lookup strategy behaves exactly like the native
* native {@code ConnectionFactory} lookup; it can thus be used in a portable fashion. * {@code ConnectionFactory} lookup; it can thus be used in a portable fashion.
* *
* <p>Alternatively, you can allow application code to work with the standard * <p>Alternatively, you can allow application code to work with the lookup pattern
* R2DBC lookup pattern {@link ConnectionFactory#create()}, for example for code * {@link ConnectionFactory#create()}, for example for code not aware of Spring.
* that is not aware of Spring at all. In that case, define a * In that case, define a {@link TransactionAwareConnectionFactoryProxy} for your
* {@link TransactionAwareConnectionFactoryProxy} for your target {@code ConnectionFactory}, * target {@code ConnectionFactory}, and pass that proxy {@code ConnectionFactory}
* and pass that proxy {@code ConnectionFactory} to your DAOs, which will automatically * to your DAOs which will automatically participate in Spring-managed transactions
* participate in Spring-managed transactions when accessing it. * when accessing it.
* *
* <p>This transaction manager triggers flush callbacks on registered transaction * <p>Spring's {@code TransactionDefinition} attributes are carried forward to
* synchronizations (if synchronization is generally active), assuming resources * R2DBC drivers using extensible R2DBC {@link io.r2dbc.spi.TransactionDefinition}.
* operating on the underlying R2DBC {@code Connection}. * Subclasses may override {@link #createTransactionDefinition(TransactionDefinition)}
* * to customize transaction definitions for vendor-specific attributes. As of 6.0.10,
* <p>Spring's {@code TransactionDefinition} attributes are carried forward to R2DBC drivers * this transaction manager supports nested transactions via R2DBC savepoints as well.
* using extensible R2DBC {@link io.r2dbc.spi.TransactionDefinition}. Subclasses may
* override {@link #createTransactionDefinition(TransactionDefinition)} to customize
* transaction definitions for vendor-specific attributes.
* *
* @author Mark Paluch * @author Mark Paluch
* @author Juergen Hoeller * @author Juergen Hoeller
@ -97,7 +92,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
/** /**
* Create a new {@code R2dbcTransactionManager} instance. * Create a new {@code R2dbcTransactionManager} instance.
* A ConnectionFactory has to be set to be able to use it. * A {@code ConnectionFactory} has to be set to be able to use it.
* @see #setConnectionFactory * @see #setConnectionFactory
*/ */
public R2dbcTransactionManager() {} public R2dbcTransactionManager() {}
@ -114,12 +109,13 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
/** /**
* Set the R2DBC {@link ConnectionFactory} that this instance should manage transactions for. * Set the R2DBC {@link ConnectionFactory} that this instance should manage transactions
* <p>This will typically be a locally defined {@code ConnectionFactory}, for example a connection pool. * for. This will typically be a locally defined {@code ConnectionFactory}, for example
* <p><b>The {@code ConnectionFactory} passed in here needs to return independent {@link Connection}s.</b> * an R2DBC connection pool.
* The {@code Connection}s may come from a pool (the typical case), but the {@code ConnectionFactory} * <p><b>The {@code ConnectionFactory} passed in here needs to return independent
* must not return scoped {@code Connection}s or the like. * {@link Connection}s.</b> The {@code Connection}s typically come from a connection
* @see TransactionAwareConnectionFactoryProxy * pool but the {@code ConnectionFactory} must not return specifically scoped or
* constrained {@code Connection}s.
*/ */
public void setConnectionFactory(@Nullable ConnectionFactory connectionFactory) { public void setConnectionFactory(@Nullable ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory; this.connectionFactory = connectionFactory;
@ -183,8 +179,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
@Override @Override
protected boolean isExistingTransaction(Object transaction) { protected boolean isExistingTransaction(Object transaction) {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; return ((ConnectionFactoryTransactionObject) transaction).isTransactionActive();
return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
} }
@Override @Override
@ -193,6 +188,11 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED &&
txObject.isTransactionActive()) {
return txObject.createSavepoint();
}
return Mono.defer(() -> { return Mono.defer(() -> {
Mono<Connection> connectionMono; Mono<Connection> connectionMono;
@ -210,7 +210,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
connectionMono = Mono.just(txObject.getConnectionHolder().getConnection()); connectionMono = Mono.just(txObject.getConnectionHolder().getConnection());
} }
return connectionMono.flatMap(con -> Mono.from(doBegin(definition, con)) return connectionMono.flatMap(con -> doBegin(definition, con)
.then(prepareTransactionalConnection(con, definition)) .then(prepareTransactionalConnection(con, definition))
.doOnSuccess(v -> { .doOnSuccess(v -> {
txObject.getConnectionHolder().setTransactionActive(true); txObject.getConnectionHolder().setTransactionActive(true);
@ -234,12 +234,12 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
}).then(); }).then();
} }
private Publisher<Void> doBegin(TransactionDefinition definition, Connection con) { private Mono<Void> doBegin(TransactionDefinition definition, Connection con) {
io.r2dbc.spi.TransactionDefinition transactionDefinition = createTransactionDefinition(definition); io.r2dbc.spi.TransactionDefinition transactionDefinition = createTransactionDefinition(definition);
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Starting R2DBC transaction on Connection [" + con + "] using [" + transactionDefinition + "]"); logger.debug("Starting R2DBC transaction on Connection [" + con + "] using [" + transactionDefinition + "]");
} }
return con.beginTransaction(transactionDefinition); return Mono.from(con.beginTransaction(transactionDefinition));
} }
/** /**
@ -300,12 +300,11 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
GenericReactiveTransaction status) throws TransactionException { GenericReactiveTransaction status) throws TransactionException {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction();
Connection connection = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) { if (status.isDebug()) {
logger.debug("Committing R2DBC transaction on Connection [" + connection + "]"); logger.debug("Committing R2DBC transaction on Connection [" +
txObject.getConnectionHolder().getConnection() + "]");
} }
return Mono.from(connection.commitTransaction()) return txObject.commit().onErrorMap(R2dbcException.class, ex -> translateException("R2DBC commit", ex));
.onErrorMap(R2dbcException.class, ex -> translateException("R2DBC commit", ex));
} }
@Override @Override
@ -313,12 +312,11 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
GenericReactiveTransaction status) throws TransactionException { GenericReactiveTransaction status) throws TransactionException {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction();
Connection connection = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) { if (status.isDebug()) {
logger.debug("Rolling back R2DBC transaction on Connection [" + connection + "]"); logger.debug("Rolling back R2DBC transaction on Connection [" +
txObject.getConnectionHolder().getConnection() + "]");
} }
return Mono.from(connection.rollbackTransaction()) return txObject.rollback().onErrorMap(R2dbcException.class, ex -> translateException("R2DBC rollback", ex));
.onErrorMap(R2dbcException.class, ex -> translateException("R2DBC rollback", ex));
} }
@Override @Override
@ -496,6 +494,9 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
private boolean newConnectionHolder; private boolean newConnectionHolder;
@Nullable
private String savepointName;
void setConnectionHolder(@Nullable ConnectionHolder connectionHolder, boolean newConnectionHolder) { void setConnectionHolder(@Nullable ConnectionHolder connectionHolder, boolean newConnectionHolder) {
setConnectionHolder(connectionHolder); setConnectionHolder(connectionHolder);
this.newConnectionHolder = newConnectionHolder; this.newConnectionHolder = newConnectionHolder;
@ -505,10 +506,6 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
return this.newConnectionHolder; return this.newConnectionHolder;
} }
void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
public void setConnectionHolder(@Nullable ConnectionHolder connectionHolder) { public void setConnectionHolder(@Nullable ConnectionHolder connectionHolder) {
this.connectionHolder = connectionHolder; this.connectionHolder = connectionHolder;
} }
@ -521,6 +518,34 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
public boolean hasConnectionHolder() { public boolean hasConnectionHolder() {
return (this.connectionHolder != null); return (this.connectionHolder != null);
} }
public boolean isTransactionActive() {
return (this.connectionHolder != null && this.connectionHolder.isTransactionActive());
}
public Mono<Void> createSavepoint() {
ConnectionHolder holder = getConnectionHolder();
this.savepointName = holder.nextSavepoint();
return Mono.from(holder.getConnection().createSavepoint(this.savepointName));
}
public Mono<Void> commit() {
Connection connection = getConnectionHolder().getConnection();
return (this.savepointName != null ?
Mono.from(connection.releaseSavepoint(this.savepointName)) :
Mono.from(connection.commitTransaction()));
}
public Mono<Void> rollback() {
Connection connection = getConnectionHolder().getConnection();
return (this.savepointName != null ?
Mono.from(connection.rollbackTransactionToSavepoint(this.savepointName)) :
Mono.from(connection.rollbackTransaction()));
}
public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
} }
} }

View File

@ -231,7 +231,6 @@ class R2dbcTransactionManagerUnitTests {
@Test @Test
void testCommitFails() { void testCommitFails() {
when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail")))); when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail"))));
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty()); when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
TransactionalOperator operator = TransactionalOperator.create(tm); TransactionalOperator operator = TransactionalOperator.create(tm);
@ -252,7 +251,6 @@ class R2dbcTransactionManagerUnitTests {
@Test @Test
void testRollback() { void testRollback() {
AtomicInteger commits = new AtomicInteger(); AtomicInteger commits = new AtomicInteger();
when(connectionMock.commitTransaction()).thenReturn( when(connectionMock.commitTransaction()).thenReturn(
Mono.fromRunnable(commits::incrementAndGet)); Mono.fromRunnable(commits::incrementAndGet));
@ -284,11 +282,8 @@ class R2dbcTransactionManagerUnitTests {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail"))), Mono.empty()); when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail"))), Mono.empty());
TransactionalOperator operator = TransactionalOperator.create(tm); TransactionalOperator operator = TransactionalOperator.create(tm);
operator.execute(reactiveTransaction -> { operator.execute(reactiveTransaction -> {
reactiveTransaction.setRollbackOnly(); reactiveTransaction.setRollbackOnly();
return ConnectionFactoryUtils.getConnection(connectionFactoryMock) return ConnectionFactoryUtils.getConnection(connectionFactoryMock)
.doOnNext(connection -> connection.createStatement("foo")).then(); .doOnNext(connection -> connection.createStatement("foo")).then();
}).as(StepVerifier::create) }).as(StepVerifier::create)
@ -306,11 +301,9 @@ class R2dbcTransactionManagerUnitTests {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
void testConnectionReleasedWhenRollbackFails() { void testConnectionReleasedWhenRollbackFails() {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Rollback should fail"))), Mono.empty()); when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Rollback should fail"))), Mono.empty());
TransactionalOperator operator = TransactionalOperator.create(tm);
when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty()); when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty());
TransactionalOperator operator = TransactionalOperator.create(tm);
operator.execute(reactiveTransaction -> ConnectionFactoryUtils.getConnection(connectionFactoryMock) operator.execute(reactiveTransaction -> ConnectionFactoryUtils.getConnection(connectionFactoryMock)
.doOnNext(connection -> { .doOnNext(connection -> {
throw new IllegalStateException("Intentional error to trigger rollback"); throw new IllegalStateException("Intentional error to trigger rollback");
@ -333,12 +326,9 @@ class R2dbcTransactionManagerUnitTests {
TransactionSynchronization.STATUS_ROLLED_BACK); TransactionSynchronization.STATUS_ROLLED_BACK);
TransactionalOperator operator = TransactionalOperator.create(tm); TransactionalOperator operator = TransactionalOperator.create(tm);
operator.execute(tx -> { operator.execute(tx -> {
tx.setRollbackOnly(); tx.setRollbackOnly();
assertThat(tx.isNewTransaction()).isTrue(); assertThat(tx.isNewTransaction()).isTrue();
return TransactionSynchronizationManager.forCurrentTransaction().doOnNext( return TransactionSynchronizationManager.forCurrentTransaction().doOnNext(
synchronizationManager -> { synchronizationManager -> {
assertThat(synchronizationManager.hasResource(connectionFactoryMock)).isTrue(); assertThat(synchronizationManager.hasResource(connectionFactoryMock)).isTrue();
@ -364,15 +354,12 @@ class R2dbcTransactionManagerUnitTests {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionalOperator operator = TransactionalOperator.create(tm, definition); TransactionalOperator operator = TransactionalOperator.create(tm, definition);
operator.execute(tx1 -> { operator.execute(tx1 -> {
assertThat(tx1.isNewTransaction()).isTrue(); assertThat(tx1.isNewTransaction()).isTrue();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER);
return operator.execute(tx2 -> { return operator.execute(tx2 -> {
fail("Should have thrown IllegalTransactionStateException"); fail("Should have thrown IllegalTransactionStateException");
return Mono.empty(); return Mono.empty();
}); });
@ -383,25 +370,122 @@ class R2dbcTransactionManagerUnitTests {
verify(connectionMock).close(); verify(connectionMock).close();
} }
@Test
void testPropagationNestedWithExistingTransaction() {
when(connectionMock.createSavepoint("SAVEPOINT_1")).thenReturn(Mono.empty());
when(connectionMock.releaseSavepoint("SAVEPOINT_1")).thenReturn(Mono.empty());
when(connectionMock.commitTransaction()).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_NESTED);
return operator.execute(tx2 -> {
assertThat(tx2.isNewTransaction()).isTrue();
return Mono.empty();
});
}).as(StepVerifier::create)
.verifyComplete();
verify(connectionMock).createSavepoint("SAVEPOINT_1");
verify(connectionMock).releaseSavepoint("SAVEPOINT_1");
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
}
@Test
void testPropagationNestedWithExistingTransactionAndRollback() {
when(connectionMock.createSavepoint("SAVEPOINT_1")).thenReturn(Mono.empty());
when(connectionMock.rollbackTransactionToSavepoint("SAVEPOINT_1")).thenReturn(Mono.empty());
when(connectionMock.commitTransaction()).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_NESTED);
return operator.execute(tx2 -> {
assertThat(tx2.isNewTransaction()).isTrue();
tx2.setRollbackOnly();
return Mono.empty();
});
}).as(StepVerifier::create)
.verifyComplete();
verify(connectionMock).createSavepoint("SAVEPOINT_1");
verify(connectionMock).rollbackTransactionToSavepoint("SAVEPOINT_1");
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
}
@Test
void testPropagationSupportsAndNested() {
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_NESTED);
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();
}
@Test
void testPropagationSupportsAndNestedWithRollback() {
when(connectionMock.rollbackTransaction()).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_NESTED);
TransactionalOperator inner = TransactionalOperator.create(tm, innerDef);
return inner.execute(tx2 -> {
assertThat(tx2.isNewTransaction()).isTrue();
tx2.setRollbackOnly();
return Mono.empty();
});
}).as(StepVerifier::create)
.verifyComplete();
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
}
@Test @Test
void testPropagationSupportsAndRequiresNew() { void testPropagationSupportsAndRequiresNew() {
when(connectionMock.commitTransaction()).thenReturn(Mono.empty()); when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
TransactionalOperator operator = TransactionalOperator.create(tm, definition); TransactionalOperator operator = TransactionalOperator.create(tm, definition);
operator.execute(tx1 -> { operator.execute(tx1 -> {
assertThat(tx1.isNewTransaction()).isFalse(); assertThat(tx1.isNewTransaction()).isFalse();
DefaultTransactionDefinition innerDef = new DefaultTransactionDefinition(); DefaultTransactionDefinition innerDef = new DefaultTransactionDefinition();
innerDef.setPropagationBehavior( innerDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionalOperator inner = TransactionalOperator.create(tm, innerDef); TransactionalOperator inner = TransactionalOperator.create(tm, innerDef);
return inner.execute(tx2 -> { return inner.execute(tx2 -> {
assertThat(tx2.isNewTransaction()).isTrue(); assertThat(tx2.isNewTransaction()).isTrue();
return Mono.empty(); return Mono.empty();
}); });
@ -412,6 +496,31 @@ class R2dbcTransactionManagerUnitTests {
verify(connectionMock).close(); verify(connectionMock).close();
} }
@Test
void testPropagationSupportsAndRequiresNewWithRollback() {
when(connectionMock.rollbackTransaction()).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();
tx2.setRollbackOnly();
return Mono.empty();
});
}).as(StepVerifier::create)
.verifyComplete();
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
}
private static class TestTransactionSynchronization implements TransactionSynchronization { private static class TestTransactionSynchronization implements TransactionSynchronization {