管理数据库模式更改的基础

发布: (2026年1月4日 GMT+8 20:51)
8 min read
原文: Dev.to

Source: Dev.to

当你更新已发布的应用程序时,通常必须修改这些模式(schema)。
安全且高效地管理这些更改是一个根本性的工程挑战。

本文概述了我对模式管理的方法、其权衡以及提升可靠性的扩展策略。作为独立创业者,我优先考虑简洁性和生产力。虽然我使用 Java 和 [JMigrate],但这些概念同样适用于任何库,例如 [Flyway][Liquibase][MyBatis Migrations],以及 Rails 等语言和框架。

更改数据库模式的问题

假设你创建了一个 user 表:

CREATE TABLE "jmigrate_test_user" (
    id INTEGER PRIMARY KEY,
    username TEXT NOT NULL,
    hashed_password TEXT NOT NULL,
    password_expired_at TIMESTAMP
);

如果你在一周后需要添加一个 last_login 列,你可以手动执行 SQL——这在 1999 年是常见做法。然而,手动更新会产生两个关键问题:

  1. 没有版本控制 – 更改未被追踪。你无法确定源代码何时开始支持新列,也无法轻松回滚。
  2. 团队不同步 – 你必须在每位团队成员的本地环境中协调手动更新,这容易出错且效率低下。

如果同事同时尝试添加一个 age 列,缺乏自动化会导致难以解决的 模式冲突

我的解决方案

每一次数据库更改都应提交到 Git。

  • 将迁移文件存放在专用文件夹中(例如 migrations/)。
  • 使用顺序文件名:1.sql2.sql3.sql,…
    要修改模式,只需在序列中添加下一个文件(例如 4.sql)。

每个迁移脚本包含两个部分:

部分用途
Up用于升级数据库模式的 SQL
Down用于撤销 Up 部分所做更改的 SQL

注意: 生产环境应禁止使用 Down 脚本以防止数据丢失,但在开发环境中执行它们对于灵活性是必不可少的。

JMigrateFlywayLiquibaseMyBatis Migrations 之类的工具可以自动化此过程。使用 JMigrate 时,只需在应用启动时调用一次 JMigrate.migrate(),即可处理所有待执行的迁移。

实际工作原理

场景 1 – 迭代迁移脚本

你想使用 BIGINT(纪元毫秒)添加一个 last_login 列。创建 5.sql

# --- !Ups
ALTER TABLE "user" ADD COLUMN "last_login" BIGINT;

# --- !Downs
ALTER TABLE "user" DROP COLUMN "last_login";

运行脚本后,你意识到 TIMESTAMP 类型更合适。修改 5.sql

# --- !Ups
ALTER TABLE "user" ADD COLUMN "last_login" TIMESTAMP;

# --- !Downs
ALTER TABLE "user" DROP COLUMN "last_login";

在开发环境中,JMigrate 检测到此修改。它会自动执行之前的 Down 脚本(DROP COLUMN),然后执行修改后的 Up 脚本(ADD COLUMN … TIMESTAMP),保持本地数据库与代码同步。

场景 2 – 两位开发者同时进行迁移

  • 你添加 5.sql,因为上一次已应用的迁移是 4.sql
  • 你的同事也添加了一个 5.sql 并先合并了它。

Git 会报告冲突。通过将你的文件重命名为 6.sql 来解决。

在每个开发者的本地环境中,JMigrate 会自动先运行 5.sql(同事的),再运行 6.sql(你的),确保数据库保持同步,无需手动干预。

场景 3 – 意外修改过去的迁移

你编辑了已经部署的 3.sql。由于在生产环境中“执行 down 脚本”被禁用,JMigrate 会抛出异常并中止部署。这是最安全的结果——你会收到警报,撤销错误的更改,并在重新部署前创建正确的修复。

使迁移更可靠

在大规模时,自动迁移过程会暴露一个缺陷:不向后兼容的更改会导致停机

重命名列是一个经典例子。如果你把 name 重命名为 full_name,现有的应用实例会继续查询 name,直到它们重新部署,从而导致运行时异常。

许多工程师(例如在 Stripe、Google)完全避免重命名,宁愿保留“糟糕”的名称。当重命名不可避免时,使用多步骤部署来保持可用性:

  1. 添加新列并部署。
  2. 双写到旧列和新列并部署。
  3. 回填数据从旧列到新列(对于大表可能需要数天)。
  4. 只从新列读取并部署。
  5. 移除旧列并部署。

JMigrate:一个简单的 Java 数据库模式迁移库

我创建 JMigrate 是为了提供一个比大型工具更轻量的替代方案。

功能JMigrate替代方案(Flyway、Liquibase、MyBatis)
简洁性
零配置
友好的 Git 顺序脚本
自动检测脚本编辑(仅限开发)
内置 “up/down” 部分
生产安全(禁用 down 脚本)

(如有需要,可在表格中添加更多行。)

TL;DR

  • 将每个模式更改存储为版本控制中的编号 SQL 文件。
  • 在每个文件中保留 updown 部分。
  • 在启动时自动运行迁移(例如 JMigrate.migrate())。
  • 对不向后兼容的更改使用多步骤部署。

遵循此模式,您可以获得一种可靠、可审计且协作的方式来演进数据库模式——无论使用何种语言或框架。

迁移工具概览

特性JMigrateFlywayLiquibase
函数调用单个函数调用即可处理所有迁移。通常需要复杂的配置。通常需要复杂的配置。
集成纯 Java;在应用程序内部运行。通常需要单独的 CLI,像 Heroku 和 Render.com 这类平台可能会限制。通常需要单独的 CLI,像 Heroku 和 Render.com 这类平台可能会限制。
大小14 KB800 KB(Flyway)3 MB(Liquibase)

使用场景指南

  • JMigrate 非常适合桌面和自托管应用程序,因为此类场景对文件体积最小化和架构简洁性要求极高。
  • 大规模服务器端应用程序——团队自行管理部署——通常更看重丰富的功能集,因此 Flyway 或 Liquibase 更为合适。

摘要

管理数据库模式迁移是基本的工程职责。虽然现代库自动化最佳实践,但工程师必须了解底层机制,以解决异常情况,例如迁移失败。

当前标准支持同步开发、严格测试和无缝部署。无论您选择 JMigrateFlywayLiquibaseMyBatis,您现在都能够自信地管理模式更改。

Back to Blog

相关文章

阅读更多 »

我直到构建持久层才明白 JPA

封面图片 👉“I Didn’t Understand JPA Until I Built the Persistence Layer” https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto...

递归的迭代

将简化 HTML 转换为 Markdown 并使用语法树 在我的一个副项目或“宠物”项目中,我编写了一个小型 parser,用于简化 HTML,p...