<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Alembic | NullSpace</title><link>https://spacetimelab.cn/tag/alembic/</link><atom:link href="https://spacetimelab.cn/tag/alembic/index.xml" rel="self" type="application/rss+xml"/><description>Alembic</description><generator>Wowchemy (https://wowchemy.com)</generator><language>en-us</language><lastBuildDate>Mon, 19 Aug 2024 09:32:43 +0800</lastBuildDate><image><url>https://spacetimelab.cn/media/icon_hue03491371183284312b8b116a73ae5ab_79511_512x512_fill_lanczos_center_3.png</url><title>Alembic</title><link>https://spacetimelab.cn/tag/alembic/</link></image><item><title>轻量级数据库迁移工具: Alembic</title><link>https://spacetimelab.cn/post/alembic-for-database-migration/</link><pubDate>Mon, 19 Aug 2024 09:32:43 +0800</pubDate><guid>https://spacetimelab.cn/post/alembic-for-database-migration/</guid><description>&lt;h2 id="数据库表结构维护的痛点">数据库表结构维护的痛点&lt;/h2>
&lt;p>在缺乏专用迁移工具的环境下，团队对数据库结构的演进通常依赖手工脚本与临时操作，由此积累出一类共性工程问题。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">痛点&lt;/th>
&lt;th style="text-align:left">具体问题&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Schema 漂移&lt;/strong>（代码与库结构不一致）&lt;/td>
&lt;td style="text-align:left">本地可运行而生产环境报错；无人能准确描述生产库的真实结构&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>多环境同步&lt;/strong>&lt;/td>
&lt;td style="text-align:left">dev / staging / prod 结构各自为政，依赖手工维护的 SQL 脚本传递&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>回滚困难&lt;/strong>&lt;/td>
&lt;td style="text-align:left">结构变更出错后缺乏可靠的反向脚本，手动回滚极易遗漏&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>变更无版本 / 无审计&lt;/strong>&lt;/td>
&lt;td style="text-align:left">难以追溯&amp;quot;何人、何时、因何&amp;quot;修改了某个字段&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>团队协作冲突&lt;/strong>&lt;/td>
&lt;td style="text-align:left">多人并行修改表结构时，SQL 变更相互覆盖&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>DBA 不开放直连&lt;/strong>&lt;/td>
&lt;td style="text-align:left">生产库仅接受经审批的 SQL 脚本，不提供连接权限&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>事务安全&lt;/strong>&lt;/td>
&lt;td style="text-align:left">迁移中途失败，数据库结构处于半成品状态&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="迁移工具alembic如何解决上面痛点">迁移工具:Alembic如何解决上面痛点&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">痛点&lt;/th>
&lt;th style="text-align:left">无迁移工具时的典型困境&lt;/th>
&lt;th style="text-align:left">Alembic 的应对&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Schema 漂移&lt;/strong>（代码与库结构不一致）&lt;/td>
&lt;td style="text-align:left">本地可运行而生产环境报错；无人能准确描述生产库的真实结构&lt;/td>
&lt;td style="text-align:left">以模型为单一真相源，&lt;code>--autogenerate&lt;/code> 比对并生成差异&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>多环境同步&lt;/strong>&lt;/td>
&lt;td style="text-align:left">dev / staging / prod 结构各自为政，依赖手工维护的 SQL 脚本传递&lt;/td>
&lt;td style="text-align:left">&lt;code>alembic upgrade head&lt;/code> 在各环境幂等执行&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>回滚困难&lt;/strong>&lt;/td>
&lt;td style="text-align:left">结构变更出错后缺乏可靠的反向脚本，手动回滚极易遗漏&lt;/td>
&lt;td style="text-align:left">每个迁移自带 &lt;code>downgrade()&lt;/code>，支持可逆升级&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>变更无版本 / 无审计&lt;/strong>&lt;/td>
&lt;td style="text-align:left">难以追溯&amp;quot;何人、何时、因何&amp;quot;修改了某个字段&lt;/td>
&lt;td style="text-align:left">迁移文件纳入版本控制，天然携带作者、时间与说明&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>团队协作冲突&lt;/strong>&lt;/td>
&lt;td style="text-align:left">多人并行修改表结构时，SQL 变更相互覆盖&lt;/td>
&lt;td style="text-align:left">基于 UUID 的 DAG 版本图，支持分支与合并&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>DBA 不开放直连&lt;/strong>&lt;/td>
&lt;td style="text-align:left">生产库仅接受经审批的 SQL 脚本，不提供连接权限&lt;/td>
&lt;td style="text-align:left">&lt;code>alembic upgrade head --sql&lt;/code> 导出纯 SQL 交付 DBA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>事务安全&lt;/strong>&lt;/td>
&lt;td style="text-align:left">迁移中途失败，数据库结构处于半成品状态&lt;/td>
&lt;td style="text-align:left">默认在事务内执行（PostgreSQL / SQL Server 支持 DDL 事务）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://alembic.sqlalchemy.org/en/latest/" target="_blank" rel="noopener">alembic官方文档&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://github.com/sqlalchemy/alembic" target="_blank" rel="noopener">Github仓库&lt;/a>&lt;/p>
&lt;p>Alembic是SQLAlchemy的子项目，SQLAlchemy通过python类定义表结构、Python对象表示数据行、通过Python而非SQL写query且支持多数据库的问题；Alembic是数据库表结构的“时间机器”&lt;/p>
&lt;blockquote>
&lt;p>[!NOTE]&lt;/p>
&lt;p>Alembic 是**&amp;ldquo;面向 SQLAlchemy 但保持解耦&amp;rdquo;**的迁移工具：它读取 &lt;code>Base.metadata&lt;/code>（模型声明）以生成迁移脚本，但脚本本身是独立的 Python 模块，可脱离 ORM 运行时单独执行，亦可导出为标准 SQL。换言之，它与 SQLAlchemy 是&amp;quot;强协作、弱绑定&amp;quot;的关系。&lt;/p>
&lt;/blockquote>
&lt;h4 id="核心功能">核心功能&lt;/h4>
&lt;ul>
&lt;li>对数据库发起ALTER语句以修改表结构&lt;/li>
&lt;li>为系统构造迁移脚本，每个脚本表明一系列步骤用来将目标数据库升级为新版本；类似地也可以将数据库降级到某个版本&lt;/li>
&lt;li>允许脚本以某种顺序执行&lt;/li>
&lt;/ul>
&lt;h4 id="目标">目标&lt;/h4>
&lt;ul>
&lt;li>对事务型DDL完全支持&lt;/li>
&lt;li>最小脚本构建&lt;/li>
&lt;/ul>
&lt;h3 id="关键技术原理">关键技术原理&lt;/h3>
&lt;h4 id="版本即-dag有向无环图">版本即 DAG（有向无环图）&lt;/h4>
&lt;p>每个迁移文件头部声明两段核心元数据：&lt;/p>
&lt;pre tabindex="0">&lt;code>revision = &amp;#34;a1b2c3d4e5f6&amp;#34; # 本迁移的唯一标识（默认 12 位十六进制）
down_revision = &amp;#34;z9y8x7w6v5u4&amp;#34; # 前驱迁移的标识（可为 None 或列表）
&lt;/code>&lt;/pre>&lt;p>全部迁移文件据此连成一条链（或多条分支）。&lt;code>alembic upgrade head&lt;/code> 即沿 &lt;code>down_revision&lt;/code> 链推进至末端。数据库中另有一张极小的 &lt;code>alembic_version&lt;/code> 表，仅记录&lt;strong>当前已应用的 revision&lt;/strong>，Alembic 据此判定&amp;quot;应从何处继续推进&amp;quot;&lt;/p>
&lt;h4 id="autogenerate自动生成原理">autogenerate（自动生成）原理&lt;/h4>
&lt;p>&lt;a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html#" target="_blank" rel="noopener">Auto Generating Migrations&lt;/a>&lt;/p>
&lt;p>工作机制为：&lt;/p>
&lt;ol>
&lt;li>借助 SQLAlchemy 的 &lt;strong>inspect&lt;/strong> 反射目标数据库当前 schema；&lt;/li>
&lt;li>读取代码中 &lt;code>Base.metadata&lt;/code> 声明的&amp;quot;期望 schema&amp;quot;；&lt;/li>
&lt;li>二者求差，将差异渲染为一组 &lt;code>op.xxx()&lt;/code> 指令并写入新迁移文件；&lt;/li>
&lt;li>&lt;strong>开发者须在应用前对生成结果进行人工审查&lt;/strong>——其输出应被视为待审的初始草案，而非可直接投产的终稿。&lt;/li>
&lt;/ol>
&lt;p>会检测的：&lt;/p>
&lt;ul>
&lt;li>表的增加和移除&lt;/li>
&lt;li>列的增加和移除&lt;/li>
&lt;li>列上nullable状态的改变&lt;/li>
&lt;li>索引和显式命名的唯一约束的改变&lt;/li>
&lt;li>外键约束的改变&lt;/li>
&lt;li>CHECK约束的增加和移除&lt;/li>
&lt;/ul>
&lt;p>可选择性检测的：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>列类型的改变&lt;/p>
&lt;p>&lt;a href="https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.environment.EnvironmentContext.configure.params.compare_type" target="_blank" rel="noopener">&lt;code>EnvironmentContext.configure.compare_type&lt;/code>&lt;/a>设置成False则不检测，默认为True&lt;/p>
&lt;/li>
&lt;li>
&lt;p>server default的改变&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>​ &lt;a href="https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.environment.EnvironmentContext.configure.params.compare_server_default" target="_blank" rel="noopener">&lt;code>EnvironmentContext.configure.compare_server_default&lt;/code>&lt;/a>参数控制，默认为False&lt;/p>
&lt;p>不检测的：&lt;/p>
&lt;ul>
&lt;li>表名的改变&lt;/li>
&lt;li>列名的改变&lt;/li>
&lt;li>匿名约束&lt;/li>
&lt;li>特定的SQLAlchemy类型比如Enum（有的数据库后端不直接支持Enum类型）&lt;/li>
&lt;/ul>
&lt;h4 id="op-指令与事务">op 指令与事务&lt;/h4>
&lt;p>迁移文件中调用的是 &lt;code>alembic.op&lt;/code>（Operations）模块：&lt;code>op.create_table&lt;/code>、&lt;code>op.add_column&lt;/code>、&lt;code>op.alter_column&lt;/code>、&lt;code>op.create_index&lt;/code>、&lt;code>op.execute(&amp;quot;RAW SQL&amp;quot;)&lt;/code> 等。它们生成的是最小化的 DDL，&lt;strong>无需重新声明整张表结构&lt;/strong>。&lt;/p>
&lt;p>关键约束：默认整段迁移包裹于单一事务内（PostgreSQL、SQL Server 支持 DDL 事务），中途失败可自动回滚。但&lt;strong>部分操作（如 Postgres 的 &lt;code>ALTER TYPE ... ADD VALUE&lt;/code>、&lt;code>CREATE INDEX CONCURRENTLY&lt;/code>）无法在事务内执行&lt;/strong>，须以 &lt;code>op.execute(&amp;quot;COMMIT&amp;quot;)&lt;/code> 显式退出事务，或借助 &lt;code>execute_if&lt;/code> 有条件地执行。&lt;/p>
&lt;h4 id="离线-sql-模式">离线 SQL 模式&lt;/h4>
&lt;p>&lt;code>alembic upgrade head --sql &amp;gt; migration.sql&lt;/code> 不连接数据库，而是将所有 DDL 打印为标准 SQL 文本。这是对接&amp;quot;DBA 审批流 / 仅交付 SQL 不开放连接&amp;quot;这类企业约束的关键能力。&lt;/p>
&lt;h4 id="render_as_batchsqlite-的补偿机制">render_as_batch（SQLite 的补偿机制）&lt;/h4>
&lt;p>SQLite 几乎不支持 &lt;code>ALTER COLUMN&lt;/code>。检测到 SQLite 时，Alembic 启用 &lt;strong>batch 模式&lt;/strong>：新建表 → 拷贝数据 → 删除旧表 → 重命名，以此透明地完成&amp;quot;改列&amp;quot;语义。在 &lt;code>env.py&lt;/code> 中设置 &lt;code>render_as_batch=True&lt;/code> 即可启用。&lt;/p>
&lt;h3 id="如何使用alembic管理数据库表结构版本">如何使用Alembic管理数据库表结构版本&lt;/h3>
&lt;p>&lt;a href="https://alembic.sqlalchemy.org/en/latest/tutorial.html" target="_blank" rel="noopener">Tutorials&lt;/a>&lt;/p>
&lt;h4 id="安装">安装&lt;/h4>
&lt;pre tabindex="0">&lt;code>uv add --dev alembic
&lt;/code>&lt;/pre>&lt;pre tabindex="0">&lt;code>positional arguments:
{branches,check,current,downgrade,edit,ensure_version,heads,history,init,list_templates,merge,revision,show,stamp,upgrade}
branches Show current branch points.
check Check if revision command with autogenerate has pending upgrade ops.
current Display the current revision for a database.
downgrade Revert to a previous version.
edit Edit revision script(s) using $EDITOR.
ensure_version Create the alembic version table if it doesn&amp;#39;t exist already .
heads Show current available heads in the script directory.
history List changeset scripts in chronological order.
init Initialize a new scripts directory.
list_templates List available templates.
merge Merge two revisions together. Creates a new migration file.
revision Create a new revision file.
show Show the revision(s) denoted by the given symbol.
stamp &amp;#39;stamp&amp;#39; the revision table with the given revision; don&amp;#39;t run any migrations.
upgrade Upgrade to a later version.
options:
-h, --help show this help message and exit
--version show program&amp;#39;s version number and exit
-c CONFIG, --config CONFIG
Alternate config file; defaults to value of ALEMBIC_CONFIG environment variable, or
&amp;#34;alembic.ini&amp;#34;. May also refer to pyproject.toml file. May be specified twice to reference both
files separately
-n NAME, --name NAME Name of section in .ini file to use for Alembic config (only applies to configparser config, not
toml)
-x X Additional arguments consumed by custom env.py scripts, e.g. -x setting1=somesetting -x
setting2=somesetting
--raiseerr Raise a full stack trace on error
-q, --quiet Do not log to std output.
&lt;/code>&lt;/pre>&lt;h4 id="标准流程">标准流程&lt;/h4>
&lt;h5 id="init初始化">init：初始化&lt;/h5>
&lt;p>创建通用模版&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">alembic init
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>生成的关键文件：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">文件&lt;/th>
&lt;th style="text-align:left">职责&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">&lt;code>alembic.ini&lt;/code>&lt;/td>
&lt;td style="text-align:left">全局配置（数据库连接串、脚本路径等）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;code>migrations/env.py&lt;/code>&lt;/td>
&lt;td style="text-align:left">&lt;strong>核心&lt;/strong>：定义连接方式、&lt;code>target_metadata&lt;/code> 获取与事务策略&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;code>migrations/script.py.mako&lt;/code>&lt;/td>
&lt;td style="text-align:left">迁移文件模板（决定每个新文件的骨架）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;code>migrations/versions/&lt;/code>&lt;/td>
&lt;td style="text-align:left">全部迁移脚本的存放目录&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>查看有哪些模版：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">alembic list_templates
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre tabindex="0">&lt;code>alembic init --template multidb
&lt;/code>&lt;/pre>&lt;h5 id="将模型接入envpy">将模型接入env.py&lt;/h5>
&lt;pre tabindex="0">&lt;code># migrations/env.py 中指定 target_metadata
from myapp.models import Base
target_metadata = Base.metadata
# 在 run_migrations_online() 的 context.configure 中开启更敏锐的探测
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True, # 检测列类型变化（默认关闭）
compare_server_default=True, # 检测 server_default 变化（默认关闭）
render_as_batch=True, # SQLite 兼容
)
&lt;/code>&lt;/pre>&lt;blockquote>
&lt;p>[!NOTE]&lt;/p>
&lt;p>&lt;strong>常见坑：Base.metadata为空&lt;/strong>,数据库里表实际存在，导致得到的迁移脚本upgrade为删除表结构。&lt;/p>
&lt;p>原因：未在env.py导入相关表的ORM模型，只导入了Base。&lt;/p>
&lt;p>根因：Alembic autogenerate的工作方式：&lt;strong>把&amp;quot;数据库当前 schema（通过 SQLAlchemy 反射得到）&amp;ldquo;与&amp;quot;你的 &lt;code>Base.metadata&lt;/code>（Python 模型声明）&amp;ldquo;做一次结构化 diff。&lt;/strong>&lt;/p>
&lt;p>但Alembic &lt;strong>不会扫描你的项目结构&lt;/strong>。原因在于它要保持 ORM 无关、项目结构无关——它不知道你的模型叫 &lt;code>models.py&lt;/code> 还是 &lt;code>schema/&lt;/code> 目录，也不知道你用的是哪个 &lt;code>Base&lt;/code>。所以它把&amp;quot;把哪个 metadata 交给它&amp;quot;这件事完全交给你手动接线。&lt;/p>
&lt;p>而SQLAlchemy 的 declarative 模型，本质是&lt;strong>类定义时自动执行的注册动作&lt;/strong>：Base.metadata.tables[&amp;rdquo;&amp;hellip;&amp;rdquo;] = Table(&amp;hellip;)。&lt;/p>
&lt;p>所以只有导入具体的ORM模型，才会往Base注册表的元数据，从而被alembic用于和真实库表结构进行对比。&lt;/p>
&lt;/blockquote>
&lt;h5 id="标准工作流">标准工作流&lt;/h5>
&lt;pre tabindex="0">&lt;code># 1) 修改 SQLAlchemy 模型后，先确保数据库处于最新状态
alembic upgrade head
# 2) 让 Alembic 比对模型与库结构，生成候选迁移
alembic revision --autogenerate -m &amp;#34;add user status column&amp;#34;
# 3) ★ 务必打开生成文件进行人工审查（参见第 6 节风险点）
vim migrations/versions/xxxx_add_user_status_column.py
# 4) 应用迁移
alembic upgrade head
# 常用诊断命令
alembic current # 查看数据库当前所处的 revision
alembic history --verbose# 查看迁移链
alembic heads # 查看是否存在多个头（多即分支冲突）
alembic downgrade -1 # 回退一个版本
alembic upgrade head --sql &amp;gt; out.sql # 导出 SQL 交付 DBA
&lt;/code>&lt;/pre>&lt;blockquote>
&lt;p>[!WARNING]&lt;/p>
&lt;p>常见陷阱：&lt;/p>
&lt;p>① 未先执行 &lt;code>upgrade head&lt;/code> 便运行 &lt;code>--autogenerate&lt;/code>，导致将他人已应用的变更重复生成；&lt;/p>
&lt;p>② 未开启 &lt;code>compare_type=True&lt;/code>，致使 &lt;code>String(50)→String(100)&lt;/code> 之类变更被静默忽略；&lt;/p>
&lt;p>③ 直接信任 autogenerate 输出、未经审查即上线。&lt;/p>
&lt;/blockquote>
&lt;h4 id="场景一个项目多个库">场景：一个项目多个库&lt;/h4>
&lt;h4 id="最佳实践">最佳实践&lt;/h4>
&lt;h5 id="数据库配置管理">数据库配置管理&lt;/h5>
&lt;p>默认生成的在alembic.ini模版文件中需要配置sqlalchemy.url，但如果alembic.ini要纳入版本管理，则不适合显式配置数据库用户密码。&lt;/p>
&lt;p>开发环境，可以直接删掉 &lt;code>sqlalchemy.url&lt;/code> 行，强制从环境变量取：&lt;/p>
&lt;pre tabindex="0">&lt;code># env.py —— 不依赖 ini 里的 url
database_url = os.environ[&amp;#34;DATABASE_URL&amp;#34;] # 缺失则直接报错，fail-fast
config.set_main_option(&amp;#34;sqlalchemy.url&amp;#34;, database_url)
&lt;/code>&lt;/pre>&lt;blockquote>
&lt;p>[!TIP]&lt;/p>
&lt;p>对于生产环境，CI（GitHub Actions / GitLab CI）里把 &lt;code>DATABASE_URL&lt;/code> 配成&lt;strong>加密变量 / secret&lt;/strong>，运行时注入，URL 永远不落盘到仓库。&lt;/p>
&lt;/blockquote>
&lt;h5 id="生成与审查">生成与审查&lt;/h5>
&lt;ul>
&lt;li>每次仅承载一个&lt;strong>逻辑变更&lt;/strong>，迁移文件宜小、宜单一职责&lt;/li>
&lt;li>&lt;code>--autogenerate&lt;/code> 的产物始终视为&lt;strong>初始草案&lt;/strong>，&lt;strong>审查者须逐行核对&lt;/strong> drop/add 是否实为 rename&lt;/li>
&lt;li>开启 &lt;code>compare_type&lt;/code> 与 &lt;code>compare_server_default&lt;/code>&lt;/li>
&lt;li>为迁移赋予有意义的 &lt;code>-m&lt;/code> 说明&lt;/li>
&lt;li>在 CI 中引入&amp;quot;漂移检测&amp;quot;：模型与库不一致即令构建失败（见下方示例）&lt;/li>
&lt;/ul>
&lt;h5 id="生产安全">生产安全&lt;/h5>
&lt;ul>
&lt;li>大表新增 NOT NULL 列：先以 nullable + 数据回填，再置 NOT NULL，或附加 &lt;code>server_default&lt;/code>&lt;/li>
&lt;li>索引用 &lt;code>CREATE INDEX CONCURRENTLY&lt;/code> 规避锁表（需退出事务）&lt;/li>
&lt;li>采用 &lt;strong>expand-contract&lt;/strong> 模式实现零停机：先加新结构、保持代码双兼容、再移除旧结构&lt;/li>
&lt;li>downgrade 仅作为&amp;quot;结构回滚&amp;quot;的安全网，&lt;strong>数据迁移不可逆&lt;/strong>&lt;/li>
&lt;li>CI 强制&lt;strong>单一 head&lt;/strong>：&lt;code>test $(alembic heads | wc -l) -eq 1&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>CI 漂移检测（强烈建议）&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code>def test_no_drift(engine):
from alembic.autogenerate import compare_metadata
from alembic.migration import MigrationContext
with engine.connect() as conn:
ctx = MigrationContext.configure(conn)
diff = compare_metadata(ctx, Base.metadata)
assert diff == [], f&amp;#34;Schema drift: {diff}&amp;#34;
&lt;/code>&lt;/pre>&lt;p>该测试可强制&amp;quot;模型变更必须配套相应迁移&amp;quot;，从源头杜绝代码与数据库悄然脱节。&lt;/p>
&lt;h4 id="常见的坑">常见的坑&lt;/h4>
&lt;p>Python 在&lt;strong>执行这个类定义&lt;/strong>时，&lt;code>DeclarativeMeta&lt;/code>（声明式元类）会顺手做一件事：&lt;code>Base.metadata.tables[&amp;quot;mcp_audit_logs&amp;quot;] = Table(...)&lt;/code>。也就是说——&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>&lt;code>Base.metadata.tables&lt;/code> 这个字典，是靠&amp;quot;模型模块被 import&amp;quot;这个副作用逐步填进去的，不是靠 Alembic 去读你的 &lt;code>.py&lt;/code> 源文件。&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;p>关键点：&lt;strong>&lt;code>Base&lt;/code> 这个类的对象本身是空的&lt;/strong>。&lt;code>from myapp.db import Base&lt;/code> 只把&amp;quot;模具&amp;quot;拿过来，并不生产任何表。只有当你真正 import 了&lt;strong>定义模型的那个模块&lt;/strong>，类体才会执行、表才会登记进 &lt;code>metadata&lt;/code>。&lt;/p>
&lt;p>Alembic 的 autogenerate 对比的是**内存里的 &lt;code>target_metadata&lt;/code>（你给它的那个 metadata 对象）*&lt;em>和*&lt;em>数据库反射回来的 schema&lt;/em>&lt;/em>。如果你的模型模块在 Alembic 进程里从没被 import，那么 &lt;code>target_metadata.tables&lt;/code> 里就根本没有 &lt;code>mcp_audit_logs&lt;/code> → 反射看到库里有、metadata 里没有 → 判定为&amp;quot;多余&amp;quot; → &lt;code>upgrade()&lt;/code> 生成 DROP。这就是你上一轮脚本反着来的完整因果链。&lt;/p>
&lt;h2 id="类似方案与优劣对比">类似方案与优劣对比&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">方案&lt;/th>
&lt;th style="text-align:left">语言/生态&lt;/th>
&lt;th style="text-align:left">自动生成&lt;/th>
&lt;th style="text-align:left">DB 无关性&lt;/th>
&lt;th style="text-align:left">数据迁移&lt;/th>
&lt;th style="text-align:left">学习曲线&lt;/th>
&lt;th style="text-align:left">典型适用&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Alembic&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Python / SQLAlchemy&lt;/td>
&lt;td style="text-align:left">强（autogenerate）&lt;/td>
&lt;td style="text-align:left">中（绑定 SQLAlchemy 模型）&lt;/td>
&lt;td style="text-align:left">支持&lt;/td>
&lt;td style="text-align:left">中&lt;/td>
&lt;td style="text-align:left">FastAPI / Flask 等非 Django 的 Python 项目&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Django Migrations&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Python / Django&lt;/td>
&lt;td style="text-align:left">强（内置）&lt;/td>
&lt;td style="text-align:left">低（强绑定 Django ORM）&lt;/td>
&lt;td style="text-align:left">支持&lt;/td>
&lt;td style="text-align:left">低&lt;/td>
&lt;td style="text-align:left">Django 项目（开箱即用，无需额外引入）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Flyway&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Java / CLI / 多语言&lt;/td>
&lt;td style="text-align:left">无（纯手写 SQL）&lt;/td>
&lt;td style="text-align:left">高（纯 SQL，跨任意 DB）&lt;/td>
&lt;td style="text-align:left">支持（依托 SQL）&lt;/td>
&lt;td style="text-align:left">低&lt;/td>
&lt;td style="text-align:left">多语言团队、企业级、DBA 主导、需严格 SQL 管控&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Liquibase&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Java / XML·YAML·JSON·SQL&lt;/td>
&lt;td style="text-align:left">部分（可 diff）&lt;/td>
&lt;td style="text-align:left">高（企业级多 DB）&lt;/td>
&lt;td style="text-align:left">强&lt;/td>
&lt;td style="text-align:left">高（概念体系庞大）&lt;/td>
&lt;td style="text-align:left">大型组织、复杂回滚、需审计与多格式&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Yoyo&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Python（轻量）&lt;/td>
&lt;td style="text-align:left">无&lt;/td>
&lt;td style="text-align:left">高（纯 SQL/Python）&lt;/td>
&lt;td style="text-align:left">支持&lt;/td>
&lt;td style="text-align:left">低&lt;/td>
&lt;td style="text-align:left">不愿引入 SQLAlchemy 依赖的小型 Python 项目&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Atlas (Ariga)&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Go / HCL·SQL&lt;/td>
&lt;td style="text-align:left">强（声明式，schema 即真相）&lt;/td>
&lt;td style="text-align:left">高&lt;/td>
&lt;td style="text-align:left">支持&lt;/td>
&lt;td style="text-align:left">中&lt;/td>
&lt;td style="text-align:left">声明式管理、Kubernetes / 云原生、希望摆脱手写顺序迁移&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>dbmate&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Go / 单一 SQL&lt;/td>
&lt;td style="text-align:left">无&lt;/td>
&lt;td style="text-align:left">高&lt;/td>
&lt;td style="text-align:left">支持&lt;/td>
&lt;td style="text-align:left">低&lt;/td>
&lt;td style="text-align:left">极简、多语言团队、仅需&amp;quot;可执行的 SQL 迁移&amp;quot;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Rails / ActiveRecord&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Ruby / Rails&lt;/td>
&lt;td style="text-align:left">强（内置）&lt;/td>
&lt;td style="text-align:left">低（绑定 Rails）&lt;/td>
&lt;td style="text-align:left">支持&lt;/td>
&lt;td style="text-align:left">低&lt;/td>
&lt;td style="text-align:left">Ruby on Rails 项目&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="延伸阅读">延伸阅读&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">资料&lt;/th>
&lt;th style="text-align:left">聚焦主题&lt;/th>
&lt;th style="text-align:left">价值与说明&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Alembic 官方文档&lt;/strong> &lt;a href="https://alembic.sqlalchemy.org/en/latest/" target="_blank" rel="noopener">https://alembic.sqlalchemy.org/en/latest/&lt;/a>&lt;/td>
&lt;td style="text-align:left">总览、Tutorial、Autogenerate&lt;/td>
&lt;td style="text-align:left">权威一手来源，覆盖完整。偏 reference 风格，需自行串联为实践路径。&lt;strong>必读&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Alembic Cookbook&lt;/strong> &lt;a href="https://alembic.sqlalchemy.org/en/latest/cookbook.html" target="_blank" rel="noopener">https://alembic.sqlalchemy.org/en/latest/cookbook.html&lt;/a>&lt;/td>
&lt;td style="text-align:left">离线迁移、多租户、自定义版本表等进阶配方&lt;/td>
&lt;td style="text-align:left">官方进阶手册，解答&amp;quot;标准教程未覆盖&amp;quot;的真实场景。&lt;strong>必读&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Alembic Cheatsheet 03 — Autogenerate&lt;/strong> &lt;a href="https://blog.rajpoot.dev/cheatsheets/alembic/03-autogenerate-cheatsheet" target="_blank" rel="noopener">https://blog.rajpoot.dev/cheatsheets/alembic/03-autogenerate-cheatsheet&lt;/a>&lt;/td>
&lt;td style="text-align:left">autogenerate 检测/漏检清单、env.py 配置、CI 漂移测试&lt;/td>
&lt;td style="text-align:left">对 autogenerate 的能与不能做了系统化梳理，并给出可复用的漂移检测代码。&lt;strong>必读&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Why Your Alembic Migrations Work Locally and Wreck Prod&lt;/strong> &lt;a href="https://krun.pro/alembic-migrations" target="_blank" rel="noopener">https://krun.pro/alembic-migrations&lt;/a>&lt;/td>
&lt;td style="text-align:left">rename 陷阱、downgrade 数据不可逆、alembic_version 不匹配&lt;/td>
&lt;td style="text-align:left">以生产事故视角剖析&amp;quot;本地通过、线上翻车&amp;quot;的根因。&lt;strong>必读&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Zero-Downtime Schema Changes with Alembic&lt;/strong> &lt;a href="https://timderzhavets.com/blog/zero-downtime-schema-changes-with-alembic-a-production" target="_blank" rel="noopener">https://timderzhavets.com/blog/zero-downtime-schema-changes-with-alembic-a-production&lt;/a>&lt;/td>
&lt;td style="text-align:left">expand-contract 模式、CONCURRENTLY、迁移审查清单&lt;/td>
&lt;td style="text-align:left">系统讲解零停机迁移的工程方法，弥补本文在&amp;quot;大规模生产演进&amp;quot;上的篇幅限制。&lt;strong>必读&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Fix: Alembic Not Working&lt;/strong> &lt;a href="https://fixdevs.com/blog/alembic-not-working" target="_blank" rel="noopener">https://fixdevs.com/blog/alembic-not-working&lt;/a>&lt;/td>
&lt;td style="text-align:left">排错向：compare_type、多 head、自定义类型 / ENUM&lt;/td>
&lt;td style="text-align:left">以&amp;quot;故障—修复&amp;quot;结构组织，适合作为踩坑时的检索手册。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table></description></item></channel></rss>