Skip to content

Commit 2e75adb

Browse files
committed
Improve transaction management for @SQL scripts
Prior to this commit, the support for SQL script execution via @SQL provided an algorithm for looking up a required PlatformTransactionManager to use to drive transactions. However, a transaction manager is not actually required for all testing scenarios. This commit improves the transaction management support for @SQL so that SQL scripts can be executed without a transaction if a transaction manger is not present in the ApplicationContext. The updated algorithm now supports the following use cases. - If a transaction manager and data source are both present (i.e., explicitly specified via the transactionManager and dataSource attributes of @SqlConfig or implicitly discovered in the ApplicationContext based on conventions), both will be used. - If a transaction manager is not explicitly specified and not implicitly discovered based on conventions, SQL scripts will be executed without a transaction but requiring the presence of a data source. If a data source is not present, an exception will be thrown. - If a data source is not explicitly specified and not implicitly discovered based on conventions, an attempt will be made to retrieve it by using reflection to invoke a public method named getDataSource() on the transaction manager. If this attempt fails, an exception will be thrown. - If a data source can be retrieved from the resolved transaction manager using reflection, an exception will be thrown if the resolved data source is not the data source associated with the resolved transaction manager. This helps to avoid possibly unintended configuration errors. - If @SqlConfig.transactionMode is set to ISOLATED, an exception will be thrown if a transaction manager is not present. Issue: SPR-11911
1 parent 44e4569 commit 2e75adb

File tree

9 files changed

+557
-70
lines changed

9 files changed

+557
-70
lines changed

spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@
2222
import java.lang.annotation.Retention;
2323
import java.lang.annotation.Target;
2424

25-
import org.springframework.jdbc.datasource.init.ScriptUtils;
26-
import org.springframework.util.ResourceUtils;
27-
2825
import static java.lang.annotation.ElementType.*;
2926
import static java.lang.annotation.RetentionPolicy.*;
3027

