2.2 模型关系设计 本节摘要:真实业务里实体从不孤立。本节讲三种关系字段(一对多、多对多、一对一)的适用判断、ondelete 级联策略的选择、relatedname 的命名艺术,以及"关系要不要建模型"这个多对多进阶问题。关系设计错了,第 3 章的查询会立刻变得别扭。 三种关系,一个判断法 Django 提供三种关系字段,对应关系型数据库的三种经典结构。不用背定义,问自己两个问题就能定位: 一个 A 可以对应多个 B 吗?一个 B 可以对应多个 A 吗? 关系本身要不要携带额外信息? 墨迹博客的实体关系如下: ForeignKey(多对一):一篇文章属于一个分类,一个分类下有多篇——"多篇属于一个"就是外键。评论对文章同理。
本节摘要:真实业务里实体从不孤立。本节讲三种关系字段(一对多、多对多、一对一)的适用判断、on_delete 级联策略的选择、related_name 的命名艺术,以及"关系要不要建模型"这个多对多进阶问题。关系设计错了,第 3 章的查询会立刻变得别扭。
Django 提供三种关系字段,对应关系型数据库的三种经典结构。不用背定义,问自己两个问题就能定位:
墨迹博客的实体关系如下:
class Category(models.Model): name = models.CharField("分类名", max_length=50, unique=True) class Tag(models.Model): name = models.CharField("标签名", max_length=50, unique=True) class Article(models.Model): # ... 上一节的字段 ... category = models.ForeignKey( Category, on_delete=models.SET_NULL, null=True, blank=True, related_name="articles", ) tags = models.ManyToManyField(Tag, blank=True, related_name="articles") class Comment(models.Model): article = models.ForeignKey( Article, on_delete=models.CASCADE, related_name="comments" ) reviewer = models.CharField("评论者", max_length=50) content = models.TextField("评论内容") created_at = models.DateTimeField(auto_now_add=True)

外键必须回答"被引用的那行删了怎么办"。三种常用策略,语义截然不同:
墨迹博客全用 CASCADE 行不行?技术上可以,业务上是灾难:清理一个测试分类,几百篇文章被静默连带删除。选择策略的标准就一条:引用方在失去被引用方之后,还有没有独立存在的意义。有,就 SET_NULL 或 PROTECT;没有,才 CASCADE。
外键定义里的 related_name 决定"从对方查回来"的通道名。有了 related_name="articles",三种子句全部可用:
cat = Category.objects.get(name="Python") cat.articles.all() # 该分类下所有文章(反向查询) art = Article.objects.first() art.comments.count() # 该文章的评论数 Tag.objects.filter(articles__status=Article.PUBLISHED) # 跨关系过滤:所有挂着已发布文章的标签
命名建议用复数小写(articles、comments),读起来像自然语言。不写 related_name 时默认是"模型名小写加 _set",能用但可读性差一截。两个模型存在多条外键时(比如文章既有作者又有最后编辑,都指向用户表),related_name 必写且不能重名,否则迁移直接报错——这是新手建关系时最常见的报错之一。
普通多对多的中间表只有两列外键。一旦"关系"本身要携带信息——比如某文章打某标签的时间、点赞关系要记时间——就该用 through 显式建模:
class ArticleTag(models.Model): article = models.ForeignKey(Article, on_delete=models.CASCADE) tag = models.ForeignKey(Tag, on_delete=models.CASCADE) tagged_at = models.DateTimeField(auto_now_add=True) class Meta: constraints = [models.UniqueConstraint( fields=["article", "tag"], name="unique_article_tag")]
代价是添加关系不能再用 add 方法一把梭,要创建 ArticleTag 实例;收益是关系数据可查询、可校验、可扩展。判断标准:中间信息现在或可见未来会用吗?会,就趁早建 through,后补的迁移成本远高于现在。
关系设计的高频错误值得单独列一份清单,因为它们的共同点是当时看起来省事。反模式一:为了省一张表把多种实体塞进同一模型,用类型字段区分——文章和页面长得像就合并,半年后两者的字段差异越拉越大,模型里一半字段对一半记录永远为空,查询与校验全部被类型分支污染。反模式二:该用外键的用了字符字段存名字,图一时插入方便,等到分类改名,全表数据跟着失联。反模式三:多对多关系需要携带附加属性(比如谁在什么时候把文章加入了收藏)却仍然用默认的自动中间表,等发现要记录收藏时间时,只能推翻重建。对照检查这份清单的时机放在每次建模评审:任何一处先用字符串存着、先合并到一张表的提议,都值得多问一句一年后的代价。数据库世界里,结构的懒惰从来不会免费。
关系设计完,下一节把这些模型交给迁移系统,落成真实的数据库表。