Executing SQL Scripts

  1. 按编程方式执行 SQL 脚本:

    • ScriptUtils 提供静态实用程序方法。

    • ResourceDatabasePopulator 提供基于对象的 API。

  2. 使用 @Sql 注解声明性地执行 SQL 脚本:

    • 在集成测试类或方法上配置 SQL 脚本或语句资源路径。

    • 支持多组脚本、脚本执行阶段、事务管理和脚本配置。

在针对关系数据库编写集成测试时,通常有益于运行SQL脚本来修改数据库架构或向表中插入测试数据。spring-jdbc`模块为_initializing_嵌入式或现有数据库提供了支持,以便在载入Spring`ApplicationContext`时执行SQL脚本。请参阅Embedded database supportTesting data access logic with an embedded database了解详情。 尽管在加载 `ApplicationContext 时对数据库进行 一次 初始化非常有用,但有时有必要能够在集成测试 期间 修改数据库。以下部分解释了如何在集成测试期间按编程方式和声明性地运行 SQL 脚本。

Executing SQL scripts programmatically

Spring 提供了以下选项,用于在集成测试方法中按编程方式执行 SQL 脚本。

  • org.springframework.jdbc.datasource.init.ScriptUtils

  • org.springframework.jdbc.datasource.init.ResourceDatabasePopulator

  • org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests

  • org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests

`ScriptUtils`为使用 SQL 脚本提供了一组静态实用程序方法,主要用于框架内的内部使用。但是,如果您需要完全控制 SQL 脚本的解析和运行方式,那么 `ScriptUtils`可能会比这里描述的其他一些备选方案更适合您的需求。有关更多详细信息,请参阅 `ScriptUtils`中各个方法的 javadoc

ResourceDatabasePopulator`提供了一个基于对象的 API,用于通过使用外部资源中定义的 SQL 脚本以编程方式填充、初始化或清理数据库。`ResourceDatabasePopulator`提供用于配置在解析和运行脚本时使用的字符编码、语句分隔符、注释分隔符和错误处理标记的选项。每个配置选项都有一个合理的默认值。有关默认值的详细信息,请参阅 javadoc。要运行在 `ResourceDatabasePopulator`中配置的脚本,您可以调用 `populate(Connection)`方法以针对 `java.sql.Connection`运行填充器,或者调用 `execute(DataSource)`方法以针对 `javax.sql.DataSource`运行填充器。以下示例指定了测试架构和测试数据的 SQL 脚本,设置语句分隔符为 `@@,并针对 `DataSource`运行脚本:

  • Java

  • Kotlin

@Test
void databaseTest() {
	ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
	populator.addScripts(
			new ClassPathResource("test-schema.sql"),
			new ClassPathResource("test-data.sql"));
	populator.setSeparator("@@");
	populator.execute(this.dataSource);
	// run code that uses the test schema and data
}
@Test
fun databaseTest() {
	val populator = ResourceDatabasePopulator()
	populator.addScripts(
			ClassPathResource("test-schema.sql"),
			ClassPathResource("test-data.sql"))
	populator.setSeparator("@@")
	populator.execute(dataSource)
	// run code that uses the test schema and data
}

请注意,ResourceDatabasePopulator`在内部委托给`ScriptUtils`来分析和运行SQL脚本。类似地,`AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests中的`executeSqlScript(..)`方法在内部使用`ResourceDatabasePopulator`来运行SQL脚本。请参阅各种`executeSqlScript(..)`方法的Javadoc以了解进一步详情。

Executing SQL scripts declaratively with @Sql

除了上述通过编程方式运行 SQL 脚本的机制外,你还可以使用 Spring TestContext Framework 声明式配置 SQL 脚本。具体来说,你可以在测试类或测试方法上声明 @Sql 注解,以配置针对给定数据库在集成测试类或测试方法之前或之后运行的各个 SQL 语句或者 SQL 脚本的资源路径。对 @Sql 的支持是由 SqlScriptsTestExecutionListener 提供的,该监听器默认已启用。

方法级别的`@Sql`声明默认覆盖类级别的声明,但是,这种行为可以通过`@SqlMergeMode`按测试类或测试方法进行配置。请参阅Merging and Overriding Configuration with @SqlMergeMode以了解进一步详情。 但是,这不适用于配置为 BEFORE_TEST_CLASSAFTER_TEST_CLASS 执行阶段的类级别声明。此类声明无法覆盖,并且对应的脚本和语句将针对每个类执行一次,此外还有任何方法级别的脚本和语句。

Path Resource Semantics

每个路径都解读为 Spring Resource。普通路径(例如,“schema.sql”)被视为相对于定义测试类的包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如,/org/example/schema.sql)。引用 URL 的路径(例如,以 classpath:file:http: 为前缀的路径)使用指定的资源协议进行加载。

以下示例展示了如何在基于 JUnit Jupiter 的集成测试类中同时在类级别和方法级别使用 @Sql

  • Java

  • Kotlin

