Liquibase在项目中的应用实践

项目需求

某项目由于历史原因,没有数据库版本控制功能,开发人员在添加或者修改表结构等操作时,需要手动归档sql,手动执行sql,效率低下且容易出错,最后,无数据库版本控制会导致产品上线后难以维护。基于以上原因,需要给该项目引入数据库版本控制功能。

技术选型

市面上有很多的数据库版本控制工具,通过筛选留下了三种开源的数据库版本管理工具作为候选:

  • flyway
  • liquibase
  • dolt

flyway很久之前在其他项目有使用过,这次重新进行了调研对比,通过对比最终选择了liquibase。主要原因如下:

  • dolt 功能不太符合我们的项目场景,它的思路是git的管理思路,最先pass掉
  • flyway的开源功能不支持版本回退和基于基线的版本控制,必须商业版本才能支持,而liquibase是支持的,这点比较重要,能用免费的开源技术实现项目的商业价值就绝对不用付费的技术
  • liquibase本身功能强大,支持主流数据库,支持sql/xml/json/yaml等多种配置方式,一次配置,多种数据库均可使用,官方文档也非常详细

liquibase的基本概念

Changelog Changeset

sql-changelog示例
xml-changelog示例

上述图片来自官方文档,展示了sql格式与xml格式的Changelog。一个Changelog可以包含多个Changeset,一个Changelog也可以同时包含其他的Changelog,一个Changeset表示一次数据变更,一个Changeset可以包含多个变更语句。由于本文主要讲解工程实践过程,详细的概念可以参考官方文档的基本概念

实战操作

由于该项目之前一直使用sql维护schema,所以自然地采取了sql格式的Changelog进行数据库的版本控制,如果是新建项目,建议使用其他格式(xml/json/yaml)进行,因为那样可以方便的迁移到各种数据库中,使用sql就跟数据库产生了绑定关系。同时,使用sql还需要自己写回退部分的配置,而其他格式支持自动生成回退部分的代码,在大多数情况无需自己编写回退部分的配置。

版本控制的集成方案选择

liquibase本身是使用java编写的,我们需要改造的项目也是java项目,候选了两种方式作为liquibase的集成方案:一种是直接引用官方提供的jar包,使用java直接调用liquibase的api进行数据库的版本控制;第二种是使用官方编译好的版本,编写脚本进行数据库的版本控制。最终通过权衡选择了第二种方式,原因如下:

  • 直接调用liquibase的api需要维护单独的工程代码,需要多创建一个代码仓库
  • 直接调用liquibase的api需要自己编写代码,虽然自由度大,但是工作量相对会增加
  • 官方编译好的版本,升级只需要升级编译好的软件制品即可,较为方便,另外,编写脚本工作量不大
  • 功能够用即可,即使将来遇到更复杂的版本控制场景,转换成第一种方案的代价也并不高

工程化方案

  1. 确定版本控制的目录结构
  2. 确定功能,编写功能脚本
  3. 实现虚拟化部署
  4. 实现自动化部署(CI/CD)

项目中的migration目录结构

migration目录结构

由于隐私问题,项目目录中隐去了具体代码部分,只剩余数据库版本控制部分的migration目录,所有跟数据库版本控制相关的文件均放在migration目录下。

  • db:Changelog归档
  • liquibase:官方release版本的软件制品,直接下载解压即可拥有,示例中为4.6.1版本
  • Dockerfile:docker镜像的构建脚本
  • sh脚本文件:支持升级与回退的脚本文件

下面详细介绍各个文件目录的功能。

db目录

db目录中混合了两种Changelog,sql格式与yaml格式,原因是因为官方支持的四种格式中只有sql格式不能包含其他Changelog,这块选择了yaml作为Changelog的入口。
001_create_tables_baseline.sql的Changelog示例,这块需要完全遵循官方规范,sql changelog开头必须是--liquibase formatted sql,每个changeset的写法为开头--changeset techgeeknext:create-tables,其中techgeeknext为作者名称,create-tables是changeset的id,changeset的id必须全局唯一,官方建议使用liquibase的项目组应该提前定好changeset的id命名规范,项目维护时每个人遵守规范即可。--rollback放在每个changeset的最后,用于编写回退规则。这块的sql使用的是官方示例。

--liquibase formatted sql
--changeset techgeeknext:create-tables

CREATE TABLE employee(
                         id INT PRIMARY KEY,
                         name VARCHAR(40)
);

CREATE TABLE branch(
                       id INT PRIMARY KEY,
                       name VARCHAR(40),
                       emp_id INT,
                       FOREIGN KEY(emp_id) REFERENCES employee(id) ON DELETE CASCADE
);
--rollback drop table if exists employee,branch;

