baeldung 2024-05-11
1. 概述
Spring Boot 让我们非常轻松地管理数据库变更。如果使用默认配置,它会自动扫描项目中的实体类,并创建对应的数据库表。
但有时我们需要对数据库变更进行更精细的控制。这时,就可以使用 Spring 提供的 data.sql 和 schema.sql 文件。
2. data.sql 文件
假设我们正在使用 JPA,并在项目中定义了一个简单的 Country 实体:
@Entity
public class Country {
@Id
@GeneratedValue(strategy = IDENTITY)
private Integer id;
@Column(nullable = false)
private String name;
//...
}
当我们运行应用程序时,Spring Boot 会为我们创建一个空表,但不会填充任何数据。
一种简单的方式是创建一个名为 data.sql 的文件:
INSERT INTO country (name) VALUES ('India');
INSERT INTO country (name) VALUES ('Brazil');
INSERT INTO country (name) VALUES ('USA');
INSERT INTO country (name) VALUES ('Italy');
默认情况下,data.sql 脚本会在 Hibernate 初始化之前执行。但我们希望 Hibernate 先创建好表,再插入数据。为此,我们需要延迟数据源的初始化。可以通过以下属性实现:
spring.jpa.defer-datasource-initialization=true
当我们将该文件放在 classpath 下并运行项目时,Spring 会自动加载并用它来填充 country 表。
注意:对于任何基于脚本的初始化(例如通过
data.sql插入数据,或通过schema.sql创建表结构),都需要设置以下属性:
spring.sql.init.mode=always
对于嵌入式数据库(如 H2),该属性默认值就是 always。
3. schema.sql 文件
有时我们不想依赖默认的表结构生成机制。
在这种情况下,可以创建一个自定义的 schema.sql 文件:
CREATE TABLE USERS(
ID int not null AUTO_INCREMENT,
NAME varchar(100) not null,
STATUS int,
PRIMARY KEY ( ID )
);
Spring 会读取这个文件并用于创建数据库结构。
即使项目中没有定义 Users 实体类,只要 schema.sql 在 classpath 中,Spring 仍会根据该文件在数据库中创建 USERS 表。
注意:如果同时使用脚本初始化(
schema.sql/data.sql)和 Hibernate 自动建表,可能会产生冲突。
为避免冲突,可以完全禁用 Hibernate 的 DDL 命令(即 Hibernate 用于创建/更新表的命令):
spring.jpa.hibernate.ddl-auto=none
这样就能确保仅通过 schema.sql 进行脚本化建表。
如果你仍希望同时使用 Hibernate 自动建表和脚本初始化(例如额外建表或填充数据),则必须设置:
spring.jpa.defer-datasource-initialization=true
这将确保:
- Hibernate 先完成其表结构创建;
- 然后执行
schema.sql(用于额外结构变更); - 最后执行
data.sql(用于填充数据)。
此外,如前所述,脚本初始化默认仅对嵌入式数据库生效。若要对所有数据库(如 MySQL、PostgreSQL)启用脚本初始化,需显式设置:
spring.sql.init.mode=always
更多详情请参考 Spring 官方文档关于使用 SQL 脚本初始化数据库。
4. 使用 Hibernate 控制数据库创建
Spring 提供了一个 JPA 特有属性 spring.jpa.hibernate.ddl-auto,用于控制 Hibernate 的 DDL 生成行为。其可选值包括:
create:Hibernate 先删除已有表,再创建新表。update:将对象模型(基于注解或 XML 映射)与现有数据库结构对比,并根据差异更新结构。不会删除不再需要的表或列。create-drop:类似create,但在应用关闭时自动删除所有表;通常用于单元测试。validate:仅验证表和列是否存在,若不存在则抛出异常。none:完全禁用 DDL 生成。
Spring Boot 内部逻辑:
- 如果未检测到 schema 管理器(如 Liquibase/Flyway),默认值为
create-drop;- 其他情况下,默认值为
none。
因此,应谨慎设置该属性,或使用其他机制初始化数据库。
5. 自定义数据库结构创建
默认情况下,Spring Boot 会自动为嵌入式数据源创建数据库结构。
若需控制此行为,可使用 spring.sql.init.mode 属性,其取值如下:
always:始终初始化数据库。embedded:仅在使用嵌入式数据库时初始化(默认值)。never:从不初始化数据库。
重要提示:如果你使用的是非嵌入式数据库(如 MySQL 或 PostgreSQL),并希望使用脚本初始化结构,则必须将该属性设为
always。
版本说明:该属性自 Spring Boot 2.5.0 引入。若使用更早版本,请使用
spring.datasource.initialization-mode。
6. 使用 @Sql 注解
Spring 还提供了 @Sql 注解——一种声明式方式,用于在测试中初始化和填充数据库。
@Sql 注解的主要属性:
config:SQL 脚本的本地配置(下文详述)。executionPhase:指定 SQL 脚本的执行时机。statements:内联 SQL 语句。scripts(或value):SQL 脚本文件路径。
@Sql 可用于类级别或方法级别。
6.1 类级别的 @Sql 注解
可在测试类上使用 @Sql 来加载测试所需数据。
示例:创建表并导入初始数据:
@Sql({"/employees_schema.sql", "/import_employees.sql"})
public class SpringBootInitialLoadIntegrationTest {
@Autowired
private EmployeeRepository employeeRepository;
@Test
public void testLoadDataForTestClass() {
assertEquals(3, employeeRepository.findAll().size());
}
}
上述代码中,两个 SQL 脚本会在测试方法执行前运行(默认执行阶段为 BEFORE_TEST_METHOD)。
新特性:Spring 6.1 与 Spring Boot 3.2.0 开始支持在类级别使用
executionPhase,可设置为BEFORE_TEST_CLASS或AFTER_TEST_CLASS。
例如,显式指定在测试类之前执行脚本:
@Sql(scripts = {"/employees_schema.sql", "/import_employees.sql"},
executionPhase = BEFORE_TEST_CLASS)
public class SpringBootInitialLoadIntegrationTest {
// ...
}
同样,可使用 AFTER_TEST_CLASS 在测试类结束后清理数据:
@Sql(scripts = {"/delete_employees_data.sql"},
executionPhase = AFTER_TEST_CLASS)
public class SpringBootInitialLoadIntegrationTest {
// ...
}
注意:类级别的
AFTER_TEST_CLASS配置不能被方法级脚本覆盖,而是会与方法级脚本同时执行。
6.2 方法级别的 @Sql 注解
可为特定测试用例加载额外数据:
@Test
@Sql({"/import_senior_employees.sql"})
public void testLoadDataForTestCase() {
assertEquals(5, employeeRepository.findAll().size());
}
默认在测试方法之前执行。
也可显式指定执行阶段:
@Test
@Sql(scripts = {"/import_senior_employees.sql"},
executionPhase = BEFORE_TEST_METHOD)
public void testLoadDataForTestCase() {
assertEquals(5, employeeRepository.findAll().size());
}
使用 AFTER_TEST_METHOD 可在测试方法结束后执行清理脚本(例如删除临时表)。
优先级规则:方法级别的
@Sql默认会覆盖类级别的@Sql。
例如:
@Sql(scripts = {"/employees_schema.sql", "/import_employees.sql"})
public class SpringBootInitialLoadIntegrationTest {
@Autowired
private EmployeeRepository employeeRepository;
@Test
@Sql(scripts = {"/import_senior_employees.sql"})
public void testLoadDataForTestClass() {
assertEquals(5, employeeRepository.findAll().size());
}
}
此时,只有 import_senior_employees.sql 会被执行。
合并模式:可通过
@SqlMergeMode注解,使方法级与类级的@Sql合并执行而非覆盖。
7. @SqlConfig 注解
可通过 @SqlConfig 自定义 SQL 脚本的解析与执行方式。
@SqlConfig 可用于类级别(作为全局配置),也可用于单个 @Sql 注解。
示例:指定脚本编码和事务模式:
@Test
@Sql(scripts = {"/import_senior_employees.sql"},
config = @SqlConfig(encoding = "utf-8", transactionMode = TransactionMode.ISOLATED))
public void testLoadDataForTestCase() {
assertEquals(5, employeeRepository.findAll().size());
}
@SqlConfig 的主要属性:
blockCommentStartDelimiter:块注释开始分隔符blockCommentEndDelimiter:块注释结束分隔符commentPrefix:单行注释前缀dataSource:指定执行脚本的数据源 Bean 名称encoding:脚本文件编码(默认为平台编码)errorMode:脚本执行出错时的处理模式separator:语句分隔符(默认为;)transactionManager:事务管理器 Bean 名称transactionMode:脚本执行的事务模式
8. @SqlGroup 注解
Java 8 及以上支持重复注解,因此可直接多次使用 @Sql。
对于 Java 7 及以下版本,可使用容器注解 @SqlGroup:
@SqlGroup({
@Sql(scripts = "/employees_schema.sql",
config = @SqlConfig(transactionMode = TransactionMode.ISOLATED)),
@Sql("/import_employees.sql")
})
public class SpringBootSqlGroupAnnotationIntegrationTest {
@Autowired
private EmployeeRepository employeeRepository;
@Test
public void testLoadDataForTestCase() {
assertEquals(3, employeeRepository.findAll().size());
}
}
9. 结论
本文介绍了如何利用 schema.sql 和 data.sql 文件初始化数据库结构并填充初始数据。
同时,也展示了如何在测试中使用 @Sql、@SqlConfig 和 @SqlGroup 注解加载测试数据。
重要提醒:这些方法适用于简单场景。对于复杂的数据库变更管理,建议使用更专业的工具,如 Liquibase 或 Flyway。