OopStorm

Using Liquibase with Gradle in Spring Project

2018/05/15 Share

Liquibase 是什么

引用 Liquibase 官网 的一张图片:

Source Control for Your Database

类似的工具还有 Flyway

为什么选择 Liquibase

Flyway 官网 上有一个同类工具特性的对比,详见 Feature Comparison 部分或下图:

Feature Comparison

看图的话,Flyway 完胜,不过 等等!看完下面内容再做决定

一句话总结一下:

1
2
面向 SQL,选择 Flyway
不面向 SQL,选择 Liquibase

How to use?

Liquibase 的使用方式可参考官方提供的 Quick Start 文档,这里主要讲一下 Liquibase 和 Spring 集成使用的方式

版本

先统一一下本文中使用的各类组件的版本:

入口 bean

1
2
3
4
5
<bean id="liquibase" class="liquibase.integration.spring.SpringLiquibase" lazy-init="false">
<property name="dataSource" ref="dataSource"/>
<property name="changeLog" value="classpath*:liquibase/changelog.xml"/>
<property name="contexts" value="${database.liquibase.profile}"/>
</bean>

需要注意几点:

  1. 数据库的变更最好在应用启动时进行,所以需要给入口 bean 配置上 lazy-init="false"
  2. Spring 提供的 profiles 机制可以对应到 Liquibase 的 Contexts 上,以便在不同的 profile 执行不同的数据库操作
  3. 大型项目一般都会分模块,模块最终可能会以 jar 包方式被引用。入口 changelog.xml 可能也是存在于 jar 包中的。故 changeLog 属性的 value 配置为 classpath*:liquibase/changelog.xml想法很不错,但这里会遇到一个问题:CORE-3139

CORE-3139

CORE-3139 是 Liquibase 无法从 JAR 包中读取资源文件的一个 bug。这个 bug 已有 PR #725 进行了处理,CORE-3139 中也标记这个问题已在 Liquibase v3.5.4 版本中修复,但事实证明在 Liquibase v3.6.1 版本中仍然存在,且之前修复这个问题的代码也基本都被覆盖掉了 orz。

这个问题只会影响到从 JAR 包中读取资源文件的情况(比如 changelog.xml 在 JAR 包中),资源直接存在于文件系统时并不受此问题影响。

为了避免这个问题再反复出现,新提交了一个 PR #767 对问题进行了修正,并补充了单元测试。如需将 changelog 打入 JAR 包中,可使用合并了此 PR 的版本的 Liquibase。

Change Log 的组织

项目模块化后,Change Log 也需要分布到各个模块中,与官方 最佳实践 给出的结构不同,项目中的结构可能更类似下面的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├── a
│   └── src
│   └── main
│   └── resources
│   └── liquibase
│   └── changelog.xml
├── b
│   └── src
│   └── main
│   └── resources
│   └── liquibase
│   └── changelogs
│   ├── changelog-ddl-b.xml
│   └── changelog-dml-b.xml
└── c
└── src
└── main
└── resources
└── liquibase
└── changelogs
└── changelog-dml-c.xml

a 模块作为 changelog 入口模块外,bc 模块可能都是被以 JAR 包方式选择性引入的,即 a 模块的 changelog.xml 不应该感知到 bc 模块内的 changelog-*.xml;同时当引入 bc 模块时又能将模块下的 changelog-*.xml 都正常加载到。

实现这个需求需借助 Liquibase 提供的 includeAll,同时要注意下其中的 Warnings

1
If you do choose to use the includeAll tag, make sure you have a naming strategy in place that will insure that you will never have conflicts or need to rename files to change to force a reordering.

可参考如下方式组织 changelogs:

  • 入口 changelog 为 liquibase/changelog.xml
  • 为避免入口 changelog include 到自己,各模块的 changelog 存放路径要与入口 changelog 路径有区别,如放在 liquibase/changelogs/ 路径下
  • 将 DDL 和 DML 分开放置,且按 changelog-[ddl/dml]-{module}.xml 方式命名,因为 Liquibase 是将所有 changelog 文件按字典序方式执行,这样可以保证 DDL 会优先 DML 执行,且不会因路径相同导致 changelog 文件被覆盖

注:即使在不同 JAR 包下,相对路径相同的 changelog 文件也会被覆盖

如需从 JAR 包中读取 changelog,你可能需要 PR #767

入口 Change Log

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>

<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">
<includeAll path="classpath*:liquibase/changelogs/" context="dev, production"/>
</databaseChangeLog>

Change Log Template

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">
<changeSet author="alphahinex" id="20180515-1">
...
</changeSet>
</databaseChangeLog>

利用 Gradle 执行 Liquibase 命令

