使用 Liquibase 实现数据库 Schema 的版本控制

2022-12-14, 星期三, 22:42

DevOpsDevCI/CD培训

Liquibase is a database schema change management solution that enables you to revise and release database changes faster and safer from development to production.

快速开始

使用 Spring Initializr 创建 Spring Boot 项目时勾选 Liquibase,或在 pom.xml 中手动添加 org.liquibase:liquibase-core 依赖。

如果使用 Spring Boot 作为基底,可以在 org.springframework.boot:spring-boot-dependencies 中看到 Spring 指定了一个 <liquibase.version>,因此开发者声明依赖的时候不需要指定版本号。

Liquibase 的配置可在 application.yml 中进行:

spring:
  liquibase:
    enabled: true
    change-log: classpath:db/changelog/changelog.mysql.sql

也可以通过 SpringLiquibase Bean 使用 Java 代码配置。使用 Java 配置类的优点有:

  • 项目拆分成多个 Module 时可以在核心模块中实现功能开关和特性约束
  • 通过继承 liquibase.database.AbstractJdbcDatabase 编写自定义类,可以支持达梦、人大金仓等国产数据库
  • 可以实现更细粒度的权限控制
  • 可以实现自定义机制控制和文件规划方案

resources/ 目录下添加数据库变更记录,一种常见的模式是创建一个主 Changelog,然后包含其他变更文件。

src/main/resources
├── application.yml
└── db
    ├── changelog
    │   └── changelog-1.0.0.mysql.sql
    └── changelog.xml

Changelog 可以使用 SQL/XML/YAML/JSON 编写,以 Changeset 为单位记录变更。

<?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.1.xsd">
    <include file="changelog/changelog-1.0.0.mysql.sql" relativeToChangelogFile="true"/>
</databaseChangeLog>

一个 Changeset 一般由如下部分组成:

  • -- changeset author:id
  • -- comment 注释
  • 数据库变更语句
  • -- rollback 修饰的回滚语句
-- liquibase formatted sql

-- changeset yufan:202212132050
-- comment 建立 post 表
create table tbl_post
(
    id          bigint       not null,
    title       varchar(255) not null,
    create_time datetime     not null,
    constraint pk_tbl_post primary key (id)
);
-- rollback drop table tbl_post;

-- changeset yufan:202212132051
-- comment 为 post 表添加数据,但是不推荐在 changeset 中做这种添加初始化数据的工作
insert into tbl_post (id, title, create_time)
values  (1, '天文学家确认至今观测到的最古老星系', '2019-01-01 00:00:00'),
        (2, '恐惧的记忆如何长期保存下来', '2020-01-01 00:00:00'),
        (3, 'DeepMind 的 AlphaCode 在编程竞赛中达到人类水平', '2021-01-01 00:00:00'),
        (4, 'Linux 6.1 释出', '2022-01-01 00:00:00');
-- rollback delete from tbl_post where id in (1,2,3,4);

-- changeset yufan:202212132052
-- comment 修改 post 表添加 deleted 列,在 rollback 中删除该列
alter table tbl_post add column deleted bit(1) not null default b'0';
-- rollback alter table tbl_post drop column deleted;

-- changeset yufan:202212132053
-- comment 添加 user 表
create table tbl_user
(
    id          bigint       not null,
    name        varchar(255) not null,
    constraint pk_tbl_user primary key (id)
);
-- rollback drop table tbl_user;

创建一张表的回滚操作显然是 drop table,删除一张表就没那么容易了,因为表结构信息已经永远丢失了,因此需要在 rollback 指令中编写创建语句。

项目启动时会检查数据库中的 DATABASECHANGELOG 表,如果该表不存在则自动创建。Liquibase 通过检查该表中条目的 IDAUTHORFILENAME 字段判断一个 Changeset 是否被应用过并执行相应的操作。

多个应用同时修改数据库时的锁机制通过 DATABASECHANGELOGLOCK 表实现。

使用 Maven Plugin 的一点小问题

Liquibase 同样提供了 Maven Plugin:

<plugin>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-maven-plugin</artifactId>
    <version>4.17.2</version>
    <configuration>
        <propertyFile>src/main/resources/liquibase-local.properties</propertyFile>
    </configuration>
</plugin>

不过 Maven Plugin 中配置的 changelogFilesrc 开始的相对路径,使用 mvn liquibase:update 更新 Schema 后,DATABASECHANGELOG 表中记录的路径是 src/main/resources/db/changelog/changelog-1.0.0.mysql.sql。启动应用后,liquibase.changelog.filter.ShouldRunChangeSetFilter#changeSetsMatch 方法在比较 Changeset 时会认为 path 不匹配,从而误判为未执行。

CLI 工具

Liquibase 也提供了适用于 Windows / macOS / Linux 的 CLI 工具,可以实现一些更复杂的功能:

  • 为现有 Schema 生成初始化 Changesets
  • 把一些 Changesets 标记为已完成
  • 回滚
  • ……

碰到了与 Maven Plugin 一样的路径问题,可以通过编写 liquibase-local.sh 脚本切换工作目录的方式调整 ChanglogFile 的相对路径。

#!/bin/zsh
# This script is used to run liquibase migrations on the local machine.
WORK_DIR='src/main/resources'
CHANGELOG_FILE='db/changelog.xml'
DB_JAR="$HOME/.m2/repository/com/mysql/mysql-connector-j/8.0.31/mysql-connector-j-8.0.31.jar"
DB_URL='jdbc:mysql://localhost:3306/showcase'

cd $WORK_DIR
case $1 {
  (update)
  liquibase --hub-mode=off \
            --driver=com.mysql.cj.jdbc.Driver \
            --classpath=$DB_JAR \
            --changelogFile=$CHANGELOG_FILE \
            --url=$DB_URL \
            --username=$DB_USERNAME \
            --password=$DB_PASSWORD \
            $1
  ;;
  (rollback-count)
  liquibase rollback-count \
            --driver=com.mysql.cj.jdbc.Driver \
            --classpath=$DB_JAR \
            --changelogFile=$CHANGELOG_FILE \
            --url=$DB_URL \
            --username=$DB_USERNAME \
            --password=$DB_PASSWORD \
            --count=$2
  ;;
}

使用脚本执行 update 指令,产生的 DATABASECHANGELOG.FILENAMEdb/changelog/changelog-1.0.0.mysql.sql,与 classpath:db/changelog/changelog-1.0.0.mysql.sql 匹配。再启动项目时 Liquibase 能成功识别这些是已生效的 Changesets。

执行 ./liquibase-local.sh rollback-count 1 则会回滚一个 Changeset,在本例中就是删除 tbl_user 表。