4. ChangeLog 文件结构:四种格式对比与组织策略

ChangeLog 是 Liquibase 迁移的核心清单文件,它定义了数据库变更的顺序、作者、ID 以及变更集之间的依赖关系。Liquibase 原生支持 XML、YAML、JSON 和 SQL 四种格式,每种格式在可读性、类型安全、版本协作和动态参数化方面各有优劣。本章将系统对比四种格式的根元素、语法结构,并深入讲解 include 与 includeAll 的模块化组织策略。

4.1 四种格式的根元素与核心结构对比

无论使用哪种格式,ChangeLog 都必须包含一个根元素(或等效的顶层结构),用于声明命名空间、数据库变更逻辑(databaseChangeLog)以及版本信息。下表从根元素、变更集定义方式、类型检查、动态参数支持等维度进行对比:

对比维度 XML YAML JSON SQL
根元素 <databaseChangeLog> databaseChangeLog: { "databaseChangeLog": [...] } 无根元素,纯 SQL 语句 + Liquibase 注释指令
变更集定义 <changeSet id="1" author="dev"> - changeSet: { id: 1, author: dev } { "changeSet": { "id": 1, "author": "dev" } } -- liquibase formatted sql
-- changeset dev:1
类型检查 强类型(XSD 校验) 弱类型(缩进敏感) 弱类型(引号敏感) 无类型检查
动态参数 支持 ${property} 支持 ${property} 支持 ${property} 支持 ${property}
IDE 支持 优秀(自动补全、校验) 良好(部分 IDE 支持) 良好(JSON Schema) 一般(纯文本)
可读性 中等(标签冗余) 高(缩进简洁) 中等(括号较多) 高(SQL 原生)
适用场景 企业级、复杂变更、需严格校验 快速原型、DevOps 流水线 API 驱动、前端集成 DBA 主导、已有 SQL 脚本迁移
选型建议: 团队以开发人员为主且追求 IDE 校验时,优先选择 XML;团队以 DBA 为主且已有大量 SQL 脚本时,选择 SQL 格式;追求简洁和 DevOps 自动化时,YAML 是最佳平衡点。

4.2 四种格式的完整示例

以下分别展示四种格式下创建 person 表的等价 ChangeLog 片段:

XML 格式

<?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-4.20.xsd">

    <changeSet id="1" author="dev">
        <createTable tableName="person">
            <column name="id" type="int" autoIncrement="true">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="name" type="varchar(100)">
                <constraints nullable="false"/>
            </column>
        </createTable>
    </changeSet>
</databaseChangeLog>

YAML 格式

databaseChangeLog:
  - changeSet:
      id: 1
      author: dev
      changes:
        - createTable:
            tableName: person
            columns:
              - column:
                  name: id
                  type: int
                  autoIncrement: true
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: name
                  type: varchar(100)
                  constraints:
                    nullable: false

JSON 格式

{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "1",
        "author": "dev",
        "changes": [
          {
            "createTable": {
              "tableName": "person",
              "columns": [
                {
                  "column": {
                    "name": "id",
                    "type": "int",
                    "autoIncrement": true,
                    "constraints": {
                      "primaryKey": true,
                      "nullable": false
                    }
                  }
                },
                {
                  "column": {
                    "name": "name",
                    "type": "varchar(100)",
                    "constraints": {
                      "nullable": false
                    }
                  }
                }
              ]
            }
          }
        ]
      }
    }
  ]
}

SQL 格式

-- liquibase formatted sql

-- changeset dev:1
CREATE TABLE person (
    id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,
    name VARCHAR(100) NOT NULL
);
-- rollback DROP TABLE person;
SQL 格式注意事项: 文件第一行必须是 -- liquibase formatted sql;每个变更集以 -- changeset author:id 开头;回滚语句以 -- rollback 开头。SQL 格式不支持 Liquibase 的 refactoring 标签(如 addColumn),必须手写 DDL。

4.3 include 与 includeAll:模块化组织策略

随着项目演进,单个 ChangeLog 文件会变得臃肿且难以维护。Liquibase 提供了 include 和 includeAll 两种指令,用于将多个 ChangeLog 文件组合成一个逻辑整体。

4.3.1 include:精确引用单个文件

include 用于显式引用一个子 ChangeLog 文件。支持相对路径、绝对路径以及 classpath 路径。常用属性如下:

属性 说明 示例
file 子 ChangeLog 文件路径(必填) file="changelogs/v1.0/create-tables.xml"
relativeToChangelogFile 路径是否相对于当前 ChangeLog 文件(默认 false) relativeToChangelogFile="true"
context 按上下文过滤(如 dev、prod) context="dev"
labels 按标签过滤 labels="release-1.0"

XML 示例:

<databaseChangeLog>
    <include file="changelogs/v1.0/create-tables.xml"
             relativeToChangelogFile="true"/>
    <include file="changelogs/v1.0/seed-data.sql"
             relativeToChangelogFile="true"
             context="dev"/>
</databaseChangeLog>

YAML 示例:

databaseChangeLog:
  - include:
      file: changelogs/v1.0/create-tables.xml
      relativeToChangelogFile: true
  - include:
      file: changelogs/v1.0/seed-data.sql
      relativeToChangelogFile: true
      context: dev

4.3.2 includeAll:批量扫描目录

includeAll 用于扫描指定目录下的所有 ChangeLog 文件,并按文件名排序后依次执行。适用于按版本或模块组织的目录结构。

属性 说明 示例
path 扫描的目录路径(必填) path="changelogs/v2.0/"
relativeToChangelogFile 路径是否相对于当前 ChangeLog 文件 relativeToChangelogFile="true"
filter 文件名正则过滤器 filter=".*\.xml"
endsWithFilter 按文件后缀过滤 endsWithFilter=".sql"
sortBy 排序方式(file_name 或 natural) sortBy="natural"

XML 示例:

<databaseChangeLog>
    <includeAll path="changelogs/v2.0/"
                relativeToChangelogFile="true"
                endsWithFilter=".xml"
                sortBy="natural"/>
</databaseChangeLog>
最佳实践: 使用 includeAll 时,建议按 sortBy="natural" 进行自然排序(如 001_create_table.xml、002_add_index.xml),避免因操作系统文件顺序不一致导致迁移顺序错乱。

4.4 模块化目录结构设计

结合 include 和 includeAll,推荐采用按版本 + 按模块的目录结构,实现清晰的变更管理:

changelogs/
├── master.xml                    # 根 ChangeLog,仅包含 include/includeAll
├── v1.0/
│   ├── 001_create_tables.xml
│   ├── 002_seed_data.sql
│   └── 003_add_indexes.xml
├── v1.1/
│   ├── 001_alter_users.xml
│   └── 002_add_orders.xml
└── v2.0/
    ├── 001_new_schema.xml
    └── 002_migrate_data.sql

master.xml 示例:

<databaseChangeLog>
    <includeAll path="v1.0/" relativeToChangelogFile="true"/>
    <includeAll path="v1.1/" relativeToChangelogFile="true"/>
    <includeAll path="v2.0/" relativeToChangelogFile="true"/>
</databaseChangeLog>
版本对齐策略: 每个版本目录对应一个数据库基线版本,目录内的文件按数字前缀排序。当需要回滚到某个版本时,只需在 master.xml 中注释掉后续版本的 includeAll 即可。

4.5 ChangeLog 组织架构图

下图展示了从根 ChangeLog 到子文件的模块化组织关系,以及 include 与 includeAll 的引用路径:

ChangeLog 模块化组织架构图 master.xml 根 ChangeLog include v1.0/ (includeAll) v2.0/ (includeAll) 001_create_tables.xml 002_seed_data.sql 003_add_indexes.xml 001_new_schema.xml 002_migrate_data.sql 说明: • include:精确引用单个文件,适合跨版本或跨模块的显式依赖。 • includeAll:批量扫描目录,按文件名排序执行,适合按版本组织的目录结构。 • 建议每个版本目录内使用数字前缀(如 001_、002_)控制执行顺序。

4.6 四种格式的迁移执行流程对比

下图展示了 Liquibase 解析四种格式 ChangeLog 并执行迁移的流程差异:

四种格式迁移执行流程对比 XML ChangeLog YAML ChangeLog JSON ChangeLog SQL ChangeLog XSD 校验 + DOM 解析 SnakeYAML 解析 Jackson 解析 正则提取 changeset 头 统一变更集模型 (ChangeSet) 数据库执行 (JDBC) 所有格式最终都会被 Liquibase 解析为统一的 ChangeSet 对象模型,执行逻辑完全一致。

4.7 常见问题与避坑指南

  • 路径问题: 始终设置 relativeToChangelogFile="true",避免因工作目录不同导致文件找不到。
  • 排序问题: includeAll 默认按文件名排序,建议使用 sortBy="natural" 并配合数字前缀(如 001_、002_)确保顺序可控。
  • 格式混用: 同一个 includeAll 目录下可以混合 XML、YAML、JSON、SQL 文件,Liquibase 会根据文件后缀自动选择解析器。
  • SQL 格式回滚: SQL 格式必须手动编写 -- rollback 语句,否则 Liquibase 无法自动生成回滚 SQL。
  • 上下文与标签: 使用 context 和 labels 属性控制不同环境(dev/prod)的变更集执行,避免生产环境执行测试数据。
重要提醒: 不要在 includeAll 目录中放置非 ChangeLog 文件(如 README.md、.gitkeep),否则 Liquibase 会尝试解析并报错。建议使用 endsWithFilter 或 filter 属性限定文件后缀。