@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {

	@Test
	void emptySchemaTest() {
		// run code that uses the test schema without any test data
	}

	@Test
	@Sql({"/test-schema.sql", "/test-user-data.sql"})
	void userTest() {
		// run code that uses the test schema and test data
	}
}
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {

	@Test
	fun emptySchemaTest() {
		// run code that uses the test schema without any test data
	}

	@Test
	@Sql("/test-schema.sql", "/test-user-data.sql")
	fun userTest() {
		// run code that uses the test schema and test data
	}
}

Default Script Detection

如果没有指定 SQL 脚本或语句,系统会尝试检测一个 default 脚本,具体取决于 @Sql 所声明的位置。如果无法检测到默认值,系统会抛出一个 IllegalStateException

  • 类级声明:如果带有注解的测试类是 com.example.MyTest,对应的默认脚本是 classpath:com/example/MyTest.sql

  • 方法级声明:如果带有注解的测试方法被命名为 testMethod() 并且在类 com.example.MyTest 中被定义,那么对应的默认脚本是 classpath:com/example/MyTest.testMethod.sql

Logging SQL Scripts and Statements

如果你想查看正在执行哪些 SQL 脚本,请将 org.springframework.test.context.jdbc 日志类别设置为 DEBUG

如果你想查看正在执行哪些 SQL 语句,请将 org.springframework.jdbc.datasource.init 日志类别设置为 DEBUG

Declaring Multiple @Sql Sets

如果你需要为给定测试类或测试方法配置多组 SQL 脚本,但不同脚本组有不同的语法配置、不同的错误处理规则或不同的执行阶段,你可以声明多个 @Sql 实例。你可以使用 @Sql 作为可重复注解,或者可以使用 @SqlGroup 作为声明多个 @Sql 实例的显式容器。

以下示例展示了如何使用 @Sql 作为可重复注解:

  • Java

  • Kotlin

@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
	// run code that uses the test schema and test data
}
@Test
@Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
fun userTest() {
	// run code that uses the test schema and test data
}

在上述示例中展示的场景中,test-schema.sql 脚本对单行注释使用了不同的语法。

以下示例与此前类似,只不过 @Sql 声明已归组在 @SqlGroup 中。使用 @SqlGroup 是可选的,但你可能需要使用 @SqlGroup 以兼容其他 JVM 语言。

  • Java

  • Kotlin

@Test
@SqlGroup({
	@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
	@Sql("/test-user-data.sql")
)}
void userTest() {
	// run code that uses the test schema and test data
}
@Test
@SqlGroup(
	Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
	Sql("/test-user-data.sql")
)
fun userTest() {
	// Run code that uses the test schema and test data
}

Script Execution Phases

默认情况下,SQL 脚本会在对应的测试方法之前运行。但是,如果你需要在测试方法之后运行特定的一组脚本(例如,清理数据库状态),你可以将 @Sql 中的 executionPhase 属性设置为 AFTER_TEST_METHOD,如下面的示例所示:

  • Java

  • Kotlin

@Test
@Sql(
	scripts = "create-test-data.sql",
	config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
	scripts = "delete-test-data.sql",
	config = @SqlConfig(transactionMode = ISOLATED),
	executionPhase = AFTER_TEST_METHOD
)
void userTest() {
	// run code that needs the test data to be committed
	// to the database outside of the test's transaction
}
@Test
@Sql("create-test-data.sql",
	config = SqlConfig(transactionMode = ISOLATED))
@Sql("delete-test-data.sql",
	config = SqlConfig(transactionMode = ISOLATED),
	executionPhase = AFTER_TEST_METHOD)
fun userTest() {
	// run code that needs the test data to be committed
	// to the database outside of the test's transaction
}

ISOLATEDAFTER_TEST_METHOD 分别从 Sql.TransactionModeSql.ExecutionPhase 静态导入。

在 Spring Framework 6.1 中,可以通过将类级别 @Sql 声明中的 executionPhase 属性设置为 BEFORE_TEST_CLASSAFTER_TEST_CLASS,在测试类之前或之后运行特定的一组脚本,如下面的示例所示:

  • Java

  • Kotlin

@SpringJUnitConfig
@Sql(scripts = "/test-schema.sql", executionPhase = BEFORE_TEST_CLASS)
class DatabaseTests {

	@Test
	void emptySchemaTest() {
		// run code that uses the test schema without any test data
	}

	@Test
	@Sql("/test-user-data.sql")
	void userTest() {
		// run code that uses the test schema and test data
	}
}
@SpringJUnitConfig
@Sql("/test-schema.sql", executionPhase = BEFORE_TEST_CLASS)
class DatabaseTests {

	@Test
	fun emptySchemaTest() {
		// run code that uses the test schema without any test data
	}

	@Test
	@Sql("/test-user-data.sql")
	fun userTest() {
		// run code that uses the test schema and test data
	}
}

BEFORE_TEST_CLASSSql.ExecutionPhase 静态导入。

Script Configuration with @SqlConfig

