Liquibase
是一个用于跟踪、管理和应用数据库变化的开源的数据库重构工具。它将所有数据库的变化(包括结构和数据)都保存在 changelog
文件中,便于版本控制,它的目标是提供一种数据库类型无关的解决方案,通过执行 schema 类型的文件来达到迁移。
Liquibase 具备如下特性:
更多详情介绍,请查阅Liquibase 官方文档
因为 Spring Boot 已经内置支持整合 Liquibase,我们只需要在项目工程中引入 Liquibase 的依赖进行配置即可。
<dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> <version>3.6.3</version> </dependency> 复制代码
在 classpath 中配置 application.properties 或 application.yml 文件,
# 启用liquibase liquibase.enabled=true # 存储变化的文件(changelog)位置 liquibase.change-log=classpath:sample_change.sql # 检查存储变化的文件是否存在 liquibase.check-change-log-location=true # 分环境执行,若在 changelog 文件中设置了对应 context 属性,则只会执行与 dev 对应值的 changeset liquibase.contexts=dev # 执行前首先删除数据库,默认 false。若设置为 true,则执行变更前,会先删除目标数据库,请谨慎 liquibase.dropFirst=false # 执行更新时将回滚 SQL 写入的文件路径 liquibase.rollback-file= # 如果使用工程已配置的 datasource 数据源,则以下三个数据库连接参数可不配置 ## 访问数据库的连接地址 liquibase.url=jdbc:mysql://10.10.4.41:3306/test?useUnicode=true&characterEncoding=utf-8 # 访问数据库的用户名 liquibase.user=test # 访问数据库的密码 liquibase.password=test 复制代码
注意:如果使用工程已配置的 datasource
数据源,则 liquibase.url、liquibase.user
和 liquibase.password
这个三个参数可不配置,目标数据源即为工程已配置好的数据源。因为 Spring 会自动为 liquibase 获取可用的数据源,详情可查看这个类: org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
。
目前 Liquibase 支持 XML、YAML、JSON 和 SQL 格式四种格式的 changelog 文件。为了方便和直观,下面以基于 SQL 格式编写 changelog 文件:sample_change.sql
--liquibase formatted sql --changeset zhouyi:1 -- 创建用户表 CREATE TABLE `user` ( `id` int(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `age` int(10) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; --rollback drop table user; --changeset zhouyi:2 insert into user(id, name, age) values(1,'张三',29); insert into user(id, name, age) values(2,'李四',20); 复制代码
启动 Spring Boot 应用后,可以从日志看到输出:
2019-03-27 13:13:16.439 [main] [] INFO liquibase.executor.jvm.JdbcExecutor.info:42 - DELETE FROM test.DATABASECHANGELOGLOCK 2019-03-27 13:13:16.442 [main] [] INFO liquibase.executor.jvm.JdbcExecutor.info:42 - INSERT INTO test.DATABASECHANGELOGLOCK (ID, `LOCKED`) VALUES (1, 0) 2019-03-27 13:13:16.506 [main] [] INFO liquibase.executor.jvm.JdbcExecutor.info:42 - SELECT `LOCKED` FROM test.DATABASECHANGELOGLOCK WHERE ID=1 2019-03-27 13:13:16.586 [main] [] INFO liquibase.lockservice.StandardLockService.info:42 - Successfully acquired change log lock 2019-03-27 13:13:16.697 [main] [] INFO liquibase.changelog.StandardChangeLogHistoryService.info:42 - Creating database history table with name: test.DATABASECHANGELOG 2019-03-27 13:13:16.700 [main] [] INFO liquibase.executor.jvm.JdbcExecutor.info:42 - CREATE TABLE test.DATABASECHANGELOG (ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED datetime NOT NULL, ORDEREXECUTED INT NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35) NULL, `DESCRIPTION` VARCHAR(255) NULL, COMMENTS VARCHAR(255) NULL, TAG VARCHAR(255) NULL, LIQUIBASE VARCHAR(20) NULL, CONTEXTS VARCHAR(255) NULL, LABELS VARCHAR(255) NULL, DEPLOYMENT_ID VARCHAR(10) NULL) 2019-03-27 13:13:17.570 [main] [] INFO liquibase.executor.jvm.JdbcExecutor.info:42 - SELECT COUNT(*) FROM test.DATABASECHANGELOG 2019-03-27 13:13:17.575 [main] [] INFO liquibase.changelog.StandardChangeLogHistoryService.info:42 - Reading from test.DATABASECHANGELOG 2019-03-27 13:13:17.579 [main] [] INFO liquibase.executor.jvm.JdbcExecutor.info:42 - SELECT * FROM test.DATABASECHANGELOG ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC 2019-03-27 13:13:17.585 [main] [] INFO liquibase.executor.jvm.JdbcExecutor.info:42 - SELECT COUNT(*) FROM test.DATABASECHANGELOGLOCK 2019-03-27 13:13:17.678 [main] [] INFO liquibase.executor.jvm.JdbcExecutor.info:42 - CREATE TABLE `user` ( `id` int(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `age` int(10) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 2019-03-27 13:13:17.935 [main] [] INFO liquibase.changelog.ChangeSet.info:42 - Custom SQL executed 2019-03-27 13:13:17.937 [main] [] INFO liquibase.changelog.ChangeSet.info:42 - ChangeSet classpath:sample_change.sql::1::zhouyi ran successfully in 297ms 2019-03-27 13:13:17.938 [main] [] INFO liquibase.executor.jvm.JdbcExecutor.info:42 - SELECT MAX(ORDEREXECUTED) FROM test.DATABASECHANGELOG 复制代码
从数据库中可以看到变化,首次运行新增了 3 张表,
其中 DATABASECHANGELOG
和 DATABASECHANGELOGLOCK
表是 Liquibase 自动生成的,而 user
表是执行我们编写的 changeset
变更集后生成的,并且也已经执行了第 2 个变更集,插入了两条数据。
SQL 格式的 changelog
文件使用 SQL 注释作为元数据,为确保这个文件被 Liquibase 识别为 changelog
文件,,SQL 文件必须以以下注释开头:
--liquibase formatted sql 复制代码
SQL 格式的 changelog
文件中在变更的 SQL 前需要加上以下注释,表示为一个 changeset
变更集:
--changeset author:id attribute1:value1 attribute2:value2 [...] 复制代码
在之前的 sample_change.sql
文件中编写了两条 changeset
(变更集),
变更集 changeset
是通过 author + id
的方式来保证唯一性,如以上 zhouyi:1
和 zhouyi:2
两条表更集。
变更集提供以下属性:
属性 | 说明 |
---|---|
stripComments | 设置为 true 可在执行之前删除 SQL 中的任何注释, 否则为 false。如果未设置, 则默认值为 true |
splitStatements | |
endDelimiter | 应用于语句结尾的分隔符。默认为“;”,也可以设置为“” |
runAlways | 在每次运行时执行变更集, 即使之前已运行 |
runOnChange | 在首次看到更改并每次更改变更集时执行更改 |
context | 如果在运行时传递了特定上下文, 则执行更改。任何字符串都可以用于上下文名称, 并且大小写不敏感。 |
logicalFilePath | 用于在创建变更集的唯一标识符时重写文件名和路径。移动或重命名更改日志时所必需。 |
labels | 标签是对变更集进行分类的通用方法集类似上下文, 但工作方式正好相反。如果不是在运行时定义一组上下文, 然后在变更集中定义一个匹配表达式, 而是在上下文中定义一组标签, 在运行时定义一个匹配表达式。 |
runInTransaction | 变更集是否应作为单个事务运行 (如果可能),默认值为 true。请注意此属性,如果设置为 false, 并且通过运行包含多个语句的变更集部分发生错误, 则 liquibase 数据库的 databasechangeloglock 表将处于无效状态 |
failOnError | 如果在执行变更集时发生错误, 迁移是否应返回失败 |
dbms | 要用于该变更集的数据库的类型。当迁移步骤运行时, 它将根据此属性检查数据库类型,如:oracle、mysql |
logicalFilePath | 在数据库 databasechangeloglock 中设置逻辑文件路径, 而不是在执行 liquibase 的 sql 物理文件位置。 |
可以为每个变更集指定先决条件。目前, 仅支持 SQL 检查前置条件。
--preconditions onFail:HALT onError:HALT --precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM my_table 复制代码
变更集可能包括回滚变更集时要应用的语句。回滚声明也是使用表注释。
--rollback SQL STATEMENT 复制代码
例如在前面编写的 changelog 文件中的第一个 changeset:
--changeset zhouyi:1 -- 创建用户表 CREATE TABLE `user` ( `id` int(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `age` int(10) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; --rollback drop table user; 复制代码
首先的变更的 SQL 含义是新建一个 user
表,回滚的 SQL 则表示删除新建的 user
表。
下面介绍可应用于项目的一些最佳实践。
尽可能地避免对一个变更集进行多次更改,以避免自动提交 SQL 语句而可能使数据库处于非预期状态。 如 --changeset zhouyi:1
变更集,只新建一张 user
表,后面不再修改该变更集,如果需要变更,可以新增一条变更集。
选择适合您的方法。有的人是使用从 1 开始的序列号, 并且在更改日志中是唯一的,也有些人选择一个描述性的名称(例如: new-address-table
)
尽量尝试以可以回滚的方式编写变更集,如 --changeset zhouyi:1
变更集新建一个 user
表,在后面跟上回滚的 SQL, --rollback drop table user;
尽量为每一个变更集条目增加注释,如 -- 创建用户表