前言

随着项目的不断迭代,数据库表结构、数据都在发生着变化。甚至有的业务在多环境版本并行运行。数据为王的时代,管理好数据库的版本也成为了迫切的需要。如何能做到像 Git 之类的版本控制工具来管理数据库?Java 项目中常用 FlywayLiquibase 来管理数据库版本。其中 Flyway 相对来说比较受欢迎。

Flyway简介

flyway 是一个敏捷工具,用于数据库的移植。采用 Java 开发,支持所有兼容 JDBC 的数据库。
主要用于在你的应用版本不断升级的同时,升级你的数据库结构和里面的数据。

工作于

Windows,macOS,Linux,Docker,Java 和 Android

支持的构建工具

Maven和Gradle

支持的数据库

Oracle、SQL Server、DB2、MySQL、Aurora MySQL、MariaDB、Percona XtraDB群集、PostgreSQL、Aurora PostgreSQL、Redshift、CockroachDB、SAP HANA、Sybase ASE、Informix、H2、HSQLDB、Derby、SQLite、Firebird

Github

https://github.com/flyway/flyway

官方文档

https://flywaydb.org/documentation/concepts/migrations (强烈推荐看官方文档)

工作原理

  1. 项目启动,应用程序完成数据库连接池的建立后,Flyway自动运行。
  2. 初次使用时,Flyway会创建一个flyway_schema_history表,用于记录sql执行记录。
  3. Flyway会扫描项目指定路径下(默认是classpath:db/migration)的所有sql脚本,与flyway_schema_history表脚本记录进行比对。如果数据库记录执行过的脚本记录,与项目中的sql脚本不一致,Flyway会报错并停止项目执行。
  4. 如果校验通过,则根据表中的sql记录最大版本号,忽略所有版本号不大于该版本的脚本。再按照版本号从小到大,逐个执行其余脚本。

image.png

配置规则

FlywaySQL 文件分为 VersionedRepeatableUndo 三种:

  • Versioned 用于版本升级, 每个版本有唯一的版本号并只能执行一次.
  • Repeatable 可重复执行, 当 Flyway检测到 Repeatable 类型的 SQL 脚本的 checksum 有变动, Flyway 就会重新应用该脚本. 它并不用于版本更新, 这类的 migration 总是在 Versioned 执行之后才被执行。
  • Undo 用于撤销具有相同版本的版本化迁移带来的影响。但是该回滚过于粗暴,过于机械化,一般不推荐使用。一般建议使用 Versioned 模式来解决。


这三种的命名规则如下图:
image.png

  • Prefix 可配置,前缀标识,默认值 V 表示 Versioned, R 表示 Repeatable, U 表示 Undo
  • Version 标识版本号, 由一个或多个数字构成, 数字之间的分隔符可用点 . 或下划线 _
  • Separator 可配置, 用于分隔版本标识与描述信息, 默认为两个下划线 __
  • Description 描述信息, 文字之间可以用下划线 _ 或空格 分隔
  • Suffix 可配置, 后续标识, 默认为 .sql

其中,V开头的SQL执行优先级要比R开头的SQL优先级高。 :::warning 注意:名称中和 之间是两个下划线! :::

Flyway 是如何比较两个 SQL 文件的先后顺序呢?它采用 采用左对齐原则, 缺位用 0 代替 。举几个例子: 1.0.1.1 比 1.0.1 版本高。 1.0.10 比 1.0.9.4 版本高。 1.0.10 和 1.0.010 版本号一样高, 每个版本号部分的前导 0 会被忽略。

Baseline

当数据中已经存在内容时,再引入 Flyway,可通过 Baseline 设定一条基线。
image.png
如果想将基线之前的数据库中表结构和数据纳入 Flyway 一同管理,可以将基线前的状态导出成数据库脚本,通过 versioned migration 添加至 flyway 中,并设定 baselin version,Flyway 会忽略基线版本号(包括)之前版本的所有 migration。
如果不想管理基线之前的数据库状态(比如多模块或应用操作同一数据库,互相之间不受影响),可以只告诉 Flyway 执行 migration 的时候是存在基线的,这样就不会报出数据库非空的异常。

