在 Spring Boot 中使用 Flyway 进行数据库迁移

在之前的 Spring Boot JdbcTemplate 教程 中,我们见识了如何使用 schema.sqldata.sql 脚本初始化数据库。这对于演示项目可能有用,但对于实际应用,我们应该使用数据库迁移工具。

Flyway 是最流行的基于 Java 的数据库迁移工具。可以将 Flyway 作为独立库,或使用 flyway-maven-plugin 或使用 Flyway Gradle 插件进行数据库迁移。

Spring Boot 提供了开箱即用的支持,用于 Flyway 数据库迁移。让我们看看如何创建一个使用 Spring Data JPA 与 PostgreSQL 数据库交互,并使用 Flyway 实现数据库迁移的 Spring Boot 应用。

首先,访问 https://start.springboot.io/,选择 Spring WebSpring Data JPAPostgreSQL DriverFlyway MigrationTestcontainers starter,创建 Spring Boot 应用程序。

创建 Flyway 迁移脚本

Flyway 遵循 V<VERSION>__<DESCRIPTION>.sql 命名约定来命名其版本化的迁移脚本。让我们在 src/main/resources/db/migration 文件夹下添加以下两个迁移脚本。

V1__create_tables.sql:

create table bookmarks
(
    id         bigserial not null,
    title      varchar   not null,
    url        varchar   not null,
    created_at timestamp,
    primary key (id)
);

V2__create_bookmarks_indexes.sql:

CREATE INDEX idx_bookmarks_title ON bookmarks(title);

使用 Testcontainers 建立 Postgres 数据库

Spring Boot 3.1.0 引入了对 Testcontainers 的支持,我们可以用它来编写集成测试和本地开发。

在生成应用时,我们选择了 PostgreSQL DriverTestcontainers starter。因此,生成的应用程序将在 src/test/java 包下有一个 TestApplication.java,内容如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration(proxyBeanMethods = false)
public class TestApplication {

  @Bean
  @ServiceConnection
  PostgreSQLContainer<?> postgresContainer() {
    return new PostgreSQLContainer<>(DockerImageName.parse("postgres:15-alpine"));
  }

  public static void main(String[] args) {
    SpringApplication
            .from(Application::main)
            .with(TestApplication.class)
            .run(args);
  }
}

执行 Flyway 迁移

现在,你可以在 IDE 中运行 TestApplication 类,或在命令行中运行 ./mvnw spring-boot:test-run 来启动应用。然后,你会发现以下与 Flyway 执行相关的日志:

