4、Schema变更的版本控制:用Git管好你的DDL脚本
说实话,我见过太多团队把DDL脚本当“一次性工具”来用。跑完就扔,下次改表结构全靠回忆。嗯,这其实是个大坑。你想想看,代码都有Git管着,凭什么数据库Schema就能例外?
我个人习惯是:所有DDL脚本必须进Git,一个都不能少。这不是矫情,是血泪教训换来的。我曾经在一个老项目里,为了查一个字段是哪个版本加的,翻了三个月的聊天记录……从那以后,我再也不敢让DDL脚本“裸奔”了。
4.1 为什么DDL脚本需要版本控制?
说白了,数据库Schema也是代码。它定义了数据的“形状”。形状变了,代码的逻辑也得跟着变。如果Schema的变更没有记录、没有追溯,那出了问题你根本不知道从哪查起。
我总结了几点核心价值:
- 可追溯:谁、在什么时候、改了哪个表、改了什么字段,一清二楚。
- 可回滚:上线出问题了,能快速找到上一个版本的DDL,把结构改回去。
- 可协作:多个开发同时改表结构,用Git分支管理,避免冲突。
- 可审计:合规要求、安全审查,Git log就是最好的证据。
核心原则:DDL脚本和业务代码一样,必须经过Code Review、测试、然后才能合并到主分支。别想着“我就改个小字段,直接连数据库改一下得了”——这种想法我劝你趁早打消。
4.2 目录结构怎么设计?
我见过很多种组织方式,踩过坑之后,我推荐下面这种结构。它清晰、可扩展,而且适合自动化工具扫描。
db/
├── migrations/
│ ├── V1.0.0__create_users_table.sql
│ ├── V1.0.1__add_email_to_users.sql
│ ├── V1.1.0__create_orders_table.sql
│ └── V1.1.1__add_index_on_orders.sql
├── rollbacks/
│ ├── V1.0.0__create_users_table.sql
│ ├── V1.0.1__add_email_to_users.sql
│ ├── V1.1.0__create_orders_table.sql
│ └── V1.1.1__add_index_on_orders.sql
├── seed/
│ └── init_data.sql
└── README.md
注意几个要点:
- 命名规范:用
V{版本号}__{描述}.sql的格式。版本号建议用语义化版本(SemVer),比如V1.0.0、V1.1.0。这样一眼就能看出是大版本还是小改动。 - 正向和回滚脚本成对出现:每个
migrations里的脚本,在rollbacks里都有一个对应的回滚脚本。这是底线。 - 不要修改已合并的脚本:一旦脚本合并到主分支并执行过,就不能修改。要改结构?新建一个版本号更高的脚本。
小技巧:我习惯在脚本文件名里加上时间戳,比如 V20240501_001__add_email_to_users.sql。这样即使多人同时开发,文件名也不会冲突,而且按时间排序后,执行顺序一目了然。
4.3 变更记录怎么写?
光有脚本还不够,你还得记录“为什么改”。Git commit message 就是最好的载体。我建议每个DDL脚本的commit message都包含以下信息:
| 字段 | 说明 | 示例 |
|---|---|---|
| 类型 | 变更类型:ADD/MODIFY/DROP | ADD |
| 对象 | 变更的表或索引名 | users |
| 变更内容 | 具体做了什么 | add column email |
| 原因 | 为什么做这个变更 | 用户需要邮箱登录 |
| JIRA编号 | 关联的需求或缺陷编号 | PROJ-1234 |
举个例子,一个规范的commit message长这样:
feat(db): ADD column email to users table
- 变更类型: ADD
- 变更对象: users
- 变更内容: 新增 email 字段,varchar(255),唯一索引
- 变更原因: 支持用户使用邮箱注册和登录
- 关联需求: PROJ-1234
正向脚本: db/migrations/V1.0.1__add_email_to_users.sql
回滚脚本: db/rollbacks/V1.0.1__add_email_to_users.sql
为什么要写这么详细?因为半年后你回头看,可能完全想不起来当时为什么加这个字段。而Git log就是你的“时光机”。
4.4 审计:谁动了我的Schema?
审计不只是为了合规,更是为了“甩锅”的时候有据可查。嗯,开个玩笑。但说真的,出了问题能快速定位责任人,能省下大量扯皮的时间。
我建议做三层审计:
- Git层:通过
git log -- db/migrations/查看所有DDL脚本的变更历史。谁提交的、什么时候提交的、commit message写了什么,一清二楚。 - 数据库层:在数据库中开启DDL触发器,记录所有Schema变更到一张审计表。这样即使有人绕过Git直接改数据库,也能抓到。
- CI/CD层:在自动化部署流水线中,记录每次部署执行的DDL脚本列表和结果。这样能知道哪个版本部署到了哪个环境。
注意:Git只能管住“通过Git提交的变更”。如果有人直接连数据库执行 ALTER TABLE,Git是不知道的。所以数据库层的DDL触发器是最后一道防线,千万别省。
4.5 核心流程:一张图看懂
下面这张图展示了从开发到上线的完整DDL版本控制流程。我把它画成了流程图,方便你理解。
这个流程看起来简单,但每个环节都有坑。比如Code Review阶段,我建议至少两个人看:一个人看SQL语法和逻辑,另一个人看变更对业务的影响。我曾经就因为只看语法没看业务逻辑,把一个生产环境的字段类型改错了,导致线上服务挂了半小时……
4.6 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 不要相信“这次改动很小,不用回滚脚本”——每次都要写回滚脚本,这是纪律问题。
- 不要在同一个脚本里混合多个不相关的变更——一个脚本只做一件事。比如加字段和加索引,拆成两个脚本。这样回滚的时候更灵活。
- 脚本执行前一定要备份——哪怕只是加个字段。备份是最后的救命稻草。
- 自动化工具不是万能的——像Flyway、Liquibase这些工具能帮你管理脚本执行顺序,但它们不会帮你判断“这个变更是否合理”。人的判断力才是核心。
我的习惯:每次上线前,我都会在预发布环境完整跑一遍所有DDL脚本,然后执行回滚脚本,确认回滚也能正常工作。这个过程虽然多花10分钟,但能避免90%的上线事故。
好了,关于DDL版本控制的核心思路就这些。记住一句话:把Schema当代码管,把变更当提交写。做到这一点,你的数据库就不会变成“黑盒”。