Spring Boot 加载初始数据快速指南

更新于 2025-12-30

baeldung 2024-05-11

1. 概述

Spring Boot 让我们非常轻松地管理数据库变更。如果使用默认配置,它会自动扫描项目中的实体类,并创建对应的数据库表。

但有时我们需要对数据库变更进行更精细的控制。这时,就可以使用 Spring 提供的 data.sqlschema.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

这将确保:

  1. Hibernate 先完成其表结构创建;
  2. 然后执行 schema.sql(用于额外结构变更);
  3. 最后执行 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_CLASSAFTER_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.sqldata.sql 文件初始化数据库结构并填充初始数据。

同时,也展示了如何在测试中使用 @Sql@SqlConfig@SqlGroup 注解加载测试数据。

重要提醒:这些方法适用于简单场景。对于复杂的数据库变更管理,建议使用更专业的工具,如 LiquibaseFlyway