@@ -39,8 +36,8 @@
3936
*
4037
* <p>The configuration options provided by this annotation and
4138
* {@link SqlConfig @SqlConfig} are equivalent to those supported by
42-
* {@link ScriptUtils} and
43-
* {@link org.springframework.jdbc.datasource.init.ResourceDatabasePopulator}
39+
* {@link org.springframework.jdbc.datasource.init.ScriptUtils ScriptUtils} and
40+
* {@link org.springframework.jdbc.datasource.init.ResourceDatabasePopulator ResourceDatabasePopulator}
4441
* but are a superset of those provided by the {@code <jdbc:initialize-database/>}
4542
* XML namespace element. Consult the javadocs of individual attributes in this
4643
* annotation and {@link SqlConfig @SqlConfig} for details.
@@ -110,9 +107,9 @@ static enum ExecutionPhase {
110107
* <em>absolute</em> classpath resource, for example:
111108
* {@code "/org/example/schema.sql"}. A path which references a
112109
* URL (e.g., a path prefixed with
113-
* {@link ResourceUtils#CLASSPATH_URL_PREFIX classpath:},
114-
* {@link ResourceUtils#FILE_URL_PREFIX file:}, {@code http:}, etc.) will be
115-
* loaded using the specified resource protocol.
110+
* {@link org.springframework.util.ResourceUtils#CLASSPATH_URL_PREFIX classpath:},
111+
* {@link org.springframework.util.ResourceUtils#FILE_URL_PREFIX file:},
112+
* {@code http:}, etc.) will be loaded using the specified resource protocol.
116113
* <h3>Default Script Detection</h3>
117114
* <p>If no SQL scripts are specified, an attempt will be made to detect a
118115
* <em>default</em> script depending on where this annotation is declared.

spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import java.lang.annotation.Retention;
2222
import java.lang.annotation.Target;
2323

24-
import org.springframework.jdbc.datasource.init.ScriptUtils;
25-
2624
import static java.lang.annotation.ElementType.*;
2725
import static java.lang.annotation.RetentionPolicy.*;
2826

@@ -46,7 +44,7 @@
4644
* attribute. Thus, in order to support overrides of <em>inherited</em> global
4745
* configuration, {@code @SqlConfig} attributes have an <em>explicit</em>
4846
* {@code default} value of either {@code ""} for Strings or {@code DEFAULT} for
49-
* Enums. This approach allows local declarations {@code @SqlConfig} to
47+
* Enums. This approach allows local declarations of {@code @SqlConfig} to
5048
* selectively override individual attributes from global declarations of
5149
* {@code @SqlConfig} by providing a value other than {@code ""} or {@code DEFAULT}.
5250
*
@@ -91,22 +89,50 @@ static enum TransactionMode {
9189
DEFAULT,
9290

9391
/**
94-
* Indicates that the transaction mode to use when executing SQL scripts
95-
* should be <em>inferred</em> based on whether or not a Spring-managed
96-
* transaction is currently present.
97-
* <p>SQL scripts will be executed within the current transaction if present;
98-
* otherwise, scripts will be executed in a new transaction that will be
99-
* immediately committed.
100-
* <p>The <em>current</em> transaction will typically be managed by the
101-
* {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener
102-
* TransactionalTestExecutionListener}.
92+
* Indicates that the transaction mode to use when executing SQL
93+
* scripts should be <em>inferred</em> using the rules listed below.
94+
* In the context of these rules, the term "<em>available</em>"
95+
* means that the bean for the data source or transaction manager
96+
* is either explicitly specified via a corresponding annotation
97+
* attribute in {@code @SqlConfig} or discoverable via conventions. See
98+
* {@link org.springframework.test.context.transaction.TestContextTransactionUtils TestContextTransactionUtils}
99+
* for details on the conventions used to discover such beans in
100+
* the {@code ApplicationContext}.
101+
*
102+
* <h4>Inference Rules</h4>
103+
* <ol>
104+
* <li>If neither a transaction manager nor a data source is
105+
* available, an exception will be thrown.
106+
* <li>If a transaction manager is not available but a data source
107+
* is available, SQL scripts will be executed directly against the
108+
* data source without a transaction.
109+
* <li>If a transaction manager is available:
110+
* <ul>
111+
* <li>If a data source is not available, an attempt will be made
112+
* to retrieve it from the transaction manager by using reflection
113+
* to invoke a public method named {@code getDataSource()} on the
114+
* transaction manager. If the attempt fails, an exception will be
115+
* thrown.
116+
* <li>Using the resolved transaction manager and data source, SQL
117+
* scripts will be executed within an existing transaction if
118+
* present; otherwise, scripts will be executed in a new transaction
119+
* that will be immediately committed. An <em>existing</em>
120+
* transaction will typically be managed by the
121+
* {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener TransactionalTestExecutionListener}.
122+
* </ul>
123+
* </ol>
103124
* @see #ISOLATED
125+
* @see org.springframework.test.context.transaction.TestContextTransactionUtils#retrieveDataSource
126+
* @see org.springframework.test.context.transaction.TestContextTransactionUtils#retrieveTransactionManager
104127
*/
105128
INFERRED,
106129

107130
/**
108131
* Indicates that SQL scripts should always be executed in a new,
109132
* <em>isolated</em> transaction that will be immediately committed.
133+
* <p>In contrast to {@link #INFERRED}, this mode requires the
134+
* presence of a transaction manager <strong>and</strong> a data
135+
* source.
110136
*/
111137
ISOLATED
112138
}
@@ -164,18 +190,24 @@ static enum ErrorMode {
164190

165191

166192
/**
167-
* The bean name of the {@link javax.sql.DataSource} against which the scripts
168-
* should be executed.
169-
* <p>The name is only used if there is more than one bean of type
170-
* {@code DataSource} in the test's {@code ApplicationContext}. If there is
171-
* only one such bean, it is not necessary to specify a bean name.
193+
* The bean name of the {@link javax.sql.DataSource} against which the
194+
* scripts should be executed.
195+
* <p>The name is only required if there is more than one bean of type
196+
* {@code DataSource} in the test's {@code ApplicationContext}. If there
197+
* is only one such bean, it is not necessary to specify a bean name.
172198
* <p>Defaults to an empty string, requiring that one of the following is
173199
* true:
174200
* <ol>
201+
* <li>An explicit bean name is defined in a global declaration of
202+
* {@code @SqlConfig}.
203+
* <li>The data source can be retrieved from the transaction manager
204+
* by using reflection to invoke a public method named
205+
* {@code getDataSource()} on the transaction manager.
175206
* <li>There is only one bean of type {@code DataSource} in the test's
176207
* {@code ApplicationContext}.</li>
177208
* <li>The {@code DataSource} to use is named {@code "dataSource"}.</li>
178209
* </ol>
210+
* @see org.springframework.test.context.transaction.TestContextTransactionUtils#retrieveDataSource
179211
*/
180212
String dataSource() default "";
181213

@@ -188,6 +220,8 @@ static enum ErrorMode {
188220
* <p>Defaults to an empty string, requiring that one of the following is
189221
* true:
190222
* <ol>
223+
* <li>An explicit bean name is defined in a global declaration of
224+
* {@code @SqlConfig}.
191225
* <li>There is only one bean of type {@code PlatformTransactionManager} in
192226
* the test's {@code ApplicationContext}.</li>
193227
* <li>{@link org.springframework.transaction.annotation.TransactionManagementConfigurer
@@ -197,6 +231,7 @@ static enum ErrorMode {
197231
* <li>The {@code PlatformTransactionManager} to use is named
198232
* {@code "transactionManager"}.</li>
199233
* </ol>
234+
* @see org.springframework.test.context.transaction.TestContextTransactionUtils#retrieveTransactionManager
200235
*/
201236
String transactionManager() default "";
202237

@@ -223,32 +258,35 @@ static enum ErrorMode {
223258
* SQL scripts.
224259
* <p>Implicitly defaults to {@code ";"} if not specified and falls back to
225260
* {@code "\n"} as a last resort.
226-
* <p>May be set to {@link ScriptUtils#EOF_STATEMENT_SEPARATOR} to signal
227-
* that each script contains a single statement without a separator.
228-
* @see ScriptUtils#DEFAULT_STATEMENT_SEPARATOR
261+
* <p>May be set to
262+
* {@link org.springframework.jdbc.datasource.init.ScriptUtils#EOF_STATEMENT_SEPARATOR}
263+
* to signal that each script contains a single statement without a
264+
* separator.
265+
* @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_STATEMENT_SEPARATOR
266+
* @see org.springframework.jdbc.datasource.init.ScriptUtils#EOF_STATEMENT_SEPARATOR
229267
*/
230268
String separator() default "";
231269

232270
/**
233271
* The prefix that identifies single-line comments within the SQL scripts.
234272
* <p>Implicitly defaults to {@code "--"}.
235-
* @see ScriptUtils#DEFAULT_COMMENT_PREFIX
273+
* @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_COMMENT_PREFIX
236274
*/
237275
String commentPrefix() default "";
238276

239277
/**
240278
* The start delimiter that identifies block comments within the SQL scripts.
241279
* <p>Implicitly defaults to {@code "/*"}.
242280
* @see #blockCommentEndDelimiter
243-
* @see ScriptUtils#DEFAULT_BLOCK_COMMENT_START_DELIMITER
281+
* @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_BLOCK_COMMENT_START_DELIMITER
244282
*/
245283
String blockCommentStartDelimiter() default "";
246284

247285
/**
248286
* The end delimiter that identifies block comments within the SQL scripts.
249287
* <p>Implicitly defaults to <code>"*&#47;"</code>.
250288
* @see #blockCommentStartDelimiter
251-
* @see ScriptUtils#DEFAULT_BLOCK_COMMENT_END_DELIMITER
289+
* @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_BLOCK_COMMENT_END_DELIMITER
252290
*/
253291
String blockCommentEndDelimiter() default "";
254292

spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.transaction.support.TransactionTemplate;
4444
import org.springframework.util.ClassUtils;
4545
import org.springframework.util.ObjectUtils;
46+
import org.springframework.util.ReflectionUtils;
4647
import org.springframework.util.ResourceUtils;
4748

4849
/**
@@ -165,24 +166,77 @@ private void executeSqlScripts(Sql sql, ExecutionPhase executionPhase, TestConte
165166
logger.debug("Executing SQL scripts: " + ObjectUtils.nullSafeToString(scripts));
166167
}
167168

168-
final DataSource dataSource = TestContextTransactionUtils.retrieveDataSource(testContext,
169-
mergedSqlConfig.getDataSource());
169+
String dsName = mergedSqlConfig.getDataSource();
170+
String tmName = mergedSqlConfig.getTransactionManager();
171+
DataSource dataSource = TestContextTransactionUtils.retrieveDataSource(testContext, dsName);
170172
final PlatformTransactionManager transactionManager = TestContextTransactionUtils.retrieveTransactionManager(
171-
testContext, mergedSqlConfig.getTransactionManager());
173+
testContext, tmName);
174+
final boolean newTxRequired = mergedSqlConfig.getTransactionMode() == TransactionMode.ISOLATED;
172175

173-
int propagation = (mergedSqlConfig.getTransactionMode() == TransactionMode.ISOLATED) ? TransactionDefinition.PROPAGATION_REQUIRES_NEW
174-
: TransactionDefinition.PROPAGATION_REQUIRED;
176+
if (transactionManager == null) {
177+
if (newTxRequired) {
178+
throw new IllegalStateException(String.format("Failed to execute SQL scripts for test context %s: "
179+
+ "cannot execute SQL scripts using Transaction Mode "
180+
+ "[%s] without a PlatformTransactionManager.", testContext, TransactionMode.ISOLATED));
181+
}
182+
183+
if (dataSource == null) {
184+
throw new IllegalStateException(String.format("Failed to execute SQL scripts for test context %s: "
185+
+ "supply at least a DataSource or PlatformTransactionManager.", testContext));
186+
}
175187

176-
TransactionAttribute transactionAttribute = TestContextTransactionUtils.createDelegatingTransactionAttribute(
177-
testContext, new DefaultTransactionAttribute(propagation));
188+
// Execute scripts directly against the DataSource
189+
populator.execute(dataSource);
190+
}
191+
else {
192+
DataSource dataSourceFromTxMgr = getDataSourceFromTransactionManager(transactionManager);
193+
194+
// Ensure user configured an appropriate DataSource/TransactionManager pair.
195+
if ((dataSource != null) && (dataSourceFromTxMgr != null) && !dataSource.equals(dataSourceFromTxMgr)) {
196+
throw new IllegalStateException(String.format("Failed to execute SQL scripts for test context %s: "
197+
+ "the configured DataSource [%s] (named '%s') is not the one associated "
198+
+ "with transaction manager [%s] (named '%s').", testContext, dataSource.getClass().getName(),
199+
dsName, transactionManager.getClass().getName(), tmName));
200+
}
201+
202+
if (dataSource == null) {
203+
dataSource = dataSourceFromTxMgr;
204+
if (dataSource == null) {
205+
throw new IllegalStateException(String.format("Failed to execute SQL scripts for test context %s: "
206+
+ "could not obtain DataSource from transaction manager [%s] (named '%s').", testContext,
207+
transactionManager.getClass().getName(), tmName));
208+
}
209+
}
178210

179-
new TransactionTemplate(transactionManager, transactionAttribute).execute(new TransactionCallbackWithoutResult() {
211+
final DataSource finalDataSource = dataSource;
212+
int propagation = newTxRequired ? TransactionDefinition.PROPAGATION_REQUIRES_NEW
213+
: TransactionDefinition.PROPAGATION_REQUIRED;
180214

181-
@Override
182-
public void doInTransactionWithoutResult(TransactionStatus status) {
183-
populator.execute(dataSource);
184-
};
185-
});
215+
TransactionAttribute transactionAttribute = TestContextTransactionUtils.createDelegatingTransactionAttribute(
216+
testContext, new DefaultTransactionAttribute(propagation));
217+
218+
new TransactionTemplate(transactionManager, transactionAttribute).execute(new TransactionCallbackWithoutResult() {
219+
220+
@Override
221+
public void doInTransactionWithoutResult(TransactionStatus status) {
222+
populator.execute(finalDataSource);
223+
}
224+
});
225+
}
226+
}
227+
228+
private DataSource getDataSourceFromTransactionManager(PlatformTransactionManager transactionManager) {
229+
try {
230+
Method getDataSourceMethod = transactionManager.getClass().getMethod("getDataSource");
231+
Object obj = ReflectionUtils.invokeMethod(getDataSourceMethod, transactionManager);
232+
if (obj instanceof DataSource) {
233+
return (DataSource) obj;
234+
}
235+
}
236+
catch (Exception e) {
237+
/* ignore */
238+
}
239+
return null;
186240
}
187241

188242
private String[] getScripts(Sql sql, TestContext testContext, boolean classLevel) {

0 commit comments

Comments
 (0)