baseline.yaml,这块建立的baseline.yaml是yaml格式的changelog,它包含了一个changeset和两个changelog,这块的changeset什么都没有实现,所以是一个空的changeset,不会对数据库造成任何影响,但是升级时会产生一条记录。这块的baseline.yaml的意义在于,该项目此前已经有相关在线环境,数据库实际上是有内容的,并不是全量更新,而是在已有的基线上增量更新,所以这块我创建了baseline.yaml,用来归档已经在数据库中的存在的sql内容。

databaseChangeLog:

  - changeSet:
      id: 1
      author: xkadmin

  - include:
      file: DDL/001_create_tables_baseline.sql
      relativeToChangelogFile: true

  - include:
      file: DML/001_insert_data_baseline.sql
      relativeToChangelogFile: true

migration.yaml包含的是基线之外,后续增量更新的内容:

databaseChangeLog:

  - include:
      file: DML/002_insert_data.sql
      relativeToChangelogFile: true

db.changelog-all.yaml包含的是baseline.yaml和migration.yaml,当全新环境安装需要全量sql更新的场景,那么直接调用db.changelog-all.yaml进行更新。

databaseChangeLog:

  - include:
      file: baseline/baseline.yaml
      relativeToChangelogFile: true

  - include:
      file: migration/migration.yaml
      relativeToChangelogFile: true

sh脚本

migration_on_baseline.sh
#!/usr/bin/env bash
echo "baseline migration start."
. /migration/var.sh
username=$1
password=$2
url=$3
baselineFile=db/changelogs/baseline/baseline.yaml
migrationFile=db/changelogs/migration/migration.yaml

echo "/migration/liquibase/liquibase  --changeLogFile=$baselineFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password changelogSync"
/migration/liquibase/liquibase  --changeLogFile=$baselineFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password changelogSync

echo "/migration/liquibase/liquibase  --changeLogFile=$migrationFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password update"
/migration/liquibase/liquibase  --changeLogFile=$migrationFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password update

echo "baseline migration finish."

该脚本用于已安装环境的增量更新,首先针对baseline.yaml使用changelogSync进行基线确定,实际上liquibase在执行这条命令时就是单纯在数据库中把baseline.yaml指定的changeset刷到记录表中,但是并不是执行sql,基线确认后,再使用update命令将migration.yaml中的changeset中的sql在数据库中进行执行,执行结束后,基于基线的更新就结束了,这种方式适用于已有环境的更新,本质就是基线sql不执行,只执行后续增量更新的sql。该功能在flyway中是收费功能,而liquibase是免费的,这也是选择liquibase的重要原因。

migration_all.sh
#!/usr/bin/env bash
. /migration/var.sh
username=$1
password=$2
url=$3
changeLogFile=db/changelogs/db.changelog-all.yaml

echo "/migration/liquibase/liquibase  --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password update"

/migration/liquibase/liquibase  --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password update

该脚本用于全量更新的场景,即全新安装环境,本质就是所有changelog的changeset中的sql都会在数据库中进行执行。db.changelog-all.yaml包含了系统从开始开发到至今的所有changelog。

rollback_count.sh
#!/usr/bin/env bash
. /migration/var.sh
username=$1
password=$2
url=$3
count=$4
changeLogFile=db/changelogs/db.changelog-all.yaml

echo "/migration/liquibase/liquibase  --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password rollbackCount $count"

/migration/liquibase/liquibase  --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password rollbackCount $count

回退脚本,这个脚本可以执行回退,通过传入count数量,可以实现基于changeset个数的回退,这个在后文可以观察实际执行结果去理解。

rollback_tag.sh
#!/usr/bin/env bash
. /migration/var.sh
username=$1
password=$2
url=$3
tag=$4
changeLogFile=db/changelogs/db.changelog-all.yaml

echo "/migration/liquibase/liquibase  --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password rollback $tag"

/migration/liquibase/liquibase  --changeLogFile=$changeLogFile --log-level=$logLevel --log-file=$logFile \
  --hub-mode=off --username=$username --url=$url --password=$password rollback $tag

基于tag标记的回退,这个很好理解,使用该命令传入tag,可以直接回退到标记位置的数据库版本。

tag.sh
#!/usr/bin/env bash
username=$1
password=$2
url=$3

md5=`cat md5`
echo "/migration/liquibase/liquibase --username=$username --password=$password --url=$url tag $md5"
/migration/liquibase/liquibase --username=$username --password=$password --url=$url tag $md5

用于打tag的脚本,在项目中的tag使用了db目录下面的所有文件和的md5值作为tag,这样保证了tag的唯一性。

var.sh
#!/usr/bin/env sh
logLevel=INFO
logFile=/var/logs/liquibase/liquibase.log

公共变量存放脚本。

migration_entrypoint.sh
#!/usr/bin/env bash
username=$1
password=$2
url=$3
type=$4
countOrTag=$5
echo url=$url
if [ $type == 0 ]
then
  /migration/migration_on_baseline.sh $username $password $url && /migration/tag.sh $username $password $url
