介绍
Liquibase是一款支持跨数据库版本控制工具,可以让你快速安全地把开发数据库迁移到生产数据库。它提供了多种描述变更集的文件格式,包括SQL、XML、YAML和JSON。
基本上,理解了下面这个图(来自官网),就大致了解了Liquibase的组成了。
关于设计的最佳实践
选择合适的changelog structure
官方推荐了两种组织changelog文件的方式:
Object-oriented Structure
按照数据库对象(类型)进行分类管理,利用include
或者includeAll
从根文件来包含其他nested changelog,例如:
perl
com
example
db
changelog
changelog-root.xml
changelog-indexes
my-favorite-index.xml
that-other-index.xml
changelog-tables
employees.xml
customers.xml
Release-Oriented Structure
按照发布版本进行分类管理,利用include
或者includeAll
从根文件来包含其他nested changelog,例如:
com
example
db
changelog
changelog-root.xml
changelog-1.0.xml
changelog-1.1.xml
changelog-2.0.xml
liquibase常用命令
初探liquibase
在配置好liquibase.properties文件以后,执行下述命令
bash
# 查看liquibase的状态,比如是否包含数据库驱动,用户名/密码是否正确
liquibase status
# inspect deployment sql(不会真正执行,但是会把sql文件输出)
liquibase update-sql
# 真正执行sql
liquibase update
常用命令
命令 | 说明 |
---|---|
liquibase drop-all |
删除当前库中的所有表,但是不包括存储过程、函数等等 |
liquibase generate-changelog |
从已有数据库生成一个xml格式的changelog文件 |
liquibase tag --tag=v1.0 |
该当前状态打上tag |
liquibase rollback --tag=v1.0 |
回滚到tag=v1.0的changeset |
liquibase status |
查看当前尚未执行的changeset |
liquibase changelog-sync |
标记changeset已经被运行过,让多个数据库保持同步的初始状态 |
注:如果发现rollback并没有达到预期效果,要检查执行rollback命令使用的changelog-file与DATABASECHANGELOG里面记录的filename是否一致。
举例:
bash
# 从已有数据库中生成changeset,并且包含数据
liquibase generate-changelog \
--diffTypes=tables,columns,data \
--dataOutputDirectory=myData
Changelog
Liquibase的属性们
context/contextFilter
用于过滤某些环境信息,比如开发环境/测试环境/生产环境等等。
-
context一般用于执行changeset的时候,作为传入的选项,比如
liquibase update --context="test,main"
,或者如果在Java程序中,指定运行属性:spring.liquibase.contexts=test,main
-
contextFilter用在changelog、changeset或者include/includeAll中,用于过滤执行时传入的context值
- 在changelog中指定的contextFilter对所有位于该chagnelog中的changeset有效
- 在include/includeAll中指定的contextFilter对被包含的所有changelog有效
- 在changeset中指定的contextFilter只在该changeset中有效
示例:
xml
<databaseChangeLog ...>
<changeSet id="3" author="jimmy" contextFilter="prod">
<addLookupTable existingTableName="person" existingColumnName="state"
newTableName="state" newColumnName="id" newColumnDataType="char(2)"/>
</changeSet>
</databaseChangeLog>
最佳实践
-
测试数据,应该与所有其他的changeset保持同步,即:把测试数据写入到同一个changelog中,并且通过contextFilter来区分测试与生产等其他环境
If you manage your test data with Liquibase, it is best practice to have this data in line with all your other changesets, but marked with a
"test"
contextFilter.xml<databaseChangeLog> <!-- 其他的changeset --> <changeSet id="4" author="jimmy" contextFilter="test"> <!-- insert all test data --> </changeSet> <!-- 其他的changeset --> </databaseChangeLog>
-
对于多数据库,不建议使用contextFilter,而是使用
dbms
这个tag来标注it is a best practice to use the
dbms
tag to differentiate changesets by database type, and then runliquibase update
in your command line.xml<changeSet id="1-lawful-good" author="jimmy" dbms="postgres"> <createTable tableName="my_postgres_table"> <column name="id" type="int"/> </createTable> </changeSet>
Labels
为changeset指定标签,来实现筛选changeset的目的。执行liquibase的update时使用labelFilter
表达式来通过标签指定要运行的changeset,在设计changeset的时候,通过labels
来指定changeset的标签。
比如,在运行时(如果使用了spring liquibase)
yaml
spring:
liquibase:
change-log: classpath:db/changelog/changelog.xml
contexts: dev
label-filter: 1.0 or (1.1 and !shopping_cart)
在changeset的配置中:
xml
<changeSet id="2" author="jimmy" labels="1.0,2.0">
<addColumn tableName="person">
<column name="username" type="varchar(8)"/>
</addColumn>
</changeSet>
<changeSet id="3" author="jimmy" labels="shopping-cart">
<addLookupTable existingTableName="person" existingColumnName="state"
newTableName="state" newColumnName="id" newColumnDataType="char(2)"/>
</changeSet>
那么id=3的changeset不会执行,因为不满足条件
最佳实践
- 使用labels来枚举或描述changeset的作用和用途,比如版本、功能等等
- 如果想要控制过滤逻辑(filter logic),那么使用context更加合适
failOnError
默认值:true,表示遇到错误就停止运行。
最佳实践
- 使用preconditions来控制changeset的运行,而不是用failOnError=false
- 使用contextFilter、dbms等控制特定环境/数据库才能只从的changeset,而不是用failOnError
If you use
failOnError
frequently, consider whether there are any underlying issues with your database architecture that you can address instead.
logicalFilePath
Liquibase uses the following pattern to create a unique identifier for a changeset:
id/author/filepath
.
filepath默认是你指定的属性spring.liquibase.change-log
的值,logicalFilePath可以修改这个默认值,用于如下几个场景:
-
多个developer共享一个changelog文件,但是每个developer都有自己的路径
- 通过logicalFilePath来指向同一个位置
-
代码重构导致changelog文件的位置发生了变化
- logicalFilePath应该仍然执行代码重构之前的位置
-
多个模块都存在changelog的时候,避免id冲突
- 比如:在logicalFilePath中加入模块名
runAlways
默认false,如果true,则表示该changeset每次都要运行,每次运行都会更新DATABASECHANGELOG表中对应的记录,使用场景包括:
- 通过update这个change type来更新时间
- 通过tagDatabase这个change type来标记当前数据库的状态
- 使用sql或者sqlFile来运行特定脚本更新对象的权限
runOnChange
默认false,如果设置成ture,当每次changeset发生修改的时候,这个属性在以下场景很有用处:
- 使用了
CREATE or REPLACE
语句的存储过程或者视图,可以避免每次修改都重新记录一个changset。
xml
<changeSet author="your.name" id="changeset01" runOnChange="true" >
<createProcedure>
. . .
</createProcedure>
</changeSet>
runOrder
可用值:first
,last
,分别用于让changeset在第一个运行和最后一个运行。
runWith
这是一个用来扩展Liquibase执行器的属性,如果默认的执行器(JDBC)不能满足需求(比如处理特定数据库的特定语法的复杂SQL),那么可以通过自己编写执行器(executor)来满足需求。
自定义executor必须继承自AbstractExecutor
,然后通过SPI机制(META-INF/services)注册到Liquibase,并且指定一个名字,这个名字就是runWith需要的值。
Attribute | Value | Notes |
---|---|---|
runWith |
jdbc |
Default value if none specified. See Class JdbcExecutor. |
mongosh |
Executor for MongoDB. See Using Liquibase with MongoDB Pro. | |
psql |
Executor for PostgreSQL. See Use PSQL and runWith on PostgreSQL. | |
sqlplus |
Executor for Oracle. See Use SQL Plus and runWith on Oracle Database and Use SQL Plus and Oracle Proxy User. | |
sqlcmd |
Executor for MSSQL Server. See Use SQLCMD and runWith on Microsoft SQL Server. | |
<custom> |
Custom executor. See Add a Native Executor. |
Preconditions
在chengeset被执行之前的预检查,根据配置,如果预检查失败,可能会导致changeset不会执行或者终结整个changelog的执行。
可用的Proconditions
Precondition | 说明 |
---|---|
changeLogPropertyDefined |
检查属性是否存在 |
changeSetExecuted |
检查是否某个changeset已经被执行过 |
dbms |
检查数据库类型 |
sqlCheck |
执行一段SQL,这段SQL必须返回一个值,将其与期望值进行比较 |
rowCount |
检查数据库表的行数是否满足条件 |
runningAs |
检查是否以某个用户运行 |
customPrecondition |
指定自定义的precondition,需要编写一个类,实现接口:liquibase.precondition.CustomPrecondition |
tableExists |
检查表是否存在 |
columnExists |
检查某个表的某个字段是否存在 |
viewExists |
检查师徒是否存在 |
sequenceExists |
检查指定的sequence是否存在 |
primaryKeyExists |
检查主键是否存在 |
foreignKeyConstraintExists |
检查外键是否存在 |
uniqueConstraintExists |
检查唯一约束是否存在 |
indexExists |
检查索引是否存在 |
举例:
xml
<changeSet id="2" author="jimmy" labels="1.0,2.0">
<preConditions onFail="HALT" onError="HALT">
<sqlCheck expectedResult="0">
SELECT COUNT(*)
FROM person
</sqlCheck>
</preConditions>
<addColumn tableName="person">
<column name="username" type="varchar(8)"/>
</addColumn>
</changeSet>
属性替换
在changeset中,可以定义属性,就像maven中的属性一样。然后这些属性可以通过命令行、环境变量、执行参数等方式传递给changeset,以下是一个示例。
首先,在changelog中,定义如下:
xml
<!-- 定义一个属性 -->
<property name="table.name" value="person"/>
<!-- 在这个changeset中,使用${table.name} -->
<changeSet id="1" author="jimmy" runInTransaction="false">
<createTable tableName="${table.name}">
<column name="id" type="int" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="firstname" type="varchar(50)"/>
<column name="lastname" type="varchar(50)">
<constraints nullable="false"/>
</column>
<column name="state" type="char(2)"/>
</createTable>
</changeSet>
<changeSet id="2" author="jimmy" labels="1.0,2.0">
<preConditions onFail="HALT" onError="HALT">
<!-- 在sqlCheck的sql中,也可以使用$table.name} -->
<sqlCheck expectedResult="0">
SELECT COUNT(*)
FROM ${table.name}
</sqlCheck>
</preConditions>
<addColumn tableName="${table.name}">
<column name="username" type="varchar(8)"/>
</addColumn>
</changeSet>
然后,如果使用了spring+Liquibase,则在application.yml中,定义如下:
yaml
spring:
liquibase:
parameters:
table.name: user
Search Path
查找changelog文件的一组基础路径,类似classpath的查找方式。
可以在liquibase.properties文件中指定searchPath:
javascript
liquibase.searchPath: /path/to/a,/path/to/b
暂时不清楚如何在Spring Boot程序中指定searchPath
元数据表
支撑Liquibase运行,需要两张元数据表:
-
DATABASECHANGELOG
- 用于跟踪哪些changeset已经运行过
id
、author
和filename
三个字段唯一标识一个changeset
-
DATABASECHANGELOGLOCK
- 用于给正在执行的操作加锁,保证同时只有一个Liquibase运行
- 如果需要手动解锁,可以运行CLI:
liquibase release-locks
,当然,也可以手动执行sql语句把locked字段设置为0
最佳实践
- 规划合适的目录结构
- Object-oriented Structure:以数据库对象(或对象类型)为基础划分changelog
- Release-oriented Structure:以release版本为基础划分changelog
- 使用root changelog,然后include/includeAll其他的changelog
- 在一个changeset中只指定一个change
- 如果能够确保多个change能够在一个事务中运行,那么可以指定多个
- 规划changeset id,最好是顺序增长的数字,比如:1、2、3
- 为复杂和不能自解释的changeset增加comment
- 规划rollback策略,在开发环境中严格测试
- 根据环境管理你的数据
- 比如使用context来准备测试数据,并且只能在测试环境运行
Liquibase Workflows
开发者的工作流程
假设你的liquibase.properties文件里面,url指向了本地数据库
-
在开发环境增加changeset到changelog中
-
在本地数据库应用changeset
bashliquibase update
-
查看有哪些changeset尚未应用到远程数据库
bashliquibase status --verbose --url=<远程数据库地址>
-
查看更新到远程数据库需要执行的sql语句
bashliquibase update-sql --rul=<远程数据库地址> # 或者使用diff命令 liquibase diff --url=<本地数据库地址> --referenceUrl=<远程数据库地址>
-
提交changelog到git仓库
-
把changeset应用到远程数据库中
bashliquibase update --url=<远程数据库地址>
在已存在环境使用Liquibase
在开发到某个阶段以后,需要在开发环境、测试环境、UAT环境使用Liquibase同步数据库状态,此时可以使用如下步骤:
-
从基准数据库生成changelog
bashliquibase generate-changelog --changelog-file=dbchangelog.xml
-
把changelog中的changeset同步到其他数据库,但是只是标记为已经执行,不做实际同步
bashliquibase changelog-sync --changelog-file=dbchangelog.xml
对离线数据库的支持
对于远程数据库不能直连的情况(比如只能通过堡垒机才能连接),可以先利用update-sql命令来生成sql文件,然后再在远程数据库中执行这个sql文件,来达到不同changeset的目的。但是需要注意:
It is important that the database you generate the SQL from is the same as the database(s) you plan to run the SQL against.
在实际操作中,可以把远程数据中的DATABASECHANGELOG表拷贝到测试数据库,然后再在测试数据库上执行update-sql命令,来生成将来要同步到生成数据库的sql语句。