例:通过设定 spring.flyway.baseline-version=1.20.0参数,以忽略 1.20.0 版本以及之前的所有 migration。

实战

准备好一个空数据库,这里以mysql为例

image.png

创建一个基本的spirngboot项目

添加依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.flywaydb</groupId>
  7. <artifactId>flyway-core</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>mysql</groupId>
  11. <artifactId>mysql-connector-java</artifactId>
  12. <scope>runtime</scope>
  13. </dependency>

配置数据库以及flyway

  1. spring:
  2. datasource:
  3. driver-class-name: com.mysql.cj.jdbc.Driver
  4. url: jdbc:mysql://127.0.0.1:3306/flyway_db?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&rewriteBatchedStatements=true&useSSL=false&serverTimezone=GMT%2B8
  5. username: root
  6. password: root
  7. # datasource:
  8. # # h2 驱动
  9. # driver-class-name: org.h2.Driver
  10. # # h2 数据库 持久化路径 库名: flyway mysql模式
  11. # url: jdbc:h2:file:/Users/anin/install/data/h2/flyway_db;MODE=MySQL;DATABASE_TO_LOWER=TRUE
  12. # username: sa
  13. # password:
  14. # h2:
  15. # # 开启console 访问 默认false
  16. # console:
  17. # enabled: true
  18. # settings:
  19. # # 开启h2 console 跟踪 方便调试 默认 false
  20. # trace: true
  21. # # 允许console 远程访问 默认false
  22. # web-allow-others: true
  23. # # h2 访问路径上下文
  24. # path: /h2-console
  25. # flyway 配置
  26. flyway:
  27. # 启用或禁用 flyway
  28. enabled: true
  29. # flyway 的 clean 命令会删除指定 schema 下的所有 table, 生产务必禁掉。这个默认值是 false 理论上作为默认配置是不科学的。
  30. clean-disabled: true
  31. # SQL 脚本的目录,多个路径使用逗号分隔 默认值 classpath:db/migration
  32. locations: classpath:db/migration
  33. # metadata 版本控制信息表 默认 flyway_schema_history
  34. table: flyway_schema_history
  35. # 如果没有 flyway_schema_history 这个 metadata 表, 在执行 flyway migrate 命令之前, 必须先执行 flyway baseline 命令
  36. # 设置为 true 后 flyway 将在需要 baseline 的时候, 自动执行一次 baseline。
  37. baseline-on-migrate: true
  38. # 指定 baseline 的版本号,默认值为 1, 低于该版本号的 SQL 文件, migrate 时会被忽略
  39. baseline-version: 1
  40. # 字符编码 默认 UTF-8
  41. encoding: UTF-8
  42. # 是否允许不按顺序迁移 开发建议 true 生产建议 false
  43. out-of-order: false
  44. # 需要 flyway 管控的 schema list,这里我们配置为flyway 缺省的话, 使用spring.datasource.url 配置的那个 schema,
  45. # 可以指定多个schema, 但仅会在第一个schema下建立 metadata 表, 也仅在第一个schema应用migration sql 脚本.
  46. # 但flyway Clean 命令会依次在这些schema下都执行一遍. 所以 确保生产 spring.flyway.clean-disabled 为 true
  47. schemas: flyway_db
  48. # 执行迁移时是否自动调用验证 当你的 版本不符合逻辑 比如 你先执行了 DML 而没有 对应的DDL 会抛出异常
  49. validate-on-migrate: true

配置数据迁移文件

在项目的 src/main/resources 下创建 db/migration 目录,该目录下放置需要数据迁移的文件。
结构如下图
image.png
此处的SQL语句命名需要遵从一定的规范,否则运行的时候flyway会报错。
本次测试新建名为 V1.0.0__add_table_user.sql 的文件,内容如下:

