【后端】【Django orm】多对多关系建议使用自定义中间表,避免语义不清晰

Djnago 的orm多对多关系自动生成时,models.ManyToManyField 可以定义在任意一方,但确实可能会导致模型的可读性变差,甚至让代码逻辑变得不直观。


多对多字段定义在哪一方?

Django 在任意一方 定义 ManyToManyField 都可以,但有些情况会导致模型语义不清晰,影响代码可读性。例如:

python 复制代码
class Student(models.Model):
    name = models.CharField(max_length=100)
    courses = models.ManyToManyField("Course")  # 定义在 Student 侧

    def __str__(self):
        return self.name

class Course(models.Model):
    title = models.CharField(max_length=200)

    def __str__(self):
        return self.title

上面代码等价于:

python 复制代码
class Course(models.Model):
    title = models.CharField(max_length=200)
    students = models.ManyToManyField("Student")  # 定义在 Course 侧

    def __str__(self):
        return self.title

无论 ManyToManyField 定义在 Student 还是 Course ,Django 都会自动创建一张中间表,效果是一样的。


为什么"任意一方"会感觉怪?

虽然 Django 允许 ManyToManyField 定义在任意一方,但有时候会让模型的语义不直观,例如:

  • 从业务逻辑来看,关系的主动性不同 。有些关系更容易被理解为谁拥有谁
  • 模型结构可能不符合直觉,让代码的可读性变差。

示例 1(好的设计:选课系统)

学生(Student)选择课程(Course) ,语义上 学生拥有课程 更合理,所以 ManyToManyField 应该定义在 Student 侧:

python 复制代码
class Student(models.Model):
    name = models.CharField(max_length=100)
    courses = models.ManyToManyField("Course", related_name="students")  # 推荐定义在 Student 侧

    def __str__(self):
        return self.name

class Course(models.Model):
    title = models.CharField(max_length=200)

    def __str__(self):
        return self.title

这样,在查询的时候:

python 复制代码
student.courses.all()  # 获取某个学生选的课程
course.students.all()  # 反向查询某门课程的学生

语义清晰,可读性更好。


示例 2(不直观的设计)

如果 ManyToManyField 定义在 Course 侧:

python 复制代码
class Student(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

class Course(models.Model):
    title = models.CharField(max_length=200)
    students = models.ManyToManyField("Student")  # 定义在 Course 侧

    def __str__(self):
        return self.title

在查询 student.courses.all() 时,Django 需要依赖反向查询 ,而 student 本身没有 courses 属性,会降低代码的直观性:

python 复制代码
student.courses.all()  ❌  # AttributeError: 'Student' object has no attribute 'courses'

student.course_set.all() ✅  # 需要反向查询

这种情况下,把 ManyToManyField 定义在 Student 侧更符合直觉。


什么时候用中间表?

多对多关系包含额外字段 (如选课成绩、加入时间),就不能使用 Django 自动生成的中间表,需要手动定义中间表

python 复制代码
class Student(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

class Course(models.Model):
    title = models.CharField(max_length=200)

    def __str__(self):
        return self.title

class Enrollment(models.Model):  # 手动中间表
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    grade = models.CharField(max_length=10, blank=True, null=True)  # 额外字段
    enrolled_at = models.DateTimeField(auto_now_add=True)

这样查询会更灵活:

python 复制代码
# 获取某个学生的所有选课记录
Enrollment.objects.filter(student=student).values("course__title", "grade")

总结

  1. ManyToManyField 可以定义在任意一方,但应该选择语义更清晰的那一方:

    • 推荐 :在"主动"关联的一方定义,比如 学生 -> 课程
    • 避免 :在关系不直观的一方定义,比如 课程 -> 学生
  2. 如果需要存储额外信息(如选课成绩) ,应该使用手动中间表

  3. 查询方便性

    • related_name="students" 可以让 course.students.all() 变得直观。
    • 直接用 ManyToManyField 自动生成的中间表,不需要手动管理关系表。

最佳实践 :如果你发现 ManyToManyField 定义的位置让代码逻辑变怪,就考虑是否换一方定义,或者改用手动中间表

相关推荐
接着奏乐接着舞。1 小时前
【2026】73道Redis 常见面试题与参考答案
数据库·redis·后端·缓存
其实防守也摸鱼2 小时前
权限提升与横向移动:从内网渗透到域控的完整技术图谱
运维·服务器·数据库·安全·github·copilot·渗透
人生百态,人生如梦2 小时前
每日论文解读(9.1)——ReToolSQL:面向鲁棒Text-to-SQL的Agentic强化学习与工具增强两阶段训练框架
数据库·sql
DevOpenClub3 小时前
网页采集如何稳定输出结构化数据:JSON、链接与快照三阶段流水线
数据库·json·api
其实防守也摸鱼3 小时前
免杀与持久化入门:从载荷免杀到隐蔽通道的完整指南
运维·服务器·数据库·安全·github·copilot·渗透
SelectDB3 小时前
统一全文检索与 SQL 分析:Apache Doris 日志分析实践
运维·数据库·掘金技术征文
麻瓜code3 小时前
【MySQL】SQL优化实战:从慢查询定位到查询改写
数据库·sql·mysql
蓝鸟19744 小时前
Oracle HIS业务视图优化实战|VI_BQ_YWTX11从高耗费到秒级响应
数据库·oracle·his数据库调优·复合索引踩坑·is null索引问题·ql视图优化
数智启示录4 小时前
Apache Doris 4.0.8 混合检索实战(第 5 篇):三套系统只返回两条结果,一条 SQL 如何避开交集丢失
大数据·数据库·经验分享·sql·面试
今天AI了吗4 小时前
大模型技术全景(一):AI、机器学习、深度学习,三者的关系到底是什么?一文搞懂
数据库·人工智能·python·sql·深度学习·机器学习·rust