Ohhnews

分类导航

$ cd ..
DZone Java原文

告别数据库迁移脚本:Java声明式模式同步工具 SchemaSynchronizer

#数据库迁移#声明式同步#java#spring boot#开源工具

如果你曾在成熟的 Java 后端应用上工作过,那么你的 src/main/resources/db/migration 文件夹很可能看起来像一本混乱的历史账本:

V1__init_schema.sql
V2__add_users_table.sql
...
V142__add_status_column.sql
V143__fix_status_typo.sql
V144__drop_old_status.sql
V145__add_missing_index.sql

像 Flyway 和 Liquibase 这样的工具十多年来一直是行业标准。它们基于命令式模型运行:“按精确顺序应用这些脚本,将数据库从版本 N 迁移到版本 N+1。”

在项目早期,这种模型感觉很合理。但在规模化场景下,命令式迁移会造成严重的运维摩擦。两名工程师从 main 分支拉出分支后,不可避免地会创建相互冲突的 V146__... 脚本。从头搭建新的测试环境需要重放数百个顺序脚本。最危险的是,如果工程师不小心提交了包含不安全 DROP TABLE 或 ALTER COLUMN 的脚本,迁移工具会盲目地对生产数据库执行它,可能导致灾难性的数据丢失。

现代基础设施工具——例如 Kubernetes 和 Terraform——多年前就已摆脱命令式脚本。它们采用声明式状态协调:“系统现在应当完全呈现这个样子。请安全地想办法让它变成这样。”

现在,是时候用同样的声明式理念来管理关系型数据库 schema 了。

SchemaSynchronizer 登场

SchemaSynchronizer 是一个面向 Java 和 Spring Boot 的开源库,为关系型数据库(PostgreSQL、MySQL、MariaDB、SQL Server 和 Oracle)带来声明式、基于期望状态的 schema 演进。你的应用不再维护一条不断增长的迁移脚本链,而是维护一个 schema-definition.json 文件,用来表示数据库结构当前期望的状态。

$ cat
{
  "formatVersion": 2,
  "dialect": "postgresql",
  "tables": {
    "work_items": {
      "createSql": "CREATE TABLE IF NOT EXISTS work_items (id UUID PRIMARY KEY, status VARCHAR(32))",
      "columns": [
        {"name": "id", "definition": "UUID NOT NULL"},
        {"name": "status", "definition": "VARCHAR(32)"}
      ],
      "indexes": [
        "CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items (status)"
      ]
    }
  },
  "changes": []
}

启动时,SchemaSynchronizer 并非盲目执行 SQL,而是执行一个状态协调循环。

同步引擎的内部工作原理

当你的 Spring Boot 应用启动时,SchemaSynchronizer 通过 JDBC 连接数据库,并提取实时目录元数据。它会检查现有表、列类型、默认值、可空性约束和索引,然后与 JSON 定义进行差异计算,并根据特定方言规则应用变更。

数据库引擎在处理数据定义语言(DDL)时向来高度碎片化。

  • 事务性 DDL:在 PostgreSQL 和 SQL Server 上,SchemaSynchronizer 将同步过程包裹在事务中。如果某一步失败,整个 schema 变更会干净地回滚。
  • 隐式提交:MySQL、MariaDB 和 Oracle 在执行 DDL 语句的那一刻会原生提交已打开的事务。SchemaSynchronizer 会针对这些方言调整执行策略,依靠细粒度验证检查,而不是事务回滚。
  • 分布式锁:为防止多个应用实例在滚动部署期间争抢 schema,该库使用原生分布式锁原语(Postgres 中的 pg_try_advisory_xact_lock、SQL Server 中的 sp_getapplock、MySQL 中的 GET_LOCK 以及 Oracle 中的 DBMS_LOCK)。

安全边界:故障关闭设计

对于自动化数据库工具而言,最关键的一项架构要求是防止意外数据破坏。SchemaSynchronizer 围绕严格的故障关闭安全边界构建。它将 schema 演进划分为两个截然不同的类别:

1. 安全的增量变更(自动执行)