V1.0.0__add_table_user.sql

  1. use `flyway_db`;
  2. CREATE TABLE `user`
  3. (
  4. `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  5. `username` varchar(255) NOT NULL unique ,
  6. `password` varchar(255) NOT NULL,
  7. `age` int(3) NOT NULL,
  8. PRIMARY KEY (`id`)
  9. ) ENGINE = InnoDB
  10. DEFAULT CHARSET = utf8mb4;
  11. insert into flyway_db.user values (1,'zhangsan','123456',18);

V1.0.1__alter_table_user_add_addr.sql

  1. ALTER TABLE `flyway_db`.`user` ADD COLUMN `addr` varchar(255) NOT NULL default '' AFTER `age`;

R__add_unknown_user.sql

  1. insert into `user`(username,password,age) values('unknown','123456',33);

运行

运行截图如下:
image.png
数据库我们看到了加入了两张表,一张是我们配置的sql创建的user表,以及flyway的默认迁移记录表。
image.png
image.png
image.png

其他问题

版本问题

  1. Flyway Teams Edition or MySQL upgrade required: MySQL 5.7 is no longer supported by Flyway Community Edition, but still supported by Flyway Teams Edition.

升级你的mysql或者使用flyway的企业版,也可以考虑降低flyway的版本
将flyway-core版本降低至8.0以下即可(springboot版本降低至2.5.x或以下)。

执行方式

Flyway 的 migration 会在 Spring Boot 应用启动时自动执行,如果不想通过启动应用的方式执行,官方提供了命令行、API、以及 Maven 和 Gradle 插件的方式,但总的来说都会麻烦一些,因为需要将已经在 Spring Boot 中配置的参数,再到其他执行方式所各自要求的位置重新配置一遍,实用性一般。

生产环境数据安全

Flyway 的 Clean 命令,会将 Flyway 所连接的数据库中的所有内容全部清理掉,不论其中的表或数据是否是通过 Flyway 添加进去的。
在生产环境中使用 Flyway 确实也存在一定的风险,但这个风险不是 Flyway 本身造成的,有权连接生产库的人的任何一个误操作,都会导致生产环境数据的丢失,建议不要因噎废食。
Flyway 对此也提供了一定的防范措施,可通过禁用 Clean 命令来防止此问题发生,比如通过 Spring Boot 的 spring.flyway.clean-disabled=true 参数
每种执行 Flyway 命令的方式均可设置此参数,建议在所有环境都禁用 Clean 命令。

SQL 报错

通过 Spring Boot 自动执行 migration 时要注意,一旦 migration 执行失败,应用启动会终止。出现 migration 执行失败时,需要将 Schema History Table 表中的失败记录处理掉,才能再次执行 migration,否则应用会一直无法启动。

out-of-order

多人开发时,可能会出现 A 写了 V1 脚本,B 写了 V2 脚本,B 的代码先合并进去了,V2 脚本先执行了,此时 A 的 V1 脚本受版本号只能增加的要求不能再执行。
这种情况可以通过将 spring.flyway.out-of-order 设置为 true 来暂时取消这个限制,不过还是强烈建议 A 将 V1 脚本版本号改为 V3。

多个系统公用一个 数据库

多个系统公用一个 数据库 schema 时配置spring.flyway.table 为不同的系统设置不同的 metadata 表名而不使用缺省值 flyway_schema_history 。

扩展

Maven插件

以上步骤中,每次想要migration都需要运行整个springboot项目,并且只能执行migrate一种命令,其实flyway还是有很多其它命令的。maven插件给了我们不需要启动项目就能执行flyway各种命令的机会。

在pom中引入flyway的插件,同时配置好对应的数据库连接。

  1. <plugin>
  2. <groupId>org.flywaydb</groupId>
  3. <artifactId>flyway-maven-plugin</artifactId>
  4. <version>5.2.4</version>
  5. <configuration>
  6. <url>jdbc:mysql://localhost:3306/flyway?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT</url>
  7. <user>root</user>
  8. <password>root</password>
  9. <driver>com.mysql.cj.jdbc.Driver</driver>
  10. </configuration>
  11. </plugin>

然后更新maven插件列表,就可以看到flyway的全部命令了。此时,我们双击执行上图中的flyway:migrate的效果和启动整个工程执行migrate的效果是一样的。
image.png

  • baseline:对已经存在数据库Schema结构的数据库一种解决方案。实现在非空数据库新建MetaData表,并把Migrations应用到该数据库;也可以在已有表结构的数据库中实现添加Metadata表。
  • clean:清除掉对应数据库Schema中所有的对象,包括表结构,视图,存储过程等,clean操作在dev 和 test阶段很好用,但在生产环境务必禁用。
  • info:用于打印所有的Migrations的详细和状态信息,也是通过MetaData和Migrations完成的,可以快速定位当前的数据库版本。
  • repairrepair:操作能够修复metaData表,该操作在metadata出现错误时很有用。
  • undo:撤销操作,社区版不支持。
  • validate:验证已经apply的Migrations是否有变更,默认开启的,原理是对比MetaData表与本地Migrations的checkNum值,如果值相同则验证通过,否则失败。

    补充知识

  • flyway执行migrate必须在空白的数据库上进行,否则报错;

  • 对于已经有数据的数据库,必须先baseline,然后才能migrate;
  • clean操作是删除数据库的所有内容,包括baseline之前的内容;
  • 尽量不要修改已经执行过的SQL,即便是R开头的可反复执行的SQL,它们会不利于数据迁移;

    附录

    flyway的配置清单:
    1. flyway.baseline-description对执行迁移时基准版本的描述.
    2. flyway.baseline-on-migrate当迁移时发现目标schema非空,而且带有没有元数据的表时,是否自动执行基准迁移,默认false.
    3. flyway.baseline-version开始执行基准迁移时对现有的schema的版本打标签,默认值为1.
    4. flyway.check-location检查迁移脚本的位置是否存在,默认false.
    5. flyway.clean-on-validation-error当发现校验错误时是否自动调用clean,默认false.
    6. flyway.enabled是否开启flywary,默认true.
    7. flyway.encoding设置迁移时的编码,默认UTF-8.
    8. flyway.ignore-failed-future-migration当读取元数据表时是否忽略错误的迁移,默认false.
    9. flyway.init-sqls当初始化好连接时要执行的SQL.
    10. flyway.locations迁移脚本的位置,默认db/migration.
    11. flyway.out-of-order是否允许无序的迁移,默认false.
    12. flyway.password目标数据库的密码.
    13. flyway.placeholder-prefix设置每个placeholder的前缀,默认${.
    14. flyway.placeholder-replacementplaceholders是否要被替换,默认true.
    15. flyway.placeholder-suffix设置每个placeholder的后缀,默认}.
    16. flyway.placeholders.[placeholder name]设置placeholdervalue
    17. flyway.schemas设定需要flywary迁移的schema,大小写敏感,默认为连接默认的schema.
    18. flyway.sql-migration-prefix迁移文件的前缀,默认为V.
    19. flyway.sql-migration-separator迁移脚本的文件名分隔符,默认__
    20. flyway.sql-migration-suffix迁移脚本的后缀,默认为.sql
    21. flyway.tableflyway使用的元数据表名,默认为schema_version
    22. flyway.target迁移时使用的目标版本,默认为latest version
    23. flyway.url迁移时使用的JDBC URL,如果没有指定的话,将使用配置的主数据源
    24. flyway.user迁移数据库的用户名
    25. flyway.validate-on-migrate迁移时是否校验,默认为true