在 Django 模型里配置外键,落点通常只有三件事:在“多”的一方写一个 ForeignKey 字段、把必填的 on_delete 写明确、生成并执行迁移让数据库层真正建出约束和索引。真正需要先想清楚的不是字段写法,而是两个模型之间是一对多还是一对一、父对象被删除时子对象怎么办、这个字段在已有表上是否允许为空。
适用场景:两个模型之间是“一个父对象对多条子记录”,需要跨表查询或定义删除行为。操作动作:在子模型上写 ForeignKey(父模型, on_delete=..., related_name=..., null=...),再执行 makemigrations 与 migrate。验证方式:在 shell 里建父子对象并反向查询,用 sqlmigrate 查看生成的 SQL。风险边界:on_delete 的实际语义、null 与数据库约束、已有数据的回填顺序,都需要结合自己的环境确认。
先确认当前场景和目标
动手之前先确认四件事,它们决定字段怎么写、迁移能不能一次跑通。
- 关联基数:一个作者对应多篇文章,就在文章模型上放外键;如果两个模型是一一对应,应该用
OneToOneField,而不是给外键加unique=True硬凑。 - 删除行为:父对象被删时,子记录是跟着删(CASCADE)、禁止删除(PROTECT)、置空(SET_NULL,要求字段可空)还是不做处理(DO_NOTHING)。这个选择要和业务规则对齐,事后改代价更大。
- 字段是否可空:新建表可以直接写
null=False;给已有数据的表加非空外键,通常需要先允许为空、回填数据、再收紧约束。 - 是否跨 app、是否循环导入:两个模型互相引用时,用字符串形式
"app.Author"指向目标模型,能避开模块加载顺序问题。
如果这四点里有任意一项还没定,先不要急着写迁移,否则后面多半要补一个额外的迁移来修正。

按这个顺序处理
第一步,在子模型上定义外键字段,把 on_delete 和 related_name 一并写清楚。下面是一个可替换的通用示例,把 Author、Article 换成自己的模型名即可:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=64)
class Article(models.Model):
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name="articles",
null=False,
blank=False,
)
title = models.CharField(max_length=200)
要点说明:on_delete 在该版本里是必填参数,不写会直接报错;related_name="articles" 让你能用 author.articles.all() 反向取数据,不写则默认是 article_set;如果明确不需要反向查询,可以写 related_name="+",避免命名冲突。
第二步,如果是在已有表上补外键,先写成可空并允许表单留空,迁移并回填后再改成非空:

# 阶段一:允许为空
author = models.ForeignKey(Author, on_delete=models.PROTECT, null=True, blank=True)
# 阶段二:数据回填完成后收紧
author = models.ForeignKey(Author, on_delete=models.PROTECT, null=False, blank=False)
第三步,生成并执行迁移:
python manage.py makemigrations app_name --dry-run
python manage.py sqlmigrate app_name 0002
python manage.py migrate
--dry-run 只打印将要生成的迁移,sqlmigrate 输出实际 DDL,先看这两步再执行 migrate,能提前发现字段非空、默认值缺失这类会失败的操作。

改完后怎么验证
验证分两层:先看迁移层,再看 ORM 行为层。
- 执行
python manage.py sqlmigrate app_name 0002,确认输出里包含指向父表的外键约束语句,以及为外键列建立的索引。 - 执行
python manage.py migrate,没有报错,再用python manage.py showmigrations app_name确认迁移标记为已应用。 - 在 shell 中建一组父子对象,验证正向、反向和外键跨表过滤:
from app_name.models import Author, Article
a = Author.objects.create(name="demo")
Article.objects.create(author=a, title="first")
print(a.articles.all()) # 反向查询,应能取到刚创建的文章
print(Article.objects.filter(author__name="demo")) # 跨表过滤,应命中同一条
预期的信号是:反向管理器返回对象,跨表过滤不额外报字段错误,删除父对象时行为符合设定——CASCADE 下子记录消失,PROTECT 下抛出 ProtectedError,SET_NULL 下外键列变为 NULL。如果删除父对象没有触发预期行为,问题多半出在 on_delete 写错或数据库里已经存在未清理的历史数据。
容易忽略的边界
- on_delete 由 ORM 执行:通过 QuerySet 删除、后台删除会走 Django 的处理逻辑,但直接执行原生 SQL 删除父表记录不会触发同样的清理,可能留下指向不存在行的外键。混用原生 SQL 的场景要自行保证一致性。
- db_constraint=False:这个参数会让数据库层不建外键约束,只在 ORM 层维持关系。它适合历史库兼容、跨库等受限场景,但代价是可能写入脏数据。没有明确理由时,建议保留默认值。
- null 与 blank 不是一回事:
null控制数据库列是否允许 NULL,blank控制表单和后台校验是否允许留空,二者常常需要同时设置。 - 大表加外键:在已有大表上增加外键列和索引,可能带来锁表和耗时问题,具体影响要结合所用数据库和执行窗口确认,必要时拆成多次迁移。
- 回退建议:迁移出问题时,可以用
python manage.py migrate app_name 0001回退到上一个迁移;如果线上数据已经写入,回退前先确认是否丢列。对暂时无法确认删除行为的场景,先选PROTECT这种保守选项,比CASCADE更容易在出错时发现。