1. 概述

Liquibase 是一款强大的数据库管理工具,为跟踪和部署数据库变更提供了便捷方式。它还允许通过元数据(metadata)来描述变更集(changeset)在部署时的行为。

Preconditions 是一种特殊的元数据,主要用于控制变更日志(changelog)或单个变更集的执行它们确保在继续部署前满足特定要求

本文将深入探讨 Preconditions,学习如何定义条件逻辑和错误处理,以优化变更集在部署时的控制。

2. 理解 Preconditions

Preconditions 是 Liquibase 的核心功能,允许我们定义应用变更前必须满足的条件。可以把它们看作数据库变更的"守门人"。

在部署变更集前,Liquibase 会系统性地评估这些条件。如果条件不满足,Liquibase 会阻止变更集执行。根据 Preconditions 的配置,可能抛出失败、警告或直接跳过变更集。这种机制确保变更仅在数据库状态符合要求时应用,从而实现部署的严格管控,最大限度降低风险。

以下是 Preconditions 的典型应用场景:

数据库兼容性控制
限制变更仅在特定数据库类型、类型组或特定版本上执行,确保兼容性。

对象存在性检查
在创建或修改数据库对象前检查其是否已存在,避免意外错误。

⚠️ 数据安全校验
执行不可逆操作前检查现有数据。例如:当表仍包含数据时阻止删除操作(dropTable)。

数据完整性验证
变更前验证特定数据是否存在。例如:向配置表插入新行前确保该行不存在,避免重复或不一致。

这些实践能有效保障部署安全,提前规避潜在问题。

3. 定义 Preconditions 语法

Preconditions 是直接添加到变更日志文件中的标签,用于控制变更执行。下面详解其语法。

3.1. 基本语法

从简单示例开始:

<preConditions>
    <dbms type="mysql,oracle" />
</preConditions>

这里使用 dbms precondition,确保数据库是 MySQL 或 Oracle。

Liquibase 提供多种 precondition 标签满足不同需求:

标签名 功能
dbms 检查数据库类型
runningAs 验证数据库用户名
tableExists 检查表是否存在
columnExists 确保特定列存在
foreignKeyExists 检查外键是否存在
sqlCheck 执行 SQL 查询并比对预期结果

这些 preconditions 可在 XML、YAML 或 JSON 格式的变更日志中定义。在 Liquibase 4.27.0 之前,只有 sqlCheck 支持格式化 SQL。**从 4.27.0 版本 起,tableExistsviewExists 等 preconditions 也支持 SQL 变更日志**:

--precondition-table-exists table:users

这对偏好使用 SQL 变更日志的用户是重大改进。

完整可用 preconditions 列表及属性可参考官方文档

3.2. 嵌套 Preconditions

Liquibase 允许使用逻辑标签(如 <and><or><not>)嵌套或组合多个 preconditions,创建复杂条件。

示例:

<preConditions>  
    <or>  
        <and>  
            <dbms type="oracle" />  
            <runningAs username="baeldung" />  
        </and>  
        <and>  
            <dbms type="mysql" />  
            <runningAs username="baeldung" />  
        </and>  
    </or>  
</preConditions>

此例检查:数据库是 Oracle 且用户是 baeldung数据库是 MySQL 且用户是 baeldung

未指定逻辑运算符时,Liquibase 默认使用 AND 逻辑:

<preConditions>  
    <tableExists tableName="users"/>  
    <columnExists tableName="users" columnName="email"/>  
</preConditions> 

此时需同时满足:users 表存在且包含 email 列。

Liquibase 对 preconditions 采用惰性求值<and> 中首个条件失败时,后续条件不再检查;<or> 中首个条件满足时,剩余条件直接跳过。

4. 全局 vs 局部 Preconditions

Preconditions 可在两个层级使用:变更日志全局级别和变更集局部级别。

全局 Preconditions 定义在变更日志顶部(所有变更集之前),适用于所有变更集,适合通用检查(如数据库兼容性):