你可以使用 @SqlConfig 注解配置脚本解析和错误处理。当在集成测试类上声明为一个类级别注解时,@SqlConfig 充当测试类层次结构中所有 SQL 脚本的全局配置。当直接通过使用 @Sql 注解的 config 属性声明时,@SqlConfig 充当封闭 @Sql 注解中声明的 SQL 脚本的局部配置。@SqlConfig 中的每个属性都有一个隐式默认值,该值在相应属性的 javadoc 中进行了说明。由于在 Java 语言规范中定义了针对注解属性的规则,因此无法将 null 值赋值给注解属性。因此,为了支持对继承的全局配置进行覆盖,@SqlConfig 属性有显式默认值,该值可能是 ""(针对字符串)、{}(针对数组)或 DEFAULT(针对枚举)。这种方法允许针对全局 @SqlConfig 声明,通过提供 ""{}DEFAULT 以外的值,局部 @SqlConfig 声明有选择地覆盖个别属性。局部 @SqlConfig 属性未提供 ""{}DEFAULT 以外的显式值时,全局 @SqlConfig 属性将被继承。因此,显式局部配置会覆盖全局配置。

@Sql`和 `@SqlConfig`提供的配置选项等效于 `ScriptUtils`和 `ResourceDatabasePopulator`支持的选项,但它们是 `<jdbc:initialize-database/>`XML 命名空间元素提供的选项的超集。有关详细信息,请参阅 javadoc `@Sqljavadoc @SqlConfig 中各个属性的 javadoc。

Transaction management for @Sql

默认情况下,SqlScriptsTestExecutionListener 推断出使用 @Sql 配置的脚本所需的交易语义。具体来说,SQL 脚本在没有交易的情况下、在现有的由 Spring 管理的交易中(例如,针对注释了 @Transactional 的测试由 TransactionalTestExecutionListener 管理的交易)或在隔离的交易中运行,具体取决于 @SqlConfigtransactionMode 属性的配置值和测试的 ApplicationContext 中是否存在 PlatformTransactionManager。然而,作为最低要求,测试的 ApplicationContext 中必须存在一个 javax.sql.DataSource

如果 SqlScriptsTestExecutionListener`用于检测 `DataSource`和 `PlatformTransactionManager`并推断事务语义所使用的算法不符合您的需求,那么您可以通过设置 `@SqlConfig`的 `dataSource`和 `transactionManager`属性来指定显式名称。此外,您可以通过设置 `@SqlConfig`的 `transactionMode`属性(例如,是否应在隔离的事务中运行脚本)来控制事务传播行为。尽管对 `@Sql`的事务管理的所有支持选项进行彻底讨论超出了本参考手册的范围,但 javadoc `@SqlConfigjavadoc SqlScriptsTestExecutionListener 的 javadoc 提供了详细信息,并且以下示例展示了一个使用 JUnit Jupiter 和带有 `@Sql`的事务测试的典型测试场景:

  • Java

  • Kotlin

@SpringJUnitConfig(TestDatabaseConfig.class)
@Transactional
class TransactionalSqlScriptsTests {

	final JdbcTemplate jdbcTemplate;

	@Autowired
	TransactionalSqlScriptsTests(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	@Test
	@Sql("/test-data.sql")
	void usersTest() {
		// verify state in test database:
		assertNumUsers(2);
		// run code that uses the test data...
	}

	int countRowsInTable(String tableName) {
		return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
	}

	void assertNumUsers(int expected) {
		assertEquals(expected, countRowsInTable("user"),
			"Number of rows in the [user] table.");
	}
}
@SpringJUnitConfig(TestDatabaseConfig::class)
@Transactional
class TransactionalSqlScriptsTests @Autowired constructor(dataSource: DataSource) {

	val jdbcTemplate: JdbcTemplate = JdbcTemplate(dataSource)

	@Test
	@Sql("/test-data.sql")
	fun usersTest() {
		// verify state in test database:
		assertNumUsers(2)
		// run code that uses the test data...
	}

	fun countRowsInTable(tableName: String): Int {
		return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName)
	}

	fun assertNumUsers(expected: Int) {
		assertEquals(expected, countRowsInTable("user"),
				"Number of rows in the [user] table.")
	}
}

请注意,无需在运行 `usersTest()`方法后清理数据库,因为对数据库所做的任何更改(在测试方法内或在 `/test-data.sql`脚本内)都会自动由 `TransactionalTestExecutionListener`回滚(有关详细信息,请参阅 transaction management)。

Merging and Overriding Configuration with @SqlMergeMode

从 Spring Framework 5.2 开始,可以将方法级别的 @Sql`声明与类级别的声明合并。例如,这允许您为每个测试类一次提供数据库架构或一些常见测试数据的配置,然后为每个测试方法提供额外的用例特定测试数据。要启用 `@Sql`合并,请使用 `@SqlMergeMode(MERGE)`注释测试类或测试方法。要针对特定测试方法(或特定测试子类)禁用合并,您可以通过 `@SqlMergeMode(OVERRIDE)`切换回默认模式。有关示例和更多详细信息,请参阅 @SqlMergeMode` annotation documentation section