关于 generate-migration
为 Sentry 后端开发者提供的 Django 数据库迁移编写指南,涵盖生成、审查和测试迁移,并说明安全删除字段与模型的分阶段流程。
主要功能
- 01生成常规或空白迁移
- 02审查生成的 SQL 与安全性
- 03测试数据回填迁移
- 04分阶段删除字段和模型
- 05解决迁移锁文件冲突
适用场景
- 01为模型变更生成迁移
- 02为数据转换编写迁移测试
- 03安全移除数据库字段或表
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ getsentry/ sentry --skill generate-migration安装后在 Claude Code 中输入/generate-migration确认已加载。
详细介绍
生成 Django 数据库迁移
命令
根据模型变更自动生成迁移:
sentry django makemigrations
针对特定 app:
sentry django makemigrations <app_name>
生成一个空迁移(用于数据迁移或自定义工作):
sentry django makemigrations <app_name> --empty
生成之后
- 如果添加了新模型,请确保它已在 app 的
__init__.py中导入 - 检查生成的迁移是否正确
- 运行
sentry django sqlmigrate <app_name> <migration_name>以验证 SQL - 使用
sentry django migrate <app_name>在本地应用该迁移 —— Sentry 的迁移框架会在应用时运行其安全检查,因此这能在 CI 之前捕获不安全的操作(缺少is_post_deployment、不安全的列变更等)。
在编辑生成的迁移时(例如将 DeleteModel 换成 SafeDeleteModel),请保留自动生成的 is_post_deployment 注释块。它记录了一个不明显的标志,并为未来的迁移作者提供了具体指导 —— 这是有用的上下文,而不是废话。只有当注释已过时或与代码相矛盾时才删除它。
不要测试 ORM
不要编写仅测试 Django ORM 的测试。标准操作 —— 创建/更新/删除、级联删除、唯一约束强制执行 —— 由 Django 和 Postgres 提供,并假定其正常工作。测试 你的 逻辑(业务规则、信号接收器、自定义管理器/验证),而不是框架的逻辑。
要测试数据迁移和回填
上述规则的例外:回填或转换数据的迁移属于你的逻辑,必须为其编写测试。使用来自 sentry.testutils.cases 的 TestMigrations 基类;测试位于 tests/sentry/migrations/ 中。
设置 app、migrate_from(在你之前的那个迁移)和 migrate_to(你的迁移)。在 setup_before_migration(self, apps) 中使用历史模型注册表(apps.get_model("sentry", "MyModel"))预置迁移前的行 —— 而不是直接 from sentry.models... 导入,因为当前模型可能与 migrate_from 时的 schema 不匹配。然后断言迁移后的状态。
只编写一个 test_* 方法。 setUp 会在 每个 测试方法上运行完整的向下迁移 → 预置数据 → 向上迁移循环,因此每多一个方法就会多付出一次往返开销,却没有增加任何覆盖率。要覆盖多个用例,请在 setup_before_migration 中预置所有用例,并在单个测试主体中逐一断言。
from sentry.testutils.cases import TestMigrations
class BackfillFooTest(TestMigrations):
app = "sentry"
migrate_from = "0123_before"
migrate_to = "0124_backfill_foo"
def setup_before_migration(self, apps):
Foo = apps.get_model("sentry", "Foo")
self.empty = Foo.objects.create(value=None)
self.already_set = Foo.objects.create(value="kept")
def test_backfill(self):
self.empty.refresh_from_db()
self.already_set.refresh_from_db()
assert self.empty.value == "expected"
assert self.already_set.value == "kept"
app 和 connection:app 是你正在测试其迁移的 Django app 标签 —— 默认为 "sentry",但当迁移位于某个 app 的 migrations/ 目录中时,请将其设置为例如 "workflow_engine"。connection 是数据库别名,默认为 "default";请将其设置为模型表实际所在的连接。两者都必须与迁移及其表实际所在的位置匹配,否则向上/向下迁移会针对错误的数据库运行。
使用 --migrations 和 --reuse-db 标志在本地运行这些测试。首次运行时,需要同时使用 --create-db 和 --reuse-db 来让数据库处于良好状态。
指南
历史模型与保存钩子
apps.get_model() 返回一个没有自定义 save() 方法的历史模型类。它发出的信号以该历史类作为发送者,因此作用域为当前模型的接收器(例如缓存失效钩子)不会运行。
在编写或审查数据迁移时,请检查当前模型的保存钩子,并显式执行所需的副作用。继续使用 apps.get_model();导入当前模型并不是安全的变通方法。
添加列
- 对于带默认值的列,使用
db_default=<value>而不是default=<value> - 可空列:使用
null=True - 非空列:必须设置
db_default
添加索引
对于大表,请在迁移上设置 is_post_deployment = True,因为创建索引可能会超过 5s 超时。
删除列
删除操作需要两个迁移。一次性把两个都写出来,但它们必须是 两个独立的 PR,其中阶段 2 堆叠在阶段 1 之上,以便其迁移依赖于阶段 1。请明确指出 在阶段 1 部署完成之前,阶段 2 不能合并 —— 将它们一起合并会在旧代码仍在运行时删除该列。
阶段 1 —— MOVE_TO_PENDING
按此顺序运行两次 makemigrations。一旦字段从模型上移除,Django 就无法再生成 AlterField,因此反过来操作会导致在缺少它的情况下静默发布。
- 在字段仍保留在模型上时,就地修改它:如果是 FK,添加
db_constraint=False;如果不可为 null 且没有db_default,则添加null=True。运行makemigrations以生成AlterField。 - 移除该字段以及所有引用它的代码,然后再次运行
makemigrations。将生成的RemoveField替换为SafeRemoveField(..., deletion_action=DeletionAction.MOVE_TO_PENDING)—— 这会移除 Django 状态,而不是该列。 - 将两者手动合并到一个迁移中。示例:
operations = [
migrations.AlterField(
model_name="testmodel",
name="project",
field=sentry.db.models.fields.foreignkey.FlexibleForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="sentry.project",
),
),
SafeRemoveField(
model_name="testmodel", name="project", deletion_action=DeletionAction.MOVE_TO_PENDING
),
]
阶段 2 —— DELETE(第二个 PR,在阶段 1 部署后合并)
makemigrations <app> --empty,然后使用相同的 SafeRemoveField,并设置 deletion_action=DeletionAction.DELETE。PR 中不包含其他内容。
移除模型(以及最终移除其表)
删除表需要两个迁移。一次性把两个都写出来,但它们必须是 两个独立的 PR,其中阶段 2 堆叠在阶段 1 之上,以便其迁移依赖于阶段 1。请明确指出 在阶段 1 部署完成之前,阶段 2 不能合并 —— 将它们一起合并会在旧代码仍在运行时删除该表。
首先,检查入站 FK。 如果其他表有指向此表的外键,那些列需要各自进行一遍“删除列”的操作,并且其两个阶段都必须在本次模型的阶段 1 合并之前部署完成。
阶段 1 —— MOVE_TO_PENDING
按此顺序运行两次 makemigrations。一旦模型被移除,Django 就无法再生成 AlterField,因此反过来操作会导致在缺少它们的情况下静默发布。
- 对该模型的每个出站 FK 字段,添加
db_constraint=False(对于HybridCloudForeignKey,则改用null=True),然后运行makemigrations以生成AlterField操作。 - 移除该模型及所有代码引用,再次运行
makemigrations,并将生成的DeleteModel替换为SafeDeleteModel(..., deletion_action=DeletionAction.MOVE_TO_PENDING)。 - 将两者合并到一个迁移中,
AlterField放在前面。 - 将该表添加到
src/sentry/db/router.py(或getsentry/db/router.py)中的historical_silo_assignments。选择该模型原本使用的 silo —— 通常是SiloMode.CELL。
删除约束不是可选项。这些表会存活到阶段 2,但 Django 不再知晓它们,因此无法级联到它们 —— 对存活下来的父表执行删除操作会因遗留的约束而失败。当一次性移除多个模型时,还要移除那些处于待删除状态的表之间的约束,这样阶段 2 的 DROP TABLE 顺序就无关紧要了。
阶段 2 —— DELETE(第二个 PR,在阶段 1 部署后合并)
makemigrations <app> --empty,然后使用相同的 SafeDeleteModel,并设置 deletion_action=DeletionAction.DELETE。保留 historical_silo_assignments 条目 —— 删除表的迁移需要它来解析 silo。
重命名列/表
不要在 Postgres 中重命名。使用 db_column 或 Meta.db_table 来保留旧名称。
解决合并冲突
如果 migrations_lockfile.txt 发生冲突:
bin/update-migration <migration_name>
这会重命名你的迁移、更新依赖并修复锁文件。