INFO 4009 --- [           main] tc.postgres:15-alpine                    : Container is started (JDBC URL: jdbc:postgresql://127.0.0.1:33331/test?loggerLevel=OFF)
INFO 4009 --- [           main] o.f.c.internal.license.VersionPrinter    : Flyway Community Edition 9.16.3 by Redgate
INFO 4009 --- [           main] o.f.c.internal.license.VersionPrinter    : See release notes here: https://rd.gt/416ObMi
INFO 4009 --- [           main] o.f.c.internal.license.VersionPrinter    : 
INFO 4009 --- [           main] o.f.c.i.database.base.BaseDatabaseType   : Database: jdbc:postgresql://127.0.0.1:33331/test (PostgreSQL 15.3)
INFO 4009 --- [           main] o.f.c.i.s.JdbcTableSchemaHistory         : Schema history table "public"."flyway_schema_history" does not exist yet
INFO 4009 --- [           main] o.f.core.internal.command.DbValidate     : Successfully validated 2 migrations (execution time 00:00.010s)
INFO 4009 --- [           main] o.f.c.i.s.JdbcTableSchemaHistory         : Creating Schema History table "public"."flyway_schema_history" ...
INFO 4009 --- [           main] o.f.core.internal.command.DbMigrate      : Current version of schema "public": << Empty Schema >>
INFO 4009 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema "public" to version "1 - create tables"
INFO 4009 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema "public" to version "2 - create bookmarks indexes"
INFO 4009 --- [           main] o.f.core.internal.command.DbMigrate      : Successfully applied 2 migrations to schema "public", now at version v2 (execution time 00:00.041s)

Flyway 默认会在 flyway_schema_history 表中记录所有应用迁移的历史。如果现在查看 flyway_schema_history 表中的数据,可以看到以下几行:

| installed_rank | version | description              | type | script                           | checksum   | installed_by | installed_on               | execution_time | success |
|:---------------|:--------|:-------------------------|:-----|:---------------------------------|:-----------|:-------------|:---------------------------|:---------------|:--------|
| 1              | 1       | create tables            | SQL  | V1__create_tables.sql            | 1020037327 | test         | 2023-08-09 09:13:04.439012 | 6              | true    |
| 2              | 2       | create bookmarks indexes | SQL  | V2__create_bookmarks_indexes.sql | 732086927  | test         | 2023-08-09 09:13:04.456876 | 4              | true    |

如果你保持运行相同的数据库实例,并重新启动应用程序,Flyway 不会重新运行已经应用的迁移。如果你添加了新的迁移脚本,那么只有这些迁移脚本会被执行。

Flyway 规则:

必须遵守以下规则,否则 Flyway 将在应用迁移时出错:

  • 在 flyway 迁移脚本文件名中不应有重复的版本号。例如:V1__init.sqlV1__indexes.sql。这里多次使用了版本号 1
  • 迁移一旦应用,就不得更改其内容。

自定义 Flyway 配置

Spring Boot 为 Flyway 迁移提供了合理的默认值,但你可以在 application.properties 文件中使用 spring.flyway.{property-name} 属性配置各种 Flyway 配置属性。

不同数据库的 Flyway 迁移

如果你正在构建一个可以与不同数据库一起使用的应用,那么你可以按以下方式配置 flyway migration location:

spring.flyway.locations=classpath:db/migration/{vendor}

然后,你可以把 mysql 专用脚本放在 src/main/resources/db/migration/mysql 目录下,把 postgresql 专用脚本放在 src/main/resources/db/migration/postgresql 目录下,等等。你可以在 org.springframework.boot.jdbc.DatabaseDriver 类中查看更多的数据库供应商名称。

除此之外,还有一些其他的 flyway properties 配置:

# 禁止flyway 执行
spring.flyway.enabled=false

# 如果你有一个现有的数据库,并打算开始使用 Flyway 进行新的数据库更改。
spring.flyway.baseline-on-migrate=true

# 自定义 flyway 迁移跟踪表名称
spring.flyway.table=db_migrations

# 如果出现执行错误,清空数据库并重新运行所有迁移操作
# 切勿在生产中使用。仅适用于开发!!!
spring.flyway.clean-disabled=false
spring.flyway.clean-on-validation-error=true

基于 Java 的 Flyway 迁移

除了 SQL 脚本,还可以使用 Java 以为编程式编写数据库迁移。在 db.migration 包中创建 V3__InsertSampleData.java 方法,如下:

package db.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;

public class V3__InsertSampleData extends BaseJavaMigration {

  public void migrate(Context context) {
      JdbcTemplate jdbcTemplate = new JdbcTemplate(
        new SingleConnectionDataSource(context.getConnection(), true));
      
      Long userId = jdbcTemplate.query("...");
      // insert roles for userId
      jdbcTemplate.update("...");
  }
  
}

如果数据库更改涉及复杂的逻辑,而使用普通 SQL 又难以编写,那么基于 Java 的迁移就会非常方便。

你可以将 Flyway 与不同的持久化技术结合使用,如 JdbcTemplateSpring Data JdbcSpring Data JPAjOOQ 等。

总结

Spring Boot 对 Flyway 开箱即用的支持使得非常容易就可以实现生产级数据库迁移。


参考:https://www.sivalabs.in/spring-boot-flyway-database-migration-tutorial/