版本控制与Schema即代码:Git管理数据库脚本、Schema即代码(SaC)理念、版本号与基线管理
说实话,我见过太多团队把数据库脚本当「二等公民」对待。
代码仓库里管理得井井有条,数据库那边却是一团乱麻——脚本散落在共享文件夹里,命名靠「最终版v3」这种玄学,上线全靠某位老同事的记忆力。这种搞法,不出事是运气,出事是必然。
这一章,我们就来聊聊怎么把数据库脚本拉回正轨。
为什么数据库脚本需要版本控制?
你想想看,应用代码有Git,有CI/CD,有回滚机制。数据库呢?很多团队还在用「手动执行+口头通知」的模式。
我在项目中遇到过一家公司,生产环境的数据表结构跟测试环境差了十几个版本。问了一圈,没人说得清哪些脚本执行过,哪些没执行。最后只能靠对比工具逐字段核对,折腾了整整两天。
数据库脚本的版本控制,说白了就解决三个问题:
- 可追溯:谁在什么时候改了什么,一查便知
- 可复现:从空库到最新版本,一条命令搞定
- 可协作:多人并行开发,冲突能发现、能解决
核心原则:数据库脚本应该和应用代码享受同等待遇——进Git、走评审、跑CI、留记录。
Git管理数据库脚本的最佳实践
Git本身不复杂,但数据库脚本有其特殊性。我个人的习惯是,按这样的目录结构来组织:
project-root/
├── migrations/
│ ├── V1__init_schema.sql
│ ├── V1.1__add_users_table.sql
│ ├── V2__add_orders_table.sql
│ └── V2.1__add_order_items.sql
├── seed/
│ ├── dev/
│ │ ├── 001_admin_user.sql
│ │ └── 002_test_data.sql
│ └── prod/
│ └── 001_admin_user.sql
├── functions/
│ ├── fn_calculate_tax.sql
│ └── fn_get_user_balance.sql
├── procedures/
│ └── sp_process_order.sql
└── rollback/
├── V1__rollback.sql
├── V1.1__rollback.sql
└── V2__rollback.sql
这里有几个要点:
- 命名规范:版本号+描述,一眼就能看出顺序和用途
- 单向递增:版本号只增不减,已经发布的版本绝不修改
- 回滚脚本:每个变更都配一个回滚脚本,这是保命用的
- 种子数据:开发环境和生产环境分开管理,避免误操作
小技巧:我习惯在提交信息里加上数据库变更的摘要,比如「V2.1: 添加订单明细表,增加外键约束」。这样看Git log就能快速定位数据库变更,不用一个个翻文件。
Schema即代码(SaC)理念
Schema即代码,英文叫Schema as Code,简称SaC。这个概念其实很简单:把数据库Schema当成应用代码一样来管理、测试和部署。
为什么强调这个?因为很多团队把数据库Schema当成「基础设施」,改一下要审批、要排期、要DBA手动操作。这种模式在微服务时代根本跑不起来。
SaC的核心主张有三条:
- 声明式定义:用代码描述你想要的最终状态,而不是手动执行一系列变更
- 自动化执行:变更过程由工具自动完成,减少人为失误
- 持续验证:每次变更都经过自动化测试,确保不会破坏现有功能
举个例子,用Liquibase或Flyway这类工具,你只需要写一个变更文件:
-- Liquibase变更集示例
-- changeset zhangsan:001
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- rollback DROP TABLE users;
工具会自动记录哪些变更已经执行过,哪些还没执行。你只管提交代码,部署的事交给流水线。
注意:SaC不是银弹。它要求团队有严格的代码评审习惯,变更文件必须经过Review才能合并。我曾经见过一个团队,因为跳过评审直接合并,结果把生产环境的索引全删了——回滚花了三个小时。
版本号与基线管理
版本号怎么定?基线又是什么?这两个概念搞不清楚,Schema管理就容易乱。
版本号策略
我个人推荐语义化版本号,格式是:主版本.次版本.修订号。
| 版本号 | 变更类型 | 示例 |
|---|---|---|
| 主版本 | 不兼容的Schema变更 | 删除表、修改字段类型 |
| 次版本 | 向后兼容的新增 | 新增表、新增字段 |
| 修订号 | 小修小改 | 修改注释、调整索引 |
实际项目中,我习惯在版本号前面加个前缀,比如V1.0.0、V1.1.0。这样排序时能按字符串顺序正确排列。
基线管理
基线是什么?说白了,就是某个时间点上Schema的「快照」。它代表一个稳定的、可复现的状态。
基线的作用:
- 快速重建:从基线出发,按顺序执行后续变更,就能得到最新Schema
- 环境同步:开发、测试、预发、生产,都从同一个基线出发,保证一致性
- 灾难恢复:出问题时,可以快速回退到某个已知的稳定状态
我通常的做法是,每个大版本发布后,生成一个基线脚本:
-- baseline_v2.sql
-- 这是V2版本的完整Schema快照
-- 生成时间:2024-06-15 10:00:00
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 后续所有变更都基于这个基线
关键点:基线一旦生成,就不可修改。如果发现基线有问题,正确的做法是写一个新的变更脚本来修复,而不是直接改基线文件。
知识体系总览
下面这张图,把这一章的核心逻辑串起来了:
避坑指南
最后,分享几个我踩过的坑:
- 不要手动修改已发布的脚本:哪怕只是改个注释,也会导致校验失败。正确的做法是发一个新版本。
- 回滚脚本一定要写:我曾经觉得「这个变更很简单,不会出问题」,结果上线后才发现有个边界情况没考虑到。没有回滚脚本,只能手动修复,压力巨大。
- 基线要定期更新:如果变更太多,每次重建都要执行几百个脚本,耗时很长。我建议每10-20个版本生成一次新的基线。
- 环境间的基线要对齐:开发环境用V2基线,测试环境用V3基线,这种错位会导致各种诡异问题。统一基线,统一出发。
嗯,这一章的内容就到这里。记住一句话:把数据库脚本当代码管,它就会像代码一样可靠。反过来,你把它当草稿,它就会给你带来灾难。