【实战】Flyway迁移指南最佳实践

本文最后更新于:2021年6月15日 晚上

项目在多环境迭代开发过程中,数据库的表结构不断变更,在部署时,往往会出现数据库表结构未及时变更导致出现问题,耗费在表结构上的时间相当多,上线过程持续痛苦,代码有 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

迁移步骤

  1. 引入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    <version>6.4.4</version>
    </dependency>

    注:如果 springboot 版本低于 2.0.0,最好使用 5.2.1 版本的 flyway-core

  2. 添加 flyway 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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_{项目名称},这样基本可以做到不会发生冲突了。

  3. 项目 resource 目录添加文件夹.

    创建上一步中 spring.flyway.locations 中指定值的目录,本文是创建 db 目录.

  4. 项目 SQL 迁移.

    SQL 迁移这里有两种情况,第一种是当前项目在所有环境都是初次部署,即数据库中尚未有任何当前项目的表,这种情况很好处理,主要讲一下非初次部署的情况 SQL 迁移步骤。

    1. 先 dump 一份所有环境中当前项目最新版本的表结构,在 resources/db目录中创建一个 base_init.sql 文件,将最新版本的 DDL 以及需要初始化的数据放到这个文件中,这个 sql 文件后期就不要做任何修改。

    2. resources/db 目录增加一个名为 V1__init.sql的文件,内容为空,用于占位

    3. 将所有环境的表结构都统一到 base_init.sql这个版本

    4. 如果有新增的 DDL,则创建一个高版本的 sql 文件,如V2__add_table.sql,项目启动的时候会自动执行 sql,但是不会执行 V1 版本的,所以添加了 V1 版本的用于占位。注意如果新增的 DDL 版本没有执行出错,切勿修改!!!

    5. sql 文件的命名具有一定规则,以V开头,接着两个下划线 __,接着可以写注释,然后以 .sql 结尾,如V3__alter_table.sql 版本号支持小版本x.y.z格式,但是为了简单起见,直接用一个数字递增更方便。

    6. 如果需要部署到新的环境,则只需要执行 base_init.sql中 DDL 即可,其他版本的 DDL 交给 flyway 就可以了

    7. Over~

    8. 有时候如果新版本的 DDL 写错了,可能会导致 flyway 执行失败,会在元数据表中增加一条执行 status 为 0 的记录,只要 status 有为 0 的记录,项目就无法启动,这样就很难受,网上解决方式多是手动去数据库删除这条记录,这未免太危险,可以利用 flyway 的 callback 来实现执行失败,自动删除失败记录。在 resources/db目录下添加名为 afterMigrateError.sql文件,文件内容为

      1
      2
      -- SQL 执行失败,清理 flyway 元数据表中失败的执行记录
      DELETE IGNORE FROM `${flyway-table}` WHERE success = 0;

      其中的变量就是当前项目元数据表的表名称。

    9. 如果当前项目在所有环境都是初次部署,那就不需要 base_init.sql,初始化直接放到 V1__init.sql 当中,上线时不再需要手动执行 SQL,全部交由 flyway 来执行即可。如果数据库比如测试环境存在经常手动修改表增加表的情况,需要关闭 flyway,存在 flyway 因为在手动执行 SQL 执行之后再执行导致执行失败的情况,所以某个环境使用了 flyway 控制版本之后,就不要再手动增删改表。

    10. Over~~~

常见问题

  1. 出现 java.sql.SQLException: sql injection violation, comment not allow : CREATE TABLE xxxxxx.flyway_schema_history_xxx
    检查是否使用的是 druid ,错误原因是建表语句中包含了 SQL 注释,druid 默认会拦截包含注释的 SQL 执行,需要修改 druid 配置,允许注释。(不知道 flyway 为什么要把注释写到建表语句中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    spring:
    datasource:
    druid:
    # ...... 省略其他
    filter:
    stat:
    enabled: true
    slf4j:
    enabled: true
    wall:
    enabled: true
    config:
    comment-allow: true
    # filters: stat,wall,slf4j 注释此行,filter改成上面的格式

总结

针对多环境迁移流程

  1. 所有环境数据库表版本统一到最新版本
  2. 将最新版本 DDL 放到 base_init.sql
  3. 后续迭代在 resource/db 目录下增加新版本的 DDL 文件
  4. 如果是新环境,先通过 base_init.sql 进行初始化,再启动项目即可,非新环境,直接启动项目即可

示例代码

参考