美文网首页
Liquibase在项目中的应用实践

Liquibase在项目中的应用实践

作者: 小鲍比大爷 | 来源:发表于2021-11-30 16:53 被阅读0次

    项目需求

    某项目由于历史原因,没有数据库版本控制功能,开发人员在添加或者修改表结构等操作时,需要手动归档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 顺序不一定一致

    转载说明

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

    相关文章

      网友评论

          本文标题:Liquibase在项目中的应用实践

          本文链接:https://www.haomeiwen.com/subject/fjmdxrtx.html