Apply read-only enforcement after R2DBC transaction begin

Includes prepareTransactionalConnection variant aligned with JDBC DataSourceTransactionManager.

Closes gh-28610
This commit is contained in:
Juergen Hoeller 2022-07-13 10:31:39 +02:00
parent 7055ddb489
commit 56fc64dd14
1 changed files with 32 additions and 30 deletions

View File

@ -80,6 +80,7 @@ import org.springframework.util.Assert;
* transaction definitions for vendor-specific attributes. * transaction definitions for vendor-specific attributes.
* *
* @author Mark Paluch * @author Mark Paluch
* @author Juergen Hoeller
* @since 5.3 * @since 5.3
* @see ConnectionFactoryUtils#getConnection(ConnectionFactory) * @see ConnectionFactoryUtils#getConnection(ConnectionFactory)
* @see ConnectionFactoryUtils#releaseConnection * @see ConnectionFactoryUtils#releaseConnection
@ -149,7 +150,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
* transactional connection: "SET TRANSACTION READ ONLY" as understood by Oracle, * transactional connection: "SET TRANSACTION READ ONLY" as understood by Oracle,
* MySQL and Postgres. * MySQL and Postgres.
* <p>The exact treatment, including any SQL statement executed on the connection, * <p>The exact treatment, including any SQL statement executed on the connection,
* can be customized through through {@link #prepareTransactionalConnection}. * can be customized through {@link #prepareTransactionalConnection}.
* @see #prepareTransactionalConnection * @see #prepareTransactionalConnection
*/ */
public void setEnforceReadOnly(boolean enforceReadOnly) { public void setEnforceReadOnly(boolean enforceReadOnly) {
@ -209,8 +210,9 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
connectionMono = Mono.just(txObject.getConnectionHolder().getConnection()); connectionMono = Mono.just(txObject.getConnectionHolder().getConnection());
} }
return connectionMono.flatMap(con -> prepareTransactionalConnection(con, definition, transaction) return connectionMono.flatMap(con -> switchAutoCommitIfNecessary(con, transaction)
.then(Mono.from(doBegin(definition, con))) .then(Mono.from(doBegin(definition, con)))
.then(prepareTransactionalConnection(con, definition))
.doOnSuccess(v -> { .doOnSuccess(v -> {
txObject.getConnectionHolder().setTransactionActive(true); txObject.getConnectionHolder().setTransactionActive(true);
Duration timeout = determineTimeout(definition); Duration timeout = determineTimeout(definition);
@ -375,32 +377,10 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
}); });
} }
/** private Mono<Void> switchAutoCommitIfNecessary(Connection con, Object transaction) {
* 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
* @param transaction the transaction object
* @see #setEnforceReadOnly
*/
protected Mono<Void> prepareTransactionalConnection(
Connection con, TransactionDefinition definition, Object transaction) {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
Mono<Void> prepare = Mono.empty(); Mono<Void> prepare = Mono.empty();
if (isEnforceReadOnly() && definition.isReadOnly()) {
prepare = Mono.from(con.createStatement("SET TRANSACTION READ ONLY").execute())
.flatMapMany(Result::getRowsUpdated)
.then();
}
// Switch to manual commit if necessary. This is very expensive in some R2DBC drivers, // Switch to manual commit if necessary. This is very expensive in some R2DBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly // so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already). // configured the connection pool to set it already).
@ -415,6 +395,29 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
return prepare; return prepare;
} }
/**
* 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
* @since 5.3.22
* @see #setEnforceReadOnly
*/
protected Mono<Void> prepareTransactionalConnection(Connection con, TransactionDefinition definition) {
Mono<Void> prepare = Mono.empty();
if (isEnforceReadOnly() && definition.isReadOnly()) {
prepare = Mono.from(con.createStatement("SET TRANSACTION READ ONLY").execute())
.flatMapMany(Result::getRowsUpdated)
.then();
}
return prepare;
}
/** /**
* Resolve the {@linkplain TransactionDefinition#getIsolationLevel() isolation level constant} to a R2DBC * Resolve the {@linkplain TransactionDefinition#getIsolationLevel() isolation level constant} to a R2DBC
* {@link IsolationLevel}. If you'd like to extend isolation level translation for vendor-specific * {@link IsolationLevel}. If you'd like to extend isolation level translation for vendor-specific
@ -452,21 +455,20 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
* to R2DBC drivers when starting a transaction. * to R2DBC drivers when starting a transaction.
*/ */
private record ExtendedTransactionDefinition(@Nullable String transactionName, private record ExtendedTransactionDefinition(@Nullable String transactionName,
boolean readOnly, boolean readOnly, @Nullable IsolationLevel isolationLevel, Duration lockWaitTimeout)
@Nullable IsolationLevel isolationLevel, implements io.r2dbc.spi.TransactionDefinition {
Duration lockWaitTimeout) implements io.r2dbc.spi.TransactionDefinition {
private ExtendedTransactionDefinition(@Nullable String transactionName, boolean readOnly, private ExtendedTransactionDefinition(@Nullable String transactionName, boolean readOnly,
@Nullable IsolationLevel isolationLevel, Duration lockWaitTimeout) { @Nullable IsolationLevel isolationLevel, Duration lockWaitTimeout) {
this.transactionName = transactionName; this.transactionName = transactionName;
this.readOnly = readOnly; this.readOnly = readOnly;
this.isolationLevel = isolationLevel; this.isolationLevel = isolationLevel;
this.lockWaitTimeout = lockWaitTimeout; this.lockWaitTimeout = lockWaitTimeout;
} }
@Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override
public <T> T getAttribute(Option<T> option) { public <T> T getAttribute(Option<T> option) {
return (T) doGetValue(option); return (T) doGetValue(option);
} }