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.
*
* @author Mark Paluch
* @author Juergen Hoeller
* @since 5.3
* @see ConnectionFactoryUtils#getConnection(ConnectionFactory)
* @see ConnectionFactoryUtils#releaseConnection
@ -149,7 +150,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
* 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}.
* can be customized through {@link #prepareTransactionalConnection}.
* @see #prepareTransactionalConnection
*/
public void setEnforceReadOnly(boolean enforceReadOnly) {
@ -209,8 +210,9 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
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(prepareTransactionalConnection(con, definition))
.doOnSuccess(v -> {
txObject.getConnectionHolder().setTransactionActive(true);
Duration timeout = determineTimeout(definition);
@ -375,32 +377,10 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
});
}
/**
* 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) {
private Mono<Void> switchAutoCommitIfNecessary(Connection con, Object transaction) {
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
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,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
@ -415,6 +395,29 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
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
* {@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.
*/
private record ExtendedTransactionDefinition(@Nullable String transactionName,
boolean readOnly,
@Nullable IsolationLevel isolationLevel,
Duration lockWaitTimeout) implements io.r2dbc.spi.TransactionDefinition {
boolean readOnly, @Nullable IsolationLevel isolationLevel, Duration lockWaitTimeout)
implements io.r2dbc.spi.TransactionDefinition {
private ExtendedTransactionDefinition(@Nullable String transactionName, boolean readOnly,
@Nullable IsolationLevel isolationLevel, Duration lockWaitTimeout) {
this.transactionName = transactionName;
this.readOnly = readOnly;
this.isolationLevel = isolationLevel;
this.lockWaitTimeout = lockWaitTimeout;
}
@Override
@SuppressWarnings("unchecked")
@Override
public <T> T getAttribute(Option<T> option) {
return (T) doGetValue(option);
}