【实战】Flyway迁移指南最佳实践
项目在多环境迭代开发过程中,数据库的表结构不断变更,在部署时,往往会出现数据库表结构未及时变更导致出现问题,耗费在表结构上的时间相当多,上线过程持续痛苦,代码有 GIT/SVN 来控制,数据库中的表版本也可以做到版本控制,本文讲解通过 flyway 的方式来管理数据库版本变动。
项目痛点
一个项目单个环境迭代开发的过程中,对于数据库表的修改 DDL,可以通过版本控制工具一起进行控制。只需要在项目上线之前,人工执行新增的 DDL 即可,DDL 的版本是与当前项目迭代版本一致,细致点不至于出现问题。
单个环境版本迭代,数据库的版本号变更流程如下图:
对于偏企业服务的公司而言,同一个项目会同时部署到多套环境当中。随着项目迭代进行,不同环境的项目版本可能并非是同步一致的,甚至因为有的环境需要定制化开发,出现同一个项目多个分支,代码也愈行愈远。
多个环境版本迭代,数据库的版本号变更流程如下图:
于是在这种情况下,上线服务之前就很痛苦,要想起上线环境的当前表版本是多少,想不起来,就要对比线上库里的表,判断是否执行过了增量的 DDL,每个环境的增量 DDL 都可能是不同的,需要针对每个环境写不同的 DDL,发布时战战兢兢地生怕漏了执行哪个版本的 DDL 导致线上 Bug。
那如何解决这种糟糕的情况呢?
理想状态:项目启动时自动维护数据库版本到最新,不需要人工处理 DDL,避免出错。
Flyway 就提供了达到这种理想状态的功能。
先说一下 Flyway 的原理。
开发者将每个版本的 DDL 放到项目中,项目在新环境启动时,会自动创建一张表用于记录 DDL 的版本信息,随后自动执行未执行过的 DDL,同时将执行过的 DDL 信息存入元数据表中。下次再启动时,检测到执行过了,就不会重复执行。
本文环境
- SpringBoot 2.1.3.RELEASE
- Flyway 5.2.1
迁移步骤
- 引入依赖
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>6.4.4</version>
</dependency>
注:如果 springboot 版本低于 2.0.0,最好使用 5.2.1 版本的 flyway-core
-
添加 flyway 配置文件
spring: flyway: # flyway 数据库 DDL 版本控制 enabled: true # 正式环境才开启 clean-disabled: true # 禁用数据库清理 encoding: UTF-8 locations: classpath:/db # flyway 会在库中创建此名称元数据表,用于记录所有版本演化和状态,同一个库不同项目可能冲突,每个项目一张表来记录 table: flyway_schema_history_FlywayExample #TODO 值的后缀指定为当前项目名称 baseline-version: 1 # 基线版本默认开始序号 默认为 1 baseline-on-migrate: true # 针对非空数据库是否默认调用基线版本,为空的话默认会调用基线版本 placeholders: # 定义 afterMigrateError.sql 要清理的元数据表表名 flyway-table: ${spring.flyway.table}
flyway 在启动的时候会自动创建一张名称为
flyway_schema_history
的元数据表,如果多个项目连接的是同一个数据库,会产生冲突影响,所以需要每个项目都有一张自己的元数据表,指定 spring.flyway.table 的值即可,可以指定为flyway_schema_history_{项目名称}
,这样基本可以做到不会发生冲突了。 -
项目 resource 目录添加文件夹.
创建上一步中 spring.flyway.locations 中指定值的目录,本文是创建 db 目录.
-
项目 SQL 迁移.
SQL 迁移这里有两种情况,第一种是当前项目在所有环境都是初次部署,即数据库中尚未有任何当前项目的表,这种情况很好处理,主要讲一下非初次部署的情况 SQL 迁移步骤。
-
先 dump 一份所有环境中当前项目最新版本的表结构,在
resources/db
目录中创建一个base_init.sql
文件,将最新版本的 DDL 以及需要初始化的数据放到这个文件中,这个 sql 文件后期就不要做任何修改。 -
resources/db
目录增加一个名为V1__init.sql
的文件,内容为空,用于占位 -
将所有环境的表结构都统一到
base_init.sql
这个版本 -
如果有新增的 DDL,则创建一个高版本的 sql 文件,如
V2__add_table.sql
,项目启动的时候会自动执行 sql,但是不会执行 V1 版本的,所以添加了 V1 版本的用于占位。注意如果新增的 DDL 版本没有执行出错,切勿修改!!! -
sql 文件的命名具有一定规则,以
V
开头,接着两个下划线__
,接着可以写注释,然后以.sql
结尾,如V3__alter_table.sql
版本号支持小版本x.y.z
格式,但是为了简单起见,直接用一个数字递增更方便。 -
如果需要部署到新的环境,则只需要执行
base_init.sql
中 DDL 即可,其他版本的 DDL 交给 flyway 就可以了。 -
Over~
-
有时候如果新版本的 DDL 写错了,可能会导致 flyway 执行失败,会在元数据表中增加一条执行 status 为 0 的记录,只要 status 有为 0 的记录,项目就无法启动,这样就很难受,网上解决方式多是手动去数据库删除这条记录,这未免太危险,可以利用 flyway 的 callback 来实现执行失败,自动删除失败记录。在
resources/db
目录下添加名为afterMigrateError.sq
文件,文件内容为-- SQL 执行失败,清理 flyway 元数据表中失败的执行记录 DELETE IGNORE FROM `${flyway-table}` WHERE success = 0;
其中的变量就是当前项目元数据表的表名称。
-
如果当前项目在所有环境都是初次部署,那就不需要
base_init.sql
,初始化直接放到V1__init.sql
当中,上线时不再需要手动执行 SQL,全部交由 flyway 来执行即可。如果数据库比如测试环境存在经常手动修改表增加表的情况,需要关闭 flyway,存在 flyway 因为在手动执行 SQL 执行之后再执行导致执行失败的情况,所以某个环境使用了 flyway 控制版本之后,就不要再手动增删改表。 -
Over~~~
-
常见问题
- 出现 java.sql.SQLException: sql injection violation, comment not allow : CREATE TABLE xxxxxx.flyway_schema_history_xxx
检查是否使用的是 druid ,错误原因是建表语句中包含了 SQL 注释,druid 默认会拦截包含注释的 SQL 执行,需要修改 druid 配置,允许注释。(不知道 flyway 为什么要把注释写到建表语句中)
spring:
datasource:
druid:
# ...... 省略其他
filter:
stat:
enabled: true
slf4j:
enabled: true
wall:
enabled: true
config:
comment-allow: true
# filters: stat,wall,slf4j 注释此行,filter改成上面的格式
总结
针对多环境迁移流程
- 所有环境数据库表版本统一到最新版本
- 将最新版本 DDL 放到
base_init.sql
中 - 后续迭代在 resource/db 目录下增加新版本的 DDL 文件
- 如果是新环境,先通过
base_init.sql
进行初始化,再启动项目即可,非新环境,直接启动项目即可
示例代码
参考
- Flyway 官网地址: https://flywaydb.org/
- Druid : https://github.com/alibaba/druid/issues/3546
- Springboot flyway repair callbacks
- Springboot flyway repair: https://stackoverflow.com/questions/37462550/flyway-repair-with-spring-boot