37
37
* Proxy for a target DataSource, fetching actual JDBC Connections lazily,
38
38
* i.e. not until first creation of a Statement. Connection initialization
39
39
* 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.
43
45
*
44
46
* <p>This DataSource proxy allows to avoid fetching JDBC Connections from
45
47
* a pool unless actually necessary. JDBC transaction control can happen
46
48
* without fetching a Connection from the pool or communicating with the
47
49
* 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}).
48
53
*
49
54
* <p><b>If you configure both a LazyConnectionDataSourceProxy and a
50
55
* TransactionAwareDataSourceProxy, make sure that the latter is the outermost
51
56
* DataSource.</b> In such a scenario, data access code will talk to the
52
57
* 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()}.
54
61
*
55
62
* <p>Lazy fetching of physical JDBC Connections is particularly beneficial
56
63
* in a generic transaction demarcation environment. It allows you to demarcate
79
86
* @author Sam Brannen
80
87
* @since 1.1.4
81
88
* @see DataSourceTransactionManager
89
+ * @see #setTargetDataSource
90
+ * @see #setReadOnlyDataSource
82
91
*/
83
92
public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
84
93
@@ -96,35 +105,58 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
96
105
private static final Log logger = LogFactory .getLog (LazyConnectionDataSourceProxy .class );
97
106
98
107
@ Nullable
99
- private Boolean defaultAutoCommit ;
108
+ private DataSource readOnlyDataSource ;
100
109
101
110
@ Nullable
102
- private Integer defaultTransactionIsolation ;
111
+ private volatile Boolean defaultAutoCommit ;
112
+
113
+ @ Nullable
114
+ private volatile Integer defaultTransactionIsolation ;
103
115
104
116
105
117
/**
106
118
* Create a new LazyConnectionDataSourceProxy.
107
119
* @see #setTargetDataSource
120
+ * @see #setReadOnlyDataSource
108
121
*/
109
122
public LazyConnectionDataSourceProxy () {
110
123
}
111
124
112
125
/**
113
126
* Create a new LazyConnectionDataSourceProxy.
114
127
* @param targetDataSource the target DataSource
128
+ * @see #setTargetDataSource
115
129
*/
116
130
public LazyConnectionDataSourceProxy (DataSource targetDataSource ) {
117
131
setTargetDataSource (targetDataSource );
118
132
afterPropertiesSet ();
119
133
}
120
134
121
135
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
+
122
155
/**
123
156
* Set the default auto-commit mode to expose when no target Connection
124
157
* 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.
128
160
* @see java.sql.Connection#setAutoCommit
129
161
*/
130
162
public void setDefaultAutoCommit (boolean defaultAutoCommit ) {
@@ -156,9 +188,8 @@ public void setDefaultTransactionIsolationName(String constantName) {
156
188
* {@link java.sql.Connection} interface; it is mainly intended for programmatic
157
189
* use. Consider using the "defaultTransactionIsolationName" property for setting
158
190
* 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.
162
193
* @see #setDefaultTransactionIsolationName
163
194
* @see java.sql.Connection#setTransactionIsolation
164
195
*/
@@ -169,12 +200,13 @@ public void setDefaultTransactionIsolation(int defaultTransactionIsolation) {
169
200
}
170
201
171
202
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 () {
178
210
if (this .defaultAutoCommit == null || this .defaultTransactionIsolation == null ) {
179
211
try {
180
212
try (Connection con = obtainTargetDataSource ().getConnection ()) {
@@ -190,14 +222,11 @@ public void afterPropertiesSet() {
190
222
/**
191
223
* Check the default connection properties (auto-commit, transaction isolation),
192
224
* 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.
197
226
* @param con the Connection to use for checking
198
227
* @throws SQLException if thrown by Connection methods
199
228
*/
200
- protected synchronized void checkDefaultConnectionProperties (Connection con ) throws SQLException {
229
+ protected void checkDefaultConnectionProperties (Connection con ) throws SQLException {
201
230
if (this .defaultAutoCommit == null ) {
202
231
this .defaultAutoCommit = con .getAutoCommit ();
203
232
}
@@ -233,6 +262,7 @@ protected Integer defaultTransactionIsolation() {
233
262
*/
234
263
@ Override
235
264
public Connection getConnection () throws SQLException {
265
+ checkDefaultConnectionProperties ();
236
266
return (Connection ) Proxy .newProxyInstance (
237
267
ConnectionProxy .class .getClassLoader (),
238
268
new Class <?>[] {ConnectionProxy .class },
@@ -251,6 +281,7 @@ public Connection getConnection() throws SQLException {
251
281
*/
252
282
@ Override
253
283
public Connection getConnection (String username , String password ) throws SQLException {
284
+ checkDefaultConnectionProperties ();
254
285
return (Connection ) Proxy .newProxyInstance (
255
286
ConnectionProxy .class .getClassLoader (),
256
287
new Class <?>[] {ConnectionProxy .class },
@@ -400,6 +431,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
400
431
}
401
432
}
402
433
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
+
403
439
// Target Connection already fetched,
404
440
// or target Connection necessary for current operation ->
405
441
// invoke method on target connection.
@@ -429,15 +465,15 @@ private Connection getTargetConnection(Method operation) throws SQLException {
429
465
}
430
466
431
467
// 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
+ }
438
474
439
475
// Apply kept transaction settings, if any.
440
- if (this .readOnly ) {
476
+ if (this .readOnly && readOnlyDataSource == null ) {
441
477
try {
442
478
this .target .setReadOnly (true );
443
479
}
@@ -450,7 +486,7 @@ private Connection getTargetConnection(Method operation) throws SQLException {
450
486
!this .transactionIsolation .equals (defaultTransactionIsolation ())) {
451
487
this .target .setTransactionIsolation (this .transactionIsolation );
452
488
}
453
- if (this .autoCommit != null && this .autoCommit != this . target . getAutoCommit ()) {
489
+ if (this .autoCommit != null && this .autoCommit != defaultAutoCommit ()) {
454
490
this .target .setAutoCommit (this .autoCommit );
455
491
}
456
492
}
@@ -464,6 +500,10 @@ private Connection getTargetConnection(Method operation) throws SQLException {
464
500
465
501
return this .target ;
466
502
}
503
+
504
+ private DataSource getDataSourceToUse () {
505
+ return (this .readOnly && readOnlyDataSource != null ? readOnlyDataSource : obtainTargetDataSource ());
506
+ }
467
507
}
468
508
469
509
}
0 commit comments