<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-latest.xsd">
    
    <preConditions>
        <dbms type="mysql"/>
        <runningAs username="baeldung"/>
        <sqlCheck expectedResult="9.1.0">
            SELECT @@version;
        </sqlCheck>
    </preConditions>

    <changeSet id="BAEL-1000" author="baeldung">
        <addColumn tableName="users">
            <column name="country" type="varchar(25)"/>
        </addColumn>
    </changeSet>

    <changeSet id="BAEL-1001" author="baeldung">
        <addColumn tableName="tutorials">
            <column name="code" type="varchar(5)"/>
        </addColumn>
    </changeSet>

</databaseChangeLog>

此例检查:数据库是 MySQL、部署用户是 baeldung、数据库版本为 9.1.0。任一条件不满足时,所有变更集都不会执行。

局部 Preconditions 定义在变更集顶部,仅作用于该变更集,适合针对性检查(如表/列存在性验证):

<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-latest.xsd">
                   
    <changeSet id="BAEL-1000" author="baeldung">
        <addColumn tableName="users">
            <column name="country" type="varchar(25)"/>
        </addColumn>
    </changeSet>

    <changeSet id="BAEL-1002" author="baeldung">
        <preConditions>
            <tableExists tableName="users"/>
            <columnExists tableName="users" columnName="last_visit"/>
        </preConditions>
    
        <dropColumn tableName="users" columnName="last_visit"/>
    </changeSet>

</databaseChangeLog>

此 precondition 仅作用于 BAEL-1002 变更集,确保 users 表和 last_visit 列存在后才允许删除列。

5. 处理 Preconditions 失败与错误

Liquibase 区分 precondition 的失败(failure)和错误(error):

  • 失败:precondition 成功执行但未满足预期条件
  • 错误:技术问题(如语法错误)导致 precondition 无法执行

通过 onFailonError 属性指定失败/错误时的处理方式。

5.1. onFailonError 属性

示例变更集:

<changeSet id="BAEL-1003" author="baeldung">
    <preConditions onFail="HALT" onError="HALT">
        <not>
            <columnExists tableName="users" columnName="verified"/>
        </not>
    </preConditions>
    
    <addColumn tableName="users">
        <column name="verified" type="boolean" defaultValue="false"/>
    </addColumn>
</changeSet>

通过 mvn liquibase:update 执行。

verified 列已存在,precondition 失败并输出:

Not precondition failed

users 表不存在,则发生错误:

liquibase.exception.DatabaseException: Table 'baeldung_liquibase.users' doesn't exist

两种情况下的 HALT 值都会立即停止执行。

5.2. onFailonError 的可选值

可配置的行为选项:

行为 适用场景
HALT 立即停止变更日志执行 默认行为
WARN 记录警告但继续执行 非关键性检查
MARK_RAN 标记变更集为已执行(写入 DATABASECHANGELOG 表)并继续 需跳过但记录执行状态
CONTINUE 跳过当前变更集并继续执行后续 局部失败不影响全局

全局与局部 Preconditions 的属性支持差异

属性 全局 Preconditions 局部 Preconditions
onFail HALT, WARN HALT, CONTINUE, MARK_RAN, WARN
onError HALT, WARN HALT, CONTINUE, MARK_RAN, WARN

可通过 onErrorMessageonFailMessage 自定义错误/失败消息:

<preConditions onFail="WARN" onFailMessage="Column verified already exists">
    <not>
        <columnExists tableName="users" columnName="verified"/>
    </not>
</preConditions>

这些属性提供了灵活的 precondition 处理机制。

6. 总结

本文深入探讨了 Liquibase Preconditions 如何提升数据库更新的控制精度。它们确保在应用变更前数据库满足特定条件,有效降低风险并避免潜在错误。

通过合理运用 Preconditions,我们能够:

  • 简化部署流程
  • 保障数据库完整性
  • 实现条件化变更执行
  • 增强跨环境兼容性

掌握 Preconditions 是高效管理数据库变更的关键技能,尤其适合复杂生产环境下的数据库版本控制。