如果你在 JSON 定义中新增一个可空的 description 列,SchemaSynchronizer 会检测到缺失的列,并在启动时自动发出 ALTER TABLE ... ADD COLUMN 语句。创建新表、创建索引、放宽兼容的数据类型(例如从 VARCHAR(50) 放宽到 VARCHAR(100)),或放宽 NOT NULL 约束,也同样如此。

2. 破坏性或歧义性变更(人工审查)

如果你从 JSON 定义中删除一列,会发生什么?SchemaSynchronizer 绝不会推断出 DROP 操作。如果它检测到孤立的实时列,或可能截断数据的类型收窄,它会停止同步过程,并输出解决差异所需的确切 SQL。

[SchemaSynchronizer] Destructive difference detected. Manual review required:
ALTER TABLE work_items DROP COLUMN old_status;

这形成了严格的运维边界。日常的增量漂移会被自动修复,从而减少开发人员的摩擦;但破坏性协调始终需要明确的人工介入和批准。

使用带校验和的账本处理复杂迁移

并非所有数据库操作都能从声明式表结构中干净地推断出来。携带特定业务逻辑的操作——例如数据回填、复杂的 CHECK 约束、触发器或特定用户授权——需要显式执行。为处理这些情况,SchemaSynchronizer 在 JSON 文档中包含一个有序的 changes 数组。这些变更充当不可变的、带校验和的账本,类似传统迁移,但有一个关键补充:验证 SQL。

$ cat
"changes": [
  {
    "id": "001-work-item-status",
    "description": "Backfill and constrain work item status",
    "statements": [
      "UPDATE work_items SET status = 'PENDING' WHERE status IS NULL",
      "ALTER TABLE work_items ALTER COLUMN status SET NOT NULL"
    ],
    "verificationSql": "SELECT NOT EXISTS (SELECT 1 FROM work_items WHERE status IS NULL)",
    "phase": "AFTER_SCHEMA"
  }
]

verificationSql 必须是一个只读查询,并返回布尔值 true。这确保了幂等性。如果数据库已经满足验证查询(也许是因为 DBA 在事故处理期间手动应用了修复),SchemaSynchronizer 会将该变更集记录为已完成,而不会尝试盲目重放 UPDATE 语句。

实现:Spring Boot 与 CLI

SchemaSynchronizer 可原生集成到 Java 应用中。对于 Spring Boot,将依赖添加到构建文件:

Maven:

$ xml
<dependency>
    <groupId>com.thinkaillc</groupId>
    <artifactId>schema-synchronizer</artifactId>
    <version>2.0.0</version>
</dependency>

配置你的 application.properties,以启用初始化器,并告诉 Hibernate 退让,仅校验 schema:

$ properties
schema-synchronizer.enabled=true
schema-synchronizer.schema=public
schema-synchronizer.fail-on-pending=true
# Let SchemaSynchronizer handle DDL; tell Hibernate to just validate
spring.jpa.hibernate.ddl-auto=validate

独立 CLI 流水线

许多组织更愿意将 schema 迁移与应用启动解耦,直接从 CI/CD 流水线(如 GitHub Actions 或 Jenkins)执行迁移。SchemaSynchronizer 提供了一个独立可执行 JAR,并打包了所有受支持引擎的 JDBC 驱动。你可以针对目标数据库试运行一次部署,准确查看会发生哪些变更,而不提交任何 DDL:

$ bash
export SCHEMA_DB_PASSWORD='target-password'
java -jar schema-synchronizer-cli-2.0.0-standalone.jar dry-run \
  jdbc:postgresql://localhost:5432/target_app app_user - \
  schema-definition.json public schema_synchronizer_history

准备好放弃迁移脚本了吗?

命令式迁移脚本多年来为 Java 生态作出了良好贡献,但随着部署拓扑变得越来越复杂,管理数百个线性状态转换文件正成为一种负担。

通过采用声明式方法,你可以消除迁移文件上的合并冲突,从单一事实来源即时搭建新环境,并强制执行严格的安全边界,使自动化工具永远不会删除生产数据。

查看仓库中的架构、方言支持和快速入门指南:

GitHub:eugenena/SchemaSynchronizer

DZone 贡献者表达的观点仅代表其个人观点。