Skip to content

Commit 3b4c7a8

Browse files
committed
Revise LazyConnectionDataSourceProxy for late connection properties check
Includes special support for a read-only DataSource in addition to the regular target DataSource, avoiding the overhead of switching the Connection's read-only flag at the beginning and end of every transaction. Closes gh-29931 Closes gh-31785 Closes gh-19688 Closes gh-21415
1 parent 2e07f9a commit 3b4c7a8

File tree

5 files changed

+208
-72
lines changed

5 files changed

+208
-72
lines changed

framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,19 @@ provided you stick to the required connection lookup pattern. Note that JTA does
230230
savepoints or custom isolation levels and has a different timeout mechanism but otherwise
231231
exposes similar behavior in terms of JDBC resources and JDBC commit/rollback management.
232232

233+
For JTA-style lazy retrieval of actual resource connections, Spring provides a
234+
corresponding `DataSource` proxy class for the target connection pool: see
235+
{spring-framework-api}/jdbc/datasource/LazyConnectionDataSourceProxy.html[`LazyConnectionDataSourceProxy`].
236+
This is particularly useful for potentially empty transactions without actual statement
237+
execution (never fetching an actual resource in such a scenario), and also in front of
238+
a routing `DataSource` which means to take the transaction-synchronized read-only flag
239+
and/or isolation level into account (e.g. `IsolationLevelDataSourceRouter`).
240+
241+
`LazyConnectionDataSourceProxy` also provides special support for a read-only connection
242+
pool to use during a read-only transaction, avoiding the overhead of switching the JDBC
243+
Connection's read-only flag at the beginning and end of every transaction when fetching
244+
it from the primary connection pool (which may be costly depending on the JDBC driver).
245+
233246
NOTE: As of 5.3, Spring provides an extended `JdbcTransactionManager` variant which adds
234247
exception translation capabilities on commit/rollback (aligned with `JdbcTemplate`).
235248
Where `DataSourceTransactionManager` will only ever throw `TransactionSystemException`

framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,12 @@ exposes the Hibernate transaction as a JDBC transaction if you have set up the p
407407
`DataSource` for which the transactions are supposed to be exposed through the
408408
`dataSource` property of the `HibernateTransactionManager` class.
409409

410+
For JTA-style lazy retrieval of actual resource connections, Spring provides a
411+
corresponding `DataSource` proxy class for the target connection pool: see
412+
{spring-framework-api}/jdbc/datasource/LazyConnectionDataSourceProxy.html[`LazyConnectionDataSourceProxy`].
413+
This is particularly useful for Hibernate read-only transactions which can often
414+
be processed from a local cache rather than hitting the database.
415+
410416

411417
[[orm-hibernate-resources]]
412418
== Comparing Container-managed and Locally Defined Resources

framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -506,13 +506,20 @@ if you have not already done so, to get more detailed coverage of Spring's decla
506506
The recommended strategy for JPA is local transactions through JPA's native transaction
507507
support. Spring's `JpaTransactionManager` provides many capabilities known from local
508508
JDBC transactions (such as transaction-specific isolation levels and resource-level
509-
read-only optimizations) against any regular JDBC connection pool (no XA requirement).
509+
read-only optimizations) against any regular JDBC connection pool, without requiring
510+
a JTA transaction coordinator and XA-capable resources.
510511

511512
Spring JPA also lets a configured `JpaTransactionManager` expose a JPA transaction
512513
to JDBC access code that accesses the same `DataSource`, provided that the registered
513-
`JpaDialect` supports retrieval of the underlying JDBC `Connection`.
514-
Spring provides dialects for the EclipseLink and Hibernate JPA implementations.
515-
See the xref:data-access/orm/jpa.adoc#orm-jpa-dialect[next section] for details on the `JpaDialect` mechanism.
514+
`JpaDialect` supports retrieval of the underlying JDBC `Connection`. Spring provides
515+
dialects for the EclipseLink and Hibernate JPA implementations. See the
516+
xref:data-access/orm/jpa.adoc#orm-jpa-dialect[next section] for details on `JpaDialect`.
517+
518+
For JTA-style lazy retrieval of actual resource connections, Spring provides a
519+
corresponding `DataSource` proxy class for the target connection pool: see
520+
{spring-framework-api}/jdbc/datasource/LazyConnectionDataSourceProxy.html[`LazyConnectionDataSourceProxy`].
521+
This is particularly useful for JPA read-only transactions which can often
522+
be processed from a local cache rather than hitting the database.
516523

517524

