版本控制与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的核心主张有三条:

  1. 声明式定义:用代码描述你想要的最终状态,而不是手动执行一系列变更
  2. 自动化执行:变更过程由工具自动完成,减少人为失误
  3. 持续验证:每次变更都经过自动化测试,确保不会破坏现有功能

举个例子,用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)
);

-- 后续所有变更都基于这个基线

关键点:基线一旦生成,就不可修改。如果发现基线有问题,正确的做法是写一个新的变更脚本来修复,而不是直接改基线文件。

知识体系总览

下面这张图,把这一章的核心逻辑串起来了:

版本控制与Schema即代码知识体系 版本控制(Git) 目录结构规范 Schema即代码 版本号与基线 migrations / seed / functions 命名规范:V1.0.0__描述 回滚脚本配套管理 声明式定义最终状态 自动化执行变更 持续验证与CI集成 语义化版本号策略 基线快照生成与锁定 灾难恢复与快速重建 核心理念 数据库脚本 = 一等公民代码 可追溯 · 可复现 · 可协作

避坑指南

最后,分享几个我踩过的坑:

  • 不要手动修改已发布的脚本:哪怕只是改个注释,也会导致校验失败。正确的做法是发一个新版本。
  • 回滚脚本一定要写:我曾经觉得「这个变更很简单,不会出问题」,结果上线后才发现有个边界情况没考虑到。没有回滚脚本,只能手动修复,压力巨大。
  • 基线要定期更新:如果变更太多,每次重建都要执行几百个脚本,耗时很长。我建议每10-20个版本生成一次新的基线。
  • 环境间的基线要对齐:开发环境用V2基线,测试环境用V3基线,这种错位会导致各种诡异问题。统一基线,统一出发。

嗯,这一章的内容就到这里。记住一句话:把数据库脚本当代码管,它就会像代码一样可靠。反过来,你把它当草稿,它就会给你带来灾难。