如果不是从项目起始时就使用了 Liquibase,或者不想手写 Change Set,可以通过 Liquibase 提供的 命令 来生成 Change Log 或 DDL 的变更(数据的变更暂不支持生成)。如果是使用 Gradle 的项目,可以利用 Liquibase 的 Gradle 插件 来执行命令,更加便捷。

Liquibase 项目本身的活跃度目前并不高,插件的活跃度及文档的准确性更是问题重重,这也是本文存在的意义之一。

Gradle 配置

H2 数据库 为例,为执行 Liquibase 引入数据库驱动:

1
2
3
4
5
buildscript {
dependencies {
classpath 'com.h2database:h2:1.3.176'
}
}

引入插件:

1
2
3
plugins {
id 'org.liquibase.gradle' version '1.2.4'
}

定义 activities 用以生成不同的 Change Log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
ext {
// 数据库地址
liquibaseUrl = 'jdbc:h2:~/data/h2/pep_dev;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=FALSE;MVCC=TRUE'
// 旧版数据库地址,比较差异用
liquibaseOldUrl = 'jdbc:h2:~/data/h2-diff/pep_dev;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=FALSE;MVCC=TRUE'
liquibaseUsername = 'sa'
liquibasePassword = ''
}

liquibase {
activities {
// 生成全库结构 Change Log
genDev {
changeLogFile 'db/dev.xml'
url liquibaseUrl
username liquibaseUsername
password liquibasePassword
}
// 生成全库数据 Change Log
genDevData {
changeLogFile 'db/dev-data.xml'
url liquibaseUrl
username liquibaseUsername
password liquibasePassword
diffTypes 'data'
}
// 对比生成数据库结构变更 Change Log
diffDev {
changeLogFile 'db/dev-diff.xml'
url liquibaseOldUrl
username liquibaseUsername
password liquibasePassword
referenceUrl liquibaseUrl
referenceUsername liquibaseUsername
referencePassword liquibasePassword
}
}
// 若不设定 runList,所有 activities 都会被执行
// -PrunList=abc 并不好用,所以要执行指定的 activities 时,可直接修改此处值
// 多个值可用逗号间隔,如 'genDev,genDevData'
runList = 'genDevData'
}

生成 Change Log

全库 Change Log,包括结构和数据

  1. runList 设定为 genDev,genDevData
  2. ./gradlew generateChangelog
  3. ./db/dev.xml 可找到全库结构的 Change Log
  4. ./db/dev-data.xml 可找到全库数据的 Change Log。

注意:执行命令前需保证这两个文件不存在,否则会报错。

生成的 Change Log 中的内容最好进行检查和一定的调整,以免自动生成的名称没有直观的含义,或产生冲突等问题。

结构变更 Change Log

  1. 欲进行数据库结构变更时,先将变更前的库备份,例如从 ~/data/h2/pep_dev 备份至 ~/data/h2-diff/pep_dev
  2. runList 设定为 diffDev
  3. ./gradlew diffChangeLog
  4. 结构变更 Change Log 将生成至 ./db/dev-diff.xml

新增数据

  1. 在数据库中执行 insert 语句
  2. 修改 build.gradle 中配置的 liquibase.runList,将其值改为 genDevData
  3. ./gradlew generateChangelog
  4. db/dev-data.xml 中找到新增数据的 changeSet,并放至相应模块的 change log 文件中

变更数据

Liquibase 目前并不支持数据变更的生成,可参照官方文档手写 Change Set,语法基本类似 SQL,可参照:

参考资料

  1. Liquibase
  2. Liquibase Documentation
  3. liquibase-gradle-plugin
  4. Why not use HBM2DDL?
  5. Generating Change Logs
  6. Adding Liquibase on an Existing project
  7. liquibase-hibernate
  8. Database “Diff”
  9. Spring Integration
  10. Trimming ChangeLog Files
  11. comparing databases and generating sql script using liquibase
  12. Bundled Liquibase Changes
CATALOG
  1. 1. Liquibase 是什么
  2. 2. 为什么选择 Liquibase
  3. 3. How to use?
    1. 3.1. 版本
    2. 3.2. 入口 bean
      1. 3.2.1. CORE-3139
    3. 3.3. Change Log 的组织
      1. 3.3.1. 入口 Change Log
      2. 3.3.2. Change Log Template
    4. 3.4. 利用 Gradle 执行 Liquibase 命令
      1. 3.4.1. Gradle 配置
    5. 3.5. 生成 Change Log
      1. 3.5.1. 全库 Change Log,包括结构和数据
      2. 3.5.2. 结构变更 Change Log
      3. 3.5.3. 新增数据
      4. 3.5.4. 变更数据
  4. 4. 参考资料