管理数据库模式更改的基础
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 年是常见做法。然而,手动更新会产生两个关键问题:
- 没有版本控制 – 更改未被追踪。你无法确定源代码何时开始支持新列,也无法轻松回滚。
- 团队不同步 – 你必须在每位团队成员的本地环境中协调手动更新,这容易出错且效率低下。
如果同事同时尝试添加一个 age 列,缺乏自动化会导致难以解决的 模式冲突。
我的解决方案
每一次数据库更改都应提交到 Git。
- 将迁移文件存放在专用文件夹中(例如
migrations/)。 - 使用顺序文件名:
1.sql、2.sql、3.sql,…
要修改模式,只需在序列中添加下一个文件(例如4.sql)。
每个迁移脚本包含两个部分:
| 部分 | 用途 |
|---|---|
| Up | 用于升级数据库模式的 SQL |
| Down | 用于撤销 Up 部分所做更改的 SQL |
注意: 生产环境应禁止使用 Down 脚本以防止数据丢失,但在开发环境中执行它们对于灵活性是必不可少的。
像 JMigrate、Flyway、Liquibase 和 MyBatis 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)完全避免重命名,宁愿保留“糟糕”的名称。当重命名不可避免时,使用多步骤部署来保持可用性:
- 添加新列并部署。
- 双写到旧列和新列并部署。
- 回填数据从旧列到新列(对于大表可能需要数天)。
- 只从新列读取并部署。
- 移除旧列并部署。
JMigrate:一个简单的 Java 数据库模式迁移库
我创建 JMigrate 是为了提供一个比大型工具更轻量的替代方案。
| 功能 | JMigrate | 替代方案(Flyway、Liquibase、MyBatis) |
|---|---|---|
| 简洁性 | ✅ | ❌ |
| 零配置 | ✅ | ❌ |
| 友好的 Git 顺序脚本 | ✅ | ✅ |
| 自动检测脚本编辑(仅限开发) | ✅ | ❌ |
| 内置 “up/down” 部分 | ✅ | ✅ |
| 生产安全(禁用 down 脚本) | ✅ | ✅ |
(如有需要,可在表格中添加更多行。)
TL;DR
- 将每个模式更改存储为版本控制中的编号 SQL 文件。
- 在每个文件中保留 up 和 down 部分。
- 在启动时自动运行迁移(例如
JMigrate.migrate())。 - 对不向后兼容的更改使用多步骤部署。
遵循此模式,您可以获得一种可靠、可审计且协作的方式来演进数据库模式——无论使用何种语言或框架。
迁移工具概览
| 特性 | JMigrate | Flyway | Liquibase |
|---|---|---|---|
| 函数调用 | 单个函数调用即可处理所有迁移。 | 通常需要复杂的配置。 | 通常需要复杂的配置。 |
| 集成 | 纯 Java;在应用程序内部运行。 | 通常需要单独的 CLI,像 Heroku 和 Render.com 这类平台可能会限制。 | 通常需要单独的 CLI,像 Heroku 和 Render.com 这类平台可能会限制。 |
| 大小 | 14 KB | 800 KB(Flyway) | 3 MB(Liquibase) |
使用场景指南
- JMigrate 非常适合桌面和自托管应用程序,因为此类场景对文件体积最小化和架构简洁性要求极高。
- 大规模服务器端应用程序——团队自行管理部署——通常更看重丰富的功能集,因此 Flyway 或 Liquibase 更为合适。
摘要
管理数据库模式迁移是基本的工程职责。虽然现代库自动化最佳实践,但工程师必须了解底层机制,以解决异常情况,例如迁移失败。
当前标准支持同步开发、严格测试和无缝部署。无论您选择 JMigrate、Flyway、Liquibase 或 MyBatis,您现在都能够自信地管理模式更改。