Atlas数据库迁移实战:用声明式Schema替代手写SQL脚本

发布时间:2026/10/19 14:10:17
Atlas数据库迁移实战:用声明式Schema替代手写SQL脚本 两周前我接手了一个遗留项目的数据库维护工作。那套系统上线快三年迁移脚本散落在三个不同的目录里有的叫v1.0.3_fix.sql有的叫add_column_xxx_temp.sql版本号对不上本地库和测试库的结构也早就漂移得不像同一个项目。我花了两天才理清楚线上到底有哪些表那一刻我就决定必须换一套数据库 Schema 管理方案。后来我把项目里的迁移流程整个切到了Atlas。它不是又一个让程序员手写 SQL 的老式迁移工具而是反了过来你先描述我最终想要什么结构Atlas 帮你算出来现在需要变哪些。对我这种被历史包袱捆住手脚的人这套思路几乎是一剂解药。这篇文章就把我换工具的过程中踩过的坑、梳理清楚的概念、以及目前跑在 CI 里的完整用法都写出来给同样在项目里挣扎的人一个参考。1. 从手写迁移脚本到声明式 Schema我为什么换到 Atlas1.1 旧方案的问题在哪过去团队用的是经典的版本化迁移脚本每改一次表结构就加一个20240118_xxx.sql。听起来很规整实际用起来有几个致命伤。第一个问题是脚本可执行性没保证。A 环境执行到第 12 个脚本时报错B 环境可能早就执行到第 15 个了。数据库不像应用代码没有一个git status能告诉你哪一步没跑。很多团队靠维护一张schema_version表来自欺欺人而这张表本身也可能在某个环境里被人手工跳过。第二个问题是评审难。一个加了索引的脚本一个删了字段的脚本都只是一堆 SQL 文本。你很难一眼看出来这次改动到底会不会锁表是不是会在生产环境丢掉数据。等老代码里某个查询还在引用被你删掉的列时爆炸的就是线上。第三个问题是数据库真实的 schema 和你想的不一样。ORM 的模型、迁移脚本、测试环境三方经常对不上。这也是我这次接手项目的直接痛点。1.2 Atlas 提供了一条不同的路径Atlas 的核心思路叫做Desired State期望状态。你不再关心第几步改了什么而是维护一个完整的 schema 声明文件告诉它你最终要什么样的表结构。Atlas 对比目标数据库的当前状态生成一份迁移计划帮你把数据库变成期望状态。这套思路有两个很舒服的地方对已经存在的数据库可以让 Atlas 直接反向解析把线上结构导出成 HCL 声明文件作为起点。以后每次改动只需要在声明文件里加表、加字段Atlas 负责算 diff、生成可阅读的 SQL 迁移文件。它不是不要迁移脚本了而是把迁移脚本从人肉维护变成了机器生成。团队 review 的对象也从SQL 文本变成了你想要的最终结构 生成的 diff。这个转变对日常开发和数据库维护来说完全不一样。如果你现在管理的项目只有几张表、也不怎么变更那继续手写 SQL 问题不大。但如果你维护的是中大型项目跨多个环境发布数据结构每周都在变Atlas 这套声明式工作流绝对值得你花一个下午试试。2. 先理解 Atlas 的底牌Desired State、HCL 与迁移计划2.1 声明式 vs 版本化两边不是对立的很多人一上来就纠结 Atlas 到底是声明式工具还是版本化工具。实际上 Atlas 两套都支持atlas schema apply走声明式流程直接对比目标库和声明文件立即执行变更。atlas migrate diffatlas migrate apply走版本化流程先生成一份增量迁移文件再按版本顺序执行。我更推荐把 Atlas 当作用声明式来生成版本化迁移的组合用法。这样做的好处是你的迁移文件是确定性的、可回溯的可以走 review 和 CI而生成它的时候又不需要人肉去写每一段 DDL。这个组合是它在生产环境里最好使的形态。2.2 HCL 到底长什么样Atlas 自己定义了一套 schema 语法用 HCL 书写。它比 SQL DDL 更紧凑也更方便程序解析。下面是一段最基础的schema.hclschema public { comment application schema } table users { schema schema.public column id { type bigint auto_increment true unsigned true } column email { type varchar(255) null false } column created_at { type timestamp null false default sql(CURRENT_TIMESTAMP) } primary_key { columns [column.id] } index idx_users_email { unique true columns [column.email] } }第一次看到这段声明的人可能有点不适应它的可读性其实不差有一张 users 表有 id、email、created_at 三个字段主键是 idemail 上有唯一索引——非常直白。如果你的项目已经有数据库不需要自己默写这份文件直接用 Atlas 从线上库里反解出来atlas schema inspect \ --url mysql://root:passtcp(localhost:3306)/mydb \ --schema mydb \ --format hcl schema.hcl这条命令会把整个库的表结构、索引、外键、触发器都变成 HCL。生成后我习惯打开文件扫一遍确认没有把奇怪的历史残留带进期望状态。2.3 迁移计划是怎么算出来的Atlas 生成迁移计划的基本逻辑是期望状态HCL 文件减去当前状态数据库实际的表结构得到变化集合。听起来简单实际执行时有个关键点不同的数据库对 DDL 的理解不一样同一个 ALTER TABLE在 MySQL 8 和 PostgreSQL 15 上产生的行为可能不同。所以 Atlas 需要你提供一个--dev-url也就是一个临时的、干净的开发数据库。它会在这个空库上模拟执行 DDL把可能产生的隐式行为比如 MySQL 对整个表的重建也考虑进去。简单说dev-url 就是它的沙盒计算器没有它很多 diff 结果会失真。我第一次用的时候偷懒只给了--url和--to结果生成的 SQL 里多了一堆奇怪的 rename。加上 dev-url 之后一切都正常了。3. 把 Atlas 接进真实项目安装、初始化到第一次迁移3.1 安装与基础命令安装方式我试过几种最省事的是用 Homebrewbrew install ariga/tap/atlas如果团队统一用 Docker也可以直接拉镜像docker pull arigaio/atlas安装完先看一下版本atlas version需要注意Atlas 的 CLI 更新很快命令参数在不同大版本之间可能有差异。我写这篇文章时用的主版本是 v0.14 左右如果你执行下面命令发现某个参数不存在第一反应应该是去看atlas --help而不是质疑文章写错了。这也是所有快速演进的工具都有的通病。3.2 用 atlas.hcl 管理环境配置每次敲一长串参数非常容易出错Atlas 提供了atlas.hcl配置文件来帮你管理不同环境。在项目根目录创建一个atlas.hclenv local { src file://schema.hcl url mysql://root:passlocalhost:3306/mydb dev docker://mysql/8 migration { dir file://migrations format atlas } } env ci { src file://schema.hcl url ${CI_DB_URL} dev docker://mysql/8 migration { dir file://migrations format atlas } }我来解释一下每个字段的含义src期望状态的来源可以是一个 HCL 文件也可以是数据库连接。url目标数据库连接串就是你要改的那个库。devdev database 连接用于计算迁移计划和做 lint。migration.dir迁移文件存放目录。migration.format迁移文件的格式用默认atlas即可。配置好之后后续命令都很短。比如直接应用期望状态到本地库atlas schema apply --env localAtlas 会先连上本地库和schema.hcl对比然后把计划变更打印出来确认后执行。3.3 生成第一个版本化迁移文件我更推荐把它纳入版本控制所以第一个步骤是生成迁移文件atlas migrate diff init_migration \ --env local \ --dev-url docker://mysql/8这里init_migration是迁移文件的名字你可以按自己的习惯命名。执行成功之后migrations/目录下会出现类似下面这样的结构migrations/ 202501100001_init_migration.sql atlas.sumatlas.sum是用于校验迁移文件完整性的 checksum 文件必须提交到 Git不然 CI 里没法验证历史文件有没有被篡改。打开生成出来的 SQL你会看到类似这样的内容-- create users table CREATE TABLE users (id bigint unsigned NOT NULL AUTO_INCREMENT, email varchar(255) NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE INDEX idx_users_email (email)) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;到这里你已经为项目建立起了基线。3.4 在已有环境上回放迁移如果你已经用atlas schema apply把本地库改了那迁移文件的状态和数据库当前状态会不一致。更规范的做法是从迁移文件出发用atlas migrate apply把历史回放atlas migrate apply --env local这个命令会检查migrations/目录下所有迁移脚本只执行没有在当前数据库记录中出现的那些。这样你的数据库演进过程就变成了一条完整的时间线不会出现本地和线上各长各的的情况。我的经验是日常开发可以用atlas migrate diff生成新迁移但在真正改动共享环境前优先用atlas migrate apply回放而不是直接用schema apply跳过历史。前者更接近生产环境的发布路径容易暴露累积问题。4. 让迁移进入工作流本地开发、测试环境与 CI 的配合4.1 多人协作时迁移文件谁来生成团队里多个人同时改 schema 是冲突高发区。我的建议是每个人在自己分支上改schema.hcl用atlas migrate diff生成迁移文件。如果多人同时生成同号迁移文件Git 合并时大概率会在atlas.sum上冲突。解决方式是保留一个版本号更大的迁移文件删掉另一个同号的再重新生成一个。这个流程其实和代码合并没本质区别但这套设计让冲突从线上数据库结构不一致提前到了代码 review 阶段成本低很多。4.2 用 migrate lint 拦下破坏性变更最让我满意的一个命令是atlas migrate lint。它能在迁移文件 review 阶段就发现危险操作。假设你在分支里删掉了users表的email字段然后执行atlas migrate lint \ --env local \ --dev-url docker://mysql/8 \ --git-base mainAtlas 会比较migrations/目录从 main 分支以来的变化并对每条迁移语句做风险分析。遇到删除列、删除表、收紧字段长度这类操作时会输出一个 destructive 或 dangerous 级别的风险提示。我团队现在的规则是CI 里执行 lint如果风险级别超过 warning就阻止合并。注意lint 不是万能的它只能从 DDL 语义上判断风险判断不了你的业务是否真的还需要那张表。但它至少能拦住一些手一抖就把线上列删了的低级事故。4.3 在 GitHub Actions 里跑一套最小 CI下面是我现在用在项目里的一个极简 GitHub Actions 片段name: db-migration-check on: pull_request: jobs: lint: runs-on: ubuntu-latest services: mysql: image: mysql:8 env: MYSQL_ROOT_PASSWORD: pass MYSQL_DATABASE: ci_db ports: - 3306:3306 steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - name: Install Atlas run: | curl -sSf https://atlasgo.sh | sh - name: Run migrate lint env: CI_DB_URL: mysql://root:passlocalhost:3306/ci_db run: | atlas migrate lint \ --env ci \ --dev-url docker://mysql/8 \ --git-base origin/main注意这里多了一个--git-base origin/main它要求 checkout 时要有完整历史所以fetch-depth不能设为 1。这个细节我踩过一次写成 fetch-depth: 0 才能让 Atlas 拿到 main 分支和当前分支的迁移文件对比基础。如果你所在团队不用 GitHub也没关系因为atlas migrate lint就是一个 CLI 命令任何 CI 平台都能跑关键是让数据库迁移的变更检查变成自动化的一环。5. 我踩过的坑类型转换、外键约束、锁表与权限5.1 timestamp 和 datetime 的类型漂移第一次用 Atlas 生成 MySQL 迁移时我发现它总是给时间字段生成datetime(6)而我的线上库是timestamp(6)。反复 diff 之后才意识到MySQL 的CURRENT_TIMESTAMP在两种类型下行为不完全一致Atlas 为了避免误判默认会倾向更精确的类型。后来我把所有时间字段统一成了timestamp并显式在 HCL 里写了type timestamp迁移文件才干净。所以说声明式工具的 diff 结果和你的历史类型越一致生成的迁移就越没有意外惊喜。如果你接手的项目里类型本身已经很不规范先在声明文件里把规范定下来再逐步用一个个迁移去纠正。5.2 重命名字段会被识别成 drop add在 HCL 里给列改名实际上就是对schema.hcl删掉旧字段、加上新字段。Atlas 并不能读心知道你其实是rename它看到的是一次 destructive 的删除操作。如果你确实要重命名列建议先手动在迁移文件里写成RENAME COLUMN再同步更新 HCL避免自动生成的 drop/add 造成数据丢失。同样的道理适用于修改约束类型、交换索引顺序等操作。生成迁移文件后务必人工扫一遍那条 ALTER TABLE特别是DROP关键字出现的地方。5.3 大表加列该锁还是要锁Atlas 不会自动帮你用 pt-osc 或者 gh-ost 去做在线 DDL。在表数据量达到几百万甚至千万行时一个看似普通的ADD COLUMN在 MySQL 的 InnoDB 引擎下也可能导致长时间锁表。我现在的团队会这样做把 Atlas 生成的 SQL 提取出来交给专门的 DBA 脚本去执行或者把它拆进低峰期窗口。Atlas 生成迁移文件的意义在于让变更可预测、可 review它不负责替你规避数据库本身的执行机制。这一点必须提前想清楚别以为声明式工具能解决所有物理局限。5.4 数据库账号权限名不副实Atlas 需要相当高的权限才能完成 diff 和 apply。如果你用的是一个只拥有DML权限的账号去跑atlas schema apply会直接报权限不足而且报错信息不够直观得看完整日志才知道是哪一句 DDL 触发的。我的建议是单独为 Atlas 建一个专用账号GRANT ALL ON mydb.* TO atlas_user% IDENTIFIED BY your_password;如果你用 dev-url 连接的是 Docker 里临时启动的 MySQL用 root 问题不大但在生产环境或共享测试环境一定要给最小但足够的 DDL 权限保证它能做CREATE、ALTER、DROP、INDEX这些操作。5.5 dev 库版本必须和目标库一致Atlas 在生成迁移计划时会把 DDL 放到 dev 库里跑一遍。如果 dev 库是 MySQL 8.0线上库是 MySQL 5.7那么生成出来的 DDL 很可能是 8.0 的语法拿到 5.7 上执行就会失败。--dev-url不只是一个随便什么临时库这么简单它的版本、字符集、collation 最好和目标环境对齐。项目里我用的是docker://mysql/8和docker://postgres/15这种模板简单可靠。如果你们是自建机房的数据库就在 dev-url 里指向一台固定的开发实例保证版本一致。6. 选型参考Atlas、Flyway、Alembic、Sqitch 该怎么挑6.1 几种工具的思维方式对比工具核心方式迁移文件生成数据库漂移检测适用场景Atlas声明式期望状态 diff自动生成 HCL/SQL强跨环境项目、CI 要求高的团队Flyway版本化手写 SQL手写 代码生成工具弱Java 生态、较稳定的结构Alembic从 ORM 模型生成迁移自动生成 Python/SQL中Python/SQLAlchemy 项目Sqitch依赖式 SQL 变更集手写 SQL弱对单个数据库 SQL 有强依赖的团队Flyway 的核心抽象是第几个版本执行了什么 SQL它很成熟但没法回答线上库是不是真的和迁移脚本一致这个问题。Alembic 和 ORM 天然绑得紧模型即结构很方便但如果你的项目里存在大量手写的复杂 SQL、视图、存储过程用 Alembic 来管反而痛苦。Sqitch 更像是一个 SQL 管理框架依赖关系清晰但一切都得自己写。Atlas 的差异化优势在于它把数据库的真实状态当成了头等公民。你可以先 inspect 一个已有的旧库再让它成为你迁移史的唯一真源这是很实用的能力。6.2 什么情况下我会首选 Atlas我的判断标准很简单项目里表的数量超过了二十张每次发布都需要带着数据库变更一起走团队里没有专门的 DBA后端同学要自己维护结构你受够了手工写 DDL 和环境之间结构不一致的反复拉扯。满足其中两条以上就值得切到 Atlas。如果你只用单机 SQLite 做小工具开发或者项目从创建到现在数据库一直没变过那换不换都无所谓。工具是拿来解决问题的不是拿来赶时髦的。6.3 对选型的一点提醒Atlas 还在快速迭代有些高级特性比如权限模型、多云平台的 Terraform provider在不同版本里的 API 变化比较大。如果你决定在生产环境使用我的建议锁定 CLI 或 Docker 镜像的版本不要在发版前一天升级。把atlas.sum里的校验和视为安全敏感文件绝不允许其他人手工修改。在 CI 之外每周至少做一次在全新库上回放所有迁移的验证确保迁移文件本身是自洽的。最后再分享一个我自己的习惯无论 Atlas 生成的迁移计划看起来多合理我在合入前都会把生成出来的 SQL 从头到尾读一遍。lint 能帮我挡掉明显的高风险操作但业务含义只有人才能判断。数据库结构里藏着很多隐性的业务契约自动化工具再强也只是助手不是决策者。这套工作流目前在我手上维护的项目里跑了半年多最大的感受是从此没人再来问我哎测试库怎么少了张表了这大概就是数据库工程该有的状态。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询