elif [ $type == 1 ]
then
  /migration/migration_all.sh $username $password $url && /migration/tag.sh $username $password $url
elif [ $type == 2 ]
then
  /migration/rollback_count.sh $username $password $url $countOrTag
elif [ $type == 3 ]
then
  /migration/rollback_tag.sh $username $password $url $countOrTag
fi

全部程序入口,通过type判断到底是全量升级、增量升级、基于tag回退还是基于changeset数量的回退。migration脚本执行成功后会自动打上当前升级的tag。

Dockerfile

FROM www.xk.docker-registry.com:5501/openjdk:8u302-jre

MAINTAINER xk

WORKDIR /migration

ADD db db

ADD md5 md5

ADD liquibase liquibase

COPY *.sh /migration/

RUN chmod -R 777 /migration

VOLUME /var/logs/liquibase

RUN echo 'Asia/Shanghai' > /etc/timezone

RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

ENTRYPOINT ["/migration/migration_entrypoint.sh"]

数据库版本控制功能docker化,打包所有相关内容到镜像中。

自动化版本控制

该项目存储在gitlab上,使用gitlab cicd来实现项目的自动化部署,那么版本控制也自然使用gitlab内置的cicd功能,具体方法是编写.gitlab-ci.yml。

stages:
  - migration

docker build:
  stage: migration
  tags:
    - compile
  script:
    - find migration/db/ -type f
    - find migration/db/ -type f | xargs cat > migration/combine_file.txt && md5sum migration/combine_file.txt | awk '{print $1}' > migration/md5
    - cat migration/md5
    - docker build -f migration/Dockerfile -t www.xk.docker-registry.com:5501/migration:`cat migration/md5` migration/
    - docker push www.xk.docker-registry.com:5501/migration:`cat migration/md5`
    - docker rm -f migration || echo "No running migration task"
    - echo "docker run --net=host -v /var/logs/liquibase:/var/logs/liquibase --name migration www.xk.docker-registry.com:5501/migration:`cat migration/md5` $MIGRATION_USER $MIGRATION_PASSWORD $MIGRATION_URL 1"
    - docker run --net=host -v /var/logs/liquibase:/var/logs/liquibase --name migration www.xk.docker-registry.com:5501/migration:`cat migration/md5` $MIGRATION_USER $MIGRATION_PASSWORD $MIGRATION_URL 1

该过程主要包含三个步骤:

  1. 生成db目录下文件和的md5值,保证本次tag的唯一性,同时docker镜像的tag也使用该值,有利于后期项目维护
  2. 构建docker镜像并推送至私有仓库
  3. 由于本文是示例环境,执行docker run进行环境的全量更新

实际效果

示例环境变量配置如下:

export MIGRATION_PASSWORD="xxxxxx"
export MIGRATION_URL="jdbc:mysql://127.0.0.1:3361/liquibase_test?createDatabaseIfNotExist=true&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
export MIGRATION_USER=xxxxxx

将所有代码推送至仓库,观察结果,这块示例使用的mysql数据库。

版本升级

示例中版本升级是自动化全量升级,最终调用的是migration_all.sh脚本。查看gitlab的cicd流水线:

gitlab流水线

流水线在32秒内进行编译并执行成功,查看详细的Job详情:


Job详情

查看Mysql数据库的升级情况:


升级结果

新建了branch和employee两张数据库表,同时创建了liquibase的版本信息表databasechangelog和databasechangeloglock。

branch

employee

databasechangelog

liquibase在databasechangelog中记录了具体的版本信息,liquibase依赖这张表对数据库的版本进行控制,表中内容包含了changeset的id、作者、文件名、执行时间、执行顺序以及tag等信息,可以看到前面介绍的的那个空的changeset在其中的描述信息就是empty,表示这个changeset什么都没干。

现在修改002_insert_data.sql,changelog在inset-branch-01的基础上新增一个changeset:insert-branch-employee-02:

--liquibase formatted sql
--changeset techgeeknext:inset-branch-01
INSERT INTO branch VALUES(1, 'User1',01);
INSERT INTO branch VALUES(2, 'User2',02);
INSERT INTO branch VALUES(3, 'User3',03);
INSERT INTO branch VALUES(4, 'User4',04);
--rollback DELETE FROM branch WHERE id in (1,2,3,4);

--changeset techgeeknext:insert-branch-employee-02
INSERT INTO employee VALUES(5, 'User5');
INSERT INTO branch VALUES(5, 'User4',05);
--rollback DELETE FROM branch WHERE id in (5);DELETE FROM employee WHERE id in (5);

提交代码,推送至代码库,查看自动化部署的结果。


升级成功

查看数据,已经更新,databasechangelog中也新增了一条changeset记录:


branch

employee

databasechangelog

版本回退

版本回退需要手动在后台执行docker命令。

基于tag的回退

现在存在两个tag标记, 前面的tag为84740b811c87a7c3fba5193f151a9c82,按照这个tag在服务器上执行回退命令:

docker run --net=host -v /var/logs/liquibase:/var/logs/liquibase --name migration www.xk.docker-registry.com:5501/migration:c81cd234dd5a956f0f6826728f1bd23b $MIGRATION_USER $MIGRATION_PASSWORD $MIGRATION_URL 3 84740b811c87a7c3fba5193f151a9c82

脚本执行结果:

####################################################
##   _     _             _ _                      ##
##  | |   (_)           (_) |                     ##
##  | |    _  __ _ _   _ _| |__   __ _ ___  ___   ##
##  | |   | |/ _` | | | | | '_ \ / _` / __|/ _ \  ##
##  | |___| | (_| | |_| | | |_) | (_| \__ \  __/  ##
##  \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___|  ##
##              | |                               ##
##              |_|                               ##
##                                                ## 
##  Get documentation at docs.liquibase.com       ##
##  Get certified courses at learn.liquibase.com  ## 
##  Free schema change activity reports at        ##
##      https://hub.liquibase.com                 ##
##                                                ##
####################################################
Starting Liquibase at 10:57:58 (version 4.6.1 #98 built at 2021-11-04 20:16+0000)
Liquibase Version: 4.6.1
Liquibase Community 4.6.1 by Liquibase
Rolling Back Changeset:db/changelogs/migration/DML/002_insert_data.sql::insert-branch-employee-02::techgeeknext
Logs saved to /var/logs/liquibase/liquibase.log
Liquibase command 'rollback' was executed successfully.

查看数据,已经回退到指定tag为84740b811c87a7c3fba5193f151a9c82的版本:


branch

employee

databasechangelog
基于changeset个数的回退

命令的最后参数表示回退的changeset个数为2,服务器上执行该命令。

docker run --net=host -v /var/logs/liquibase:/var/logs/liquibase --name migration www.xk.docker-registry.com:5501/migration:c81cd234dd5a956f0f6826728f1bd23b $MIGRATION_USER $MIGRATION_PASSWORD $MIGRATION_URL 2 2

命令执行结果:

####################################################
##   _     _             _ _                      ##
##  | |   (_)           (_) |                     ##
##  | |    _  __ _ _   _ _| |__   __ _ ___  ___   ##
##  | |   | |/ _` | | | | | '_ \ / _` / __|/ _ \  ##
##  | |___| | (_| | |_| | | |_) | (_| \__ \  __/  ##
##  \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___|  ##
##              | |                               ##
##              |_|                               ##
##                                                ## 
##  Get documentation at docs.liquibase.com       ##
##  Get certified courses at learn.liquibase.com  ## 
##  Free schema change activity reports at        ##
##      https://hub.liquibase.com                 ##
##                                                ##
####################################################
Starting Liquibase at 11:08:14 (version 4.6.1 #98 built at 2021-11-04 20:16+0000)
Liquibase Version: 4.6.1
Liquibase Community 4.6.1 by Liquibase
Rolling Back Changeset:db/changelogs/migration/DML/002_insert_data.sql::inset-branch-01::techgeeknext
Rolling Back Changeset:db/changelogs/baseline/DML/001_insert_data_baseline.sql::inset-employee-01::techgeeknext
Logs saved to /var/logs/liquibase/liquibase.log
Liquibase command 'rollbackCount' was executed successfully.

查看数据,branch与employee数据均为空,databasechangelog减少了两条changeset记录,表示已经回退成功:


branch

employee

databasechangelog

总结

本文主要介绍了liquibase在实际项目中的应用实践,liquibase本身的功能比较强大,本文仅使用了其中非常小的一部分,满足了我们的项目需求,更多功能可以在liquibase官网进行探索。同时,本文使用了自动化流水线的方式引入liquibase进行数据库的版本控制,开发人员仅需要添加更新的sql脚本,推送代码后gitlab的流水线会自动触发数据库版本的升级,无需人工干预,避免了低级错误。

参考资料

liquibase官方文档
如何在已有项目中引入liquibase并进行多环境的增量更新

示例代码地址

https://github.com/jy03187487/liquibase_example

需要注意的问题

  • 具体数据库的依赖需要自行下载放置到liquibase/lib目录下
  • .gitlab-ci.yml使用的环境变量需要提前配置在系统中,该项目直接配置到了/etc/profile中,保险起见,配置完成后请重启gitlab-runner确认环境变量已经生效
  • find migration/db/ -type f | sort 添加sort是为了保证文件顺序一致,因为发现在不同操作系统中执行find migration/db/ -type f 顺序不一定一致

转载说明

码字不易,如需转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容