Skip to content

Commit a330277

Browse files
committed
Preserve Connection readOnly state for defaultReadOnly DataSource
Includes DataSourceTransactionManagerTests alignment with main branch. Closes gh-35743
1 parent ae804cb commit a330277

File tree

5 files changed

+681
-793
lines changed

5 files changed

+681
-793
lines changed

spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
126126

127127
private boolean enforceReadOnly = false;
128128

129+
private volatile @Nullable Boolean defaultReadOnly;
130+
129131

130132
/**
131133
* Create a new {@code DataSourceTransactionManager} instance.
@@ -270,13 +272,18 @@ protected void doBegin(Object transaction, TransactionDefinition definition) {
270272
if (logger.isDebugEnabled()) {
271273
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
272274
}
275+
if (definition.isReadOnly()) {
276+
checkDefaultReadOnly(newCon);
277+
}
273278
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
274279
}
275280

276281
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
277282
con = txObject.getConnectionHolder().getConnection();
278283

279-
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
284+
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con,
285+
definition.getIsolationLevel(),
286+
(definition.isReadOnly() && !isDefaultReadOnly()));
280287
txObject.setPreviousIsolationLevel(previousIsolationLevel);
281288
txObject.setReadOnly(definition.isReadOnly());
282289

@@ -381,8 +388,9 @@ protected void doCleanupAfterCompletion(Object transaction) {
381388
if (txObject.isMustRestoreAutoCommit()) {
382389
con.setAutoCommit(true);
383390
}
384-
DataSourceUtils.resetConnectionAfterTransaction(
385-
con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
391+
DataSourceUtils.resetConnectionAfterTransaction(con,
392+
txObject.getPreviousIsolationLevel(),
393+
(txObject.isReadOnly() && !isDefaultReadOnly()));
386394
}
387395
catch (Throwable ex) {
388396
logger.debug("Could not reset JDBC Connection after transaction", ex);
@@ -399,6 +407,37 @@ protected void doCleanupAfterCompletion(Object transaction) {
399407
}
400408

401409

410+
/**
411+
* Check the default {@link Connection#isReadOnly()} flag on a freshly
412+
* obtained connection from the {@code DataSource}, assuming that the
413+
* same flag applies to all connections obtained from the given setup.
414+
* @param newCon the Connection to check
415+
* @since 6.2.13
416+
* @see #isDefaultReadOnly()
417+
*/
418+
private void checkDefaultReadOnly(Connection newCon) {
419+
if (this.defaultReadOnly == null) {
420+
try {
421+
this.defaultReadOnly = newCon.isReadOnly();
422+
}
423+
catch (Throwable ex) {
424+
logger.debug("Could not determine default JDBC Connection isReadOnly - assuming false", ex);
425+
this.defaultReadOnly = false;
426+
}
427+
}
428+
}
429+
430+
/**
431+
* Check whether the default read-only flag has been determined as {@code true},
432+
* assuming that all encountered connections will be read-only by default and
433+
* therefore do not need explicit {@link Connection#setReadOnly} (re)setting.
434+
* @since 6.2.13
435+
* @see #checkDefaultReadOnly(Connection)
436+
*/
437+
private boolean isDefaultReadOnly() {
438+
return (this.defaultReadOnly == Boolean.TRUE);
439+
}
440+
402441
/**
403442
* Prepare the transactional {@code Connection} right after transaction begin.
404443
* <p>The default implementation executes a "SET TRANSACTION READ ONLY" statement

spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,19 +170,38 @@ private static Connection fetchConnection(DataSource dataSource) throws SQLExcep
170170
* @param definition the transaction definition to apply
171171
* @return the previous isolation level, if any
172172
* @throws SQLException if thrown by JDBC methods
173-
* @see #resetConnectionAfterTransaction
173+
* @see #prepareConnectionForTransaction(Connection, int, boolean)
174+
*/
175+
@Nullable
176+
public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition)
177+
throws SQLException {
178+
179+
return prepareConnectionForTransaction(con,
180+
(definition != null ? definition.getIsolationLevel() : TransactionDefinition.ISOLATION_DEFAULT),
181+
(definition != null && definition.isReadOnly()));
182+
}
183+
184+
/**
185+
* Prepare the given Connection with the given transaction semantics.
186+
* @param con the Connection to prepare
187+
* @param isolationLevel the isolation level to apply
188+
* @param setReadOnly whether to set the read-only flag
189+
* @return the previous isolation level, if any
190+
* @throws SQLException if thrown by JDBC methods
191+
* @since 6.2.13
192+
* @see #resetConnectionAfterTransaction(Connection, Integer, boolean)
174193
* @see Connection#setTransactionIsolation
175194
* @see Connection#setReadOnly
176195
*/
177196
@Nullable
178-
public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition)
197+
static Integer prepareConnectionForTransaction(Connection con, int isolationLevel, boolean setReadOnly)
179198
throws SQLException {
180199

181200
Assert.notNull(con, "No Connection specified");
182201

183202
boolean debugEnabled = logger.isDebugEnabled();
184203
// Set read-only flag.
185-
if (definition != null && definition.isReadOnly()) {
204+
if (setReadOnly) {
186205
try {
187206
if (debugEnabled) {
188207
logger.debug("Setting JDBC Connection [" + con + "] read-only");
@@ -205,15 +224,14 @@ public static Integer prepareConnectionForTransaction(Connection con, @Nullable
205224

206225
// Apply specific isolation level, if any.
207226
Integer previousIsolationLevel = null;
208-
if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
227+
if (isolationLevel != TransactionDefinition.ISOLATION_DEFAULT) {
209228
if (debugEnabled) {
210-
logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " +
211-
definition.getIsolationLevel());
229+
logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + isolationLevel);
212230
}
213231
int currentIsolation = con.getTransactionIsolation();
214-
if (currentIsolation != definition.getIsolationLevel()) {
232+
if (currentIsolation != isolationLevel) {
215233
previousIsolationLevel = currentIsolation;
216-
con.setTransactionIsolation(definition.getIsolationLevel());
234+
con.setTransactionIsolation(isolationLevel);
217235
}
218236
}
219237

spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ public LazyConnectionDataSourceProxy(DataSource targetDataSource) {
153153
*/
154154
public void setReadOnlyDataSource(@Nullable DataSource readOnlyDataSource) {
155155
this.readOnlyDataSource = readOnlyDataSource;
156+
if (getTargetDataSource() == null) {
157+
setTargetDataSource(readOnlyDataSource);
158+
}
156159
}
157160

158161
/**
@@ -395,7 +398,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
395398
return null;
396399
}
397400
case "isReadOnly" -> {
398-
return this.readOnly;
401+
return (this.readOnly || getTargetDataSource() == readOnlyDataSource);
399402
}
400403
case "setReadOnly" -> {
401404
this.readOnly = (Boolean) args[0];

0 commit comments

Comments
 (0)