变更集依赖基础:dependsOn属性的作用、语法格式、前置变更集与后置变更集的关系

在Liquibase的变更集编排体系中,dependsOn是控制执行顺序的核心属性。它显式声明一个变更集必须在另一个(或多个)变更集成功应用之后才能执行,从而在数据库对象之间建立严格的依赖契约。理解dependsOn的语义与用法,是构建可靠、可重复的数据库迁移流水线的第一步。

2.1 dependsOn属性的作用

dependsOn解决的核心问题是:当变更集之间的依赖关系无法通过文件顺序或runOrder隐式表达时,如何强制指定执行先后顺序。典型场景包括:

  • 对象引用依赖:先建表A,再建引用表A的外键约束;先创建存储过程,再创建调用该过程的触发器。
  • 数据依赖:先插入基础字典数据,再插入业务数据;先执行数据清洗脚本,再执行数据聚合脚本。
  • 环境准备依赖:先创建表空间/文件组,再在该表空间上建表;先创建用户,再授予权限。
  • 回滚顺序保障:通过依赖关系隐式定义回滚时的逆序执行路径。
关键认知: dependsOn 不是“建议性排序”,而是“强制性前置条件”。如果前置变更集失败或未执行,后置变更集将直接跳过(SKIP),不会尝试执行。

2.2 语法格式与使用位置

dependsOn属性定义在变更集的XML根元素<changeSet>上,通过dependsOn子元素指定一个或多个前置变更集。每个前置变更集通过changeSetId和changeSetAuthor两个属性唯一标识。

2.2.1 基本语法

<changeSet id="2" author="zhang">
    <dependsOn>
        <changeSet id="1" author="zhang"/>
    </dependsOn>
    <createTable tableName="orders">
        <column name="id" type="int">
            <constraints primaryKey="true"/>
        </column>
        <column name="customer_id" type="int"/>
    </createTable>
</changeSet>

2.2.2 多依赖语法

一个变更集可以依赖多个前置变更集,此时所有前置变更集都必须成功应用,后置变更集才能执行:

<changeSet id="3" author="li">
    <dependsOn>
        <changeSet id="1" author="zhang"/>
        <changeSet id="2" author="zhang"/>
    </dependsOn>
    <addForeignKeyConstraint 
        baseTableName="orders" 
        baseColumnNames="customer_id"
        referencedTableName="customers" 
        referencedColumnNames="id"
        constraintName="fk_orders_customers"/>
</changeSet>

2.2.3 跨文件依赖

dependsOn支持跨变更日志文件的依赖,只需在<changeSet>上额外指定filePath属性(可选),Liquibase会自动解析文件路径:

<changeSet id="4" author="wang" filePath="changelogs/v2/seed-data.xml">
    <dependsOn>
        <changeSet id="1" author="zhang" filePath="changelogs/v1/schema.xml"/>
    </dependsOn>
    <insert tableName="customers">
        <column name="id" valueNumeric="1"/>
        <column name="name" value="Acme Corp"/>
    </insert>
</changeSet>
注意: 跨文件依赖时,filePath 必须与 Liquibase 解析后的实际路径一致。建议使用相对于根变更日志的路径,避免因工作目录不同导致依赖解析失败。

2.3 前置变更集与后置变更集的关系

理解前置(Predecessor)与后置(Successor)变更集的关系,是正确使用dependsOn的前提。下表从多个维度对比两者的差异:

对比维度 前置变更集(Predecessor) 后置变更集(Successor)
定义位置 被 <dependsOn> 引用的变更集 包含 <dependsOn> 声明的变更集
执行顺序 必须先执行成功 在前置变更集执行成功后执行
失败影响 失败时,后置变更集被跳过 前置失败时,自身不执行
回滚顺序 在后置变更集回滚之后回滚 在前置变更集回滚之前回滚
依赖方向 被依赖方(被动) 依赖方(主动)
数量关系 一个前置可被多个后置依赖 一个后置可依赖多个前置

2.4 依赖关系执行流程

下图展示了Liquibase在运行时如何解析dependsOn依赖并决定变更集的执行状态:

dependsOn 依赖解析与执行流程 变更集执行队列 检查当前变更集是否有 dependsOn 有依赖? (dependsOn 非空) 无 直接执行 有 检查前置状态 前置已执行 且成功? 是 执行当前 否 跳过当前变更集 前置失败 → 后置跳过(SKIP) 前置未执行 → 后置跳过(SKIP)

2.5 依赖关系的最佳实践

  • 显式优于隐式:即使变更集在文件中的顺序已经满足依赖,也建议显式声明dependsOn,提高可读性和可维护性。
  • 避免循环依赖:Liquibase不会检测循环依赖,但循环依赖会导致变更集永远无法执行。设计时应确保依赖图是有向无环图(DAG)。
  • 最小依赖原则:只声明直接依赖,不要声明传递依赖。例如A→B→C,C只需依赖B,无需依赖A。
  • 使用filePath明确跨文件依赖:跨变更日志文件时,始终指定filePath属性,避免路径歧义。
  • 依赖与回滚配合:确保依赖链上的每个变更集都有对应的回滚脚本,否则回滚时可能因缺少回滚操作导致数据库状态不一致。
冒烟验证提示: 在测试环境中,可以通过 liquibase updateSQL --changeset="id,author" 查看依赖解析后的执行顺序。如果发现后置变更集被跳过,优先检查前置变更集的执行状态和 dependsOn 的引用是否正确。