518525
[[orm-jpa-dialect]]

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

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,27 @@
3737
* Proxy for a target DataSource, fetching actual JDBC Connections lazily,
3838
* i.e. not until first creation of a Statement. Connection initialization
3939
* properties like auto-commit mode, transaction isolation and read-only mode
40-
* will be kept and applied to the actual JDBC Connection as soon as an
41-
* actual Connection is fetched (if ever). Consequently, commit and rollback
42-
* calls will be ignored if no Statements have been created.
40+
* will be kept and applied to the actual JDBC Connection as soon as an actual
41+
* Connection is fetched (if ever). Consequently, commit and rollback calls will
42+
* be ignored if no Statements have been created. As of 6.1.2, there is also
43+
* special support for a {@link #setReadOnlyDataSource read-only DataSource} to use
44+
* during a read-only transaction, in addition to the regular target DataSource.
4345
*
4446
* <p>This DataSource proxy allows to avoid fetching JDBC Connections from
4547
* a pool unless actually necessary. JDBC transaction control can happen
4648
* without fetching a Connection from the pool or communicating with the
4749
* database; this will be done lazily on first creation of a JDBC Statement.
50+
* As a bonus, this allows for taking the transaction-synchronized read-only
51+
* flag and/or isolation level into account in a routing DataSource (e.g.
52+
* {@link org.springframework.jdbc.datasource.lookup.IsolationLevelDataSourceRouter}).
4853
*
4954
* <p><b>If you configure both a LazyConnectionDataSourceProxy and a
5055
* TransactionAwareDataSourceProxy, make sure that the latter is the outermost
5156
* DataSource.</b> In such a scenario, data access code will talk to the
5257
* transaction-aware DataSource, which will in turn work with the
53-
* LazyConnectionDataSourceProxy.
58+
* LazyConnectionDataSourceProxy. As of 6.1.2, LazyConnectionDataSourceProxy will
59+
* initialize its default connection characteristics on first Connection access;
60+
* to enforce this on startup, call {@link #checkDefaultConnectionProperties()}.
5461
*
5562
* <p>Lazy fetching of physical JDBC Connections is particularly beneficial
5663
* in a generic transaction demarcation environment. It allows you to demarcate
@@ -79,6 +86,8 @@
7986
* @author Sam Brannen
8087
* @since 1.1.4
8188
* @see DataSourceTransactionManager
89+
* @see #setTargetDataSource
90+
* @see #setReadOnlyDataSource
8291
*/
8392
public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
8493

@@ -96,35 +105,58 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
96105
private static final Log logger = LogFactory.getLog(LazyConnectionDataSourceProxy.class);
97106

98107
@Nullable
99-
private Boolean defaultAutoCommit;
108+
private DataSource readOnlyDataSource;
100109

101110
@Nullable
102-
private Integer defaultTransactionIsolation;
111+
private volatile Boolean defaultAutoCommit;
112+
113+
@Nullable
114+
private volatile Integer defaultTransactionIsolation;
103115

104116

105117
/**
106118
* Create a new LazyConnectionDataSourceProxy.
107119
* @see #setTargetDataSource
120+
* @see #setReadOnlyDataSource
108121
*/
109122
public LazyConnectionDataSourceProxy() {
110123
}
111124

112125
/**
113126
* Create a new LazyConnectionDataSourceProxy.
114127
* @param targetDataSource the target DataSource
128+
* @see #setTargetDataSource
115129
*/
116130
public LazyConnectionDataSourceProxy(DataSource targetDataSource) {
117131
setTargetDataSource(targetDataSource);
118132
afterPropertiesSet();
119133
}
120134

121135

136+
/**
137+
* Specify a variant of the target DataSource to use for read-only transactions.
138+
* <p>If available, a Connection from such a read-only DataSource will be lazily
139+
* obtained within a Spring-managed transaction that has been marked as read-only.
140+
* The {@link Connection#setReadOnly} flag will be left untouched, expecting it
141+
* to be pre-configured as a default on the read-only DataSource, avoiding the
142+
* overhead of switching it at the beginning and end of every transaction.
143+
* Also, the default auto-commit and isolation level settings are expected to
144+
* match the default connection properties of the primary target DataSource.
145+
* @since 6.1.2
146+
* @see #setTargetDataSource
147+
* @see #setDefaultAutoCommit
148+
* @see #setDefaultTransactionIsolation
149+
* @see org.springframework.transaction.TransactionDefinition#isReadOnly()
150+
*/
151+
public void setReadOnlyDataSource(@Nullable DataSource readOnlyDataSource) {
152+
this.readOnlyDataSource = readOnlyDataSource;
153+
}
154+
122155
/**
123156
* Set the default auto-commit mode to expose when no target Connection
124157
* has been fetched yet (when the actual JDBC Connection default is not known yet).
125-
* <p>If not specified, the default gets determined by checking a target
126-
* Connection on startup. If that check fails, the default will be determined
127-
* lazily on first access of a Connection.
158+
* <p>If not specified, the default gets determined by checking lazily on first
159+
* access of a Connection.
128160
* @see java.sql.Connection#setAutoCommit
129161
*/
130162
public void setDefaultAutoCommit(boolean defaultAutoCommit) {
@@ -156,9 +188,8 @@ public void setDefaultTransactionIsolationName(String constantName) {
156188
* {@link java.sql.Connection} interface; it is mainly intended for programmatic
157189
* use. Consider using the "defaultTransactionIsolationName" property for setting
158190
* the value by name (for example, {@code "TRANSACTION_SERIALIZABLE"}).
159-
* <p>If not specified, the default gets determined by checking a target
160-
* Connection on startup. If that check fails, the default will be determined
161-
* lazily on first access of a Connection.
191+
* <p>If not specified, the default gets determined by checking lazily on first
192+
* access of a Connection.
162193
* @see #setDefaultTransactionIsolationName
163194
* @see java.sql.Connection#setTransactionIsolation
164195
*/
@@ -169,12 +200,13 @@ public void setDefaultTransactionIsolation(int defaultTransactionIsolation) {
169200
}
170201

171202

172-
@Override
173-
public void afterPropertiesSet() {
174-
super.afterPropertiesSet();
175-
176-
// Determine default auto-commit and transaction isolation
177-
// via a Connection from the target DataSource, if possible.
203+
/**
204+
* Determine default auto-commit and transaction isolation
205+
* via a Connection from the target DataSource, if possible.
206+
* @since 6.1.2
207+
* @see #checkDefaultConnectionProperties(Connection)
208+
*/
209+
public void checkDefaultConnectionProperties() {
178210
if (this.defaultAutoCommit == null || this.defaultTransactionIsolation == null) {
179211
try {
180212
try (Connection con = obtainTargetDataSource().getConnection()) {
@@ -190,14 +222,11 @@ public void afterPropertiesSet() {
190222
/**
191223
* Check the default connection properties (auto-commit, transaction isolation),
192224
* keeping them to be able to expose them correctly without fetching an actual
193-
* JDBC Connection from the target DataSource.
194-
* <p>This will be invoked once on startup, but also for each retrieval of a
195-
* target Connection. If the check failed on startup (because the database was
196-
* down), we'll lazily retrieve those settings.
225+
* JDBC Connection from the target DataSource later on.
197226
* @param con the Connection to use for checking
198227
* @throws SQLException if thrown by Connection methods
199228
*/
200-
protected synchronized void checkDefaultConnectionProperties(Connection con) throws SQLException {
229+
protected void checkDefaultConnectionProperties(Connection con) throws SQLException {
201230
if (this.defaultAutoCommit == null) {
202231
this.defaultAutoCommit = con.getAutoCommit();
203232
}
@@ -233,6 +262,7 @@ protected Integer defaultTransactionIsolation() {
233262
*/
234263
@Override
235264
public Connection getConnection() throws SQLException {
265+
checkDefaultConnectionProperties();
236266
return (Connection) Proxy.newProxyInstance(
237267
ConnectionProxy.class.getClassLoader(),
238268
new Class<?>[] {ConnectionProxy.class},
@@ -251,6 +281,7 @@ public Connection getConnection() throws SQLException {
251281
*/
252282
@Override
253283
public Connection getConnection(String username, String password) throws SQLException {
284+
checkDefaultConnectionProperties();
254285
return (Connection) Proxy.newProxyInstance(
255286
ConnectionProxy.class.getClassLoader(),
256287
new Class<?>[] {ConnectionProxy.class},
@@ -400,6 +431,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
400431
}
401432
}
402433

434+
if (readOnlyDataSource != null && "setReadOnly".equals(method.getName())) {
435+
// Suppress setReadOnly reset call in case of dedicated read-only DataSource
436+
return null;
437+
}
438+
403439
// Target Connection already fetched,
404440
// or target Connection necessary for current operation ->
405441
// invoke method on target connection.
@@ -429,15 +465,15 @@ private Connection getTargetConnection(Method operation) throws SQLException {
429465
}
430466

431467
// Fetch physical Connection from DataSource.
432-
this.target = (this.username != null) ?
433-
obtainTargetDataSource().getConnection(this.username, this.password) :
434-
obtainTargetDataSource().getConnection();
435-
436-
// If we still lack default connection properties, check them now.
437-
checkDefaultConnectionProperties(this.target);
468+
DataSource dataSource = getDataSourceToUse();
469+
this.target = (this.username != null ? dataSource.getConnection(this.username, this.password) :
470+
dataSource.getConnection());
471+
if (this.target == null) {
472+
throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
473+
}
438474

439475
// Apply kept transaction settings, if any.
440-
if (this.readOnly) {
476+
if (this.readOnly && readOnlyDataSource == null) {
441477
try {
442478
this.target.setReadOnly(true);
443479
}
@@ -450,7 +486,7 @@ private Connection getTargetConnection(Method operation) throws SQLException {
450486
!this.transactionIsolation.equals(defaultTransactionIsolation())) {
451487
this.target.setTransactionIsolation(this.transactionIsolation);
452488
}
453-
if (this.autoCommit != null && this.autoCommit != this.target.getAutoCommit()) {
489+
if (this.autoCommit != null && this.autoCommit != defaultAutoCommit()) {
454490
this.target.setAutoCommit(this.autoCommit);
455491
}
456492
}
@@ -464,6 +500,10 @@ private Connection getTargetConnection(Method operation) throws SQLException {
464500

465501
return this.target;
466502
}
503+
504+
private DataSource getDataSourceToUse() {
505+
return (this.readOnly && readOnlyDataSource != null ? readOnlyDataSource : obtainTargetDataSource());
506+
}
467507
}
468508

469509
}

0 commit comments

Comments
 (0)