Django基础教程(三十七)Django模型关系字段之多对多字段:Django模型“朋友圈”大揭秘:多对多字段,你的数据牵线月老!

嘿,伙计们!今天咱们来深度扒一扒Django模型关系中那位最擅长“牵线搭桥”的社交达人——多对多字段

想象一下这个场景:你是一个学生,你可以选多门课,比如《摸鱼学导论》、《高效划水实战》;同样,每一门课底下也有一大群像你一样嗷嗷待哺的同学。这种“我中有你,你中有我”的复杂关系,在数据库里怎么存?

如果用一张表硬存,你得在学生表里重复填课程信息,或者在课程表里重复填学生信息,结果就是数据冗余,混乱得像个永远理不清的毛线团。

别慌!Django的**ManyToManyField**就是为此而生的!它就像一个神通广大的“月老”,在你的数据之间建立了一个“朋友圈”,让它们自由地、优雅地发生关系。

第一幕:月老的基础技能——自动中间表(懒人模式)

有时候,你只需要知道“谁和谁有关系”,而不关心他们是什么时候、在什么情况下认识的。这种纯粹的关系,就用最简单的模式。

让我们用最经典的“文章(Post)”和“标签(Tag)”模型来开场。

# models.py
from django.db import models

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True, verbose_name="标签名")
    
    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200, verbose_name="文章标题")
    content = models.TextField(verbose_name="文章内容")
    # 看这里!核心角色登场!
    tags = models.ManyToManyField(Tag, verbose_name="标签")
    
    def __str__(self):
        return self.title

看!在Post模型里,我们只潇洒地写了一行:tags = models.ManyToManyField(Tag)。Django看到这行代码,会在背后偷偷做一件大事:

  1. 创建Post表来存文章信息。
  2. 创建Tag表来存标签信息。
  3. 自动创建一个隐藏的中间表(默认名是app名_Post_tags),这个表只有三个字段:id, post_id, tag_id

这个中间表就是个纯粹的“关系记录本”,只负责记下“哪篇文章(post_id)”和“哪个标签(tag_id)”是好朋友。你完全不用管它,Django会自动维护。

实战操作一下(进入Django Shell的奇妙世界):

python manage.py shell
# 1. 创建几个标签
>>> tag_python = Tag.objects.create(name="Python")
>>> tag_django = Tag.objects.create(name="Django")
>>> tag_funny = Tag.objects.create(name="搞笑")

# 2. 创建一篇文章
>>> post = Post.objects.create(title="我的第一篇博客", content="今天学习了Django,真有意思!")

# 3. 核心操作:给文章添加标签!这就像给朋友圈照片加Tag。
>>> post.tags.add(tag_python, tag_django)
# 看,月老已经把线和文章、标签牵好了!

# 4. 查询:这篇文章有哪些标签?
>>> post.tags.all()
<QuerySet [<Tag: Python>, <Tag: Django>]>

# 5. 反向查询:被打上“Python”标签的文章有哪些?
>>> tag_python.post_set.all()
# 注意:默认反向关系名是 `模型名_set`,这里是 `post_set`
<QuerySet [<Post: 我的第一篇博客>]>

# 如果你想给反向关系起个更优雅的名字,可以用 `related_name`
# 在ManyToManyField里加上 related_name=‘posts’
# 然后就可以这样查了:
# >>> tag_python.posts.all()

常用操作命令(你的月老红线手册):

  • add(*objs): 添加关系。可以传一个或多个对象。
  • remove(*objs): 移除关系。
  • clear(): 清空所有关系。
  • set(objs): 设置关系集合(会覆盖旧的)。这招特别有用,比如前端传来一组新的标签ID,直接post.tags.set(new_tag_list)就全更新了。
  • create(**kwargs): 直接创建一个新的Tag对象并添加到关系中去。

这种模式,简单、省心,适合绝大多数“只需关系,不问细节”的场景。


第二幕:月老的进阶技能——自定义中间表(高级玩家模式)

但是!生活不是童话,关系往往没那么纯粹。如果你想记录更多关于这段“关系”本身的细节,怎么办?

比如,在“学生&课程”的关系里,你想记录学生的成绩选课日期。这个成绩和日期,既不属于学生,也不属于课程,而是属于“学生选修某门课程”这个关系本身

这时候,自动创建的中间表就无能为力了。我们必须亲自下场,定义一个“加强版”的中间表模型!

华丽升级的模型代码:

# models.py
from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=100)
    
    def __str__(self):
        return self.name

class Course(models.Model):
    name = models.CharField(max_length=100)
    
    def __str__(self):
        return self.name

# 看!我们自定义的“关系详情”中间表模型
class Enrollment(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    # 下面是关系本身的额外信息
    score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True, verbose_name="成绩")
    enrolled_at = models.DateTimeField(auto_now_add=True, verbose_name="选课时间")
    
    class Meta:
        # 联合唯一约束,防止一个学生重复选同一门课
        unique_together = ('student', 'course')

# 在Student或Course中定义多对多字段,并通过`through`参数指定我们自定义的中间表
class Student(models.Model):
    name = models.CharField(max_length=100)
    courses = models.ManyToManyField(Course, through=Enrollment, verbose_name="选修课程") # 关键在这里!
    
    def __str__(self):
        return self.name

瞧见没?我们创建了一个Enrollment( enrollment)模型,它明确地通过两个ForeignKey分别指向StudentCourse。然后,在Studentcourses字段里,用 through=Enrollment 告诉Django:“别用你自动生成的表了,用我自定义的这个!”

这样一来,世界大不同了:

实战操作(高级版):

# 1. 创建学生和课程
>>> alice = Student.objects.create(name="Alice")
>>> bob = Student.objects.create(name="Bob")
>>> math_course = Course.objects.create(name="高等数学")
>>> physics_course = Course.objects.create(name="大学物理")

# 2. 现在不能直接用 add 了!因为我们要提供额外信息。
# 我们必须通过自定义的中间模型 Enrollment 来创建关系。
>>> enrollment1 = Enrollment.objects.create(student=alice, course=math_course, score=95.50)
>>> enrollment2 = Enrollment.objects.create(student=alice, course=physics_course, score=88.00)
>>> Enrollment.objects.create(student=bob, course=math_course, score=92.00)

# 3. 查询依然很直观:Alice选修了哪些课?
>>> alice.courses.all()
<QuerySet [<Course: 高等数学>, <Course: 大学物理>]>

# 4. 反向查询:选修了高等数学的学生有哪些?
>>> math_course.student_set.all()
<QuerySet [<Student: Alice>, <Student: Bob>]>

# 5. **灵魂操作:如何获取那个额外的“成绩”信息?**
# 比如,我想知道Alice在高等数学这门课的成绩。
# 思路:通过中间表Enrollment来查!
>>> alice_math_enrollment = Enrollment.objects.get(student=alice, course=math_course)
>>> print(f"Alice的高数成绩是:{alice_math_enrollment.score}")
Alice的高数成绩是:95.50

# 或者,通过关系查询集来遍历带有额外信息的关系
>>> for enrollment in alice.enrollment_set.all(): # 通过默认的反向关系名
...     print(f"课程:{enrollment.course.name}, 成绩:{enrollment.score}")
...
课程:高等数学, 成绩:95.50
课程:大学物理, 成绩:88.00

看到了吗?在自定义中间表模式下,add(), create(), remove() 这些便捷方法会失效。因为Django不知道该如何处理你那些额外的字段(比如score)。你必须像创建普通模型实例一样,通过中间模型Enrollment来手动创建和删除关系。

这虽然增加了一点步骤,但换来了巨大的灵活性!你可以记录任何你想记录的、关于这段关系的元数据。

总结与抉择:你该选哪种月老?

来,一张表帮你做出人生抉择:

特性

自动中间表 (懒人模式)

自定义中间表 (高级玩家模式)

适用场景

纯粹的关系,无需额外信息

关系本身带有属性(如日期、状态、分数)

灵活性

便捷性

(可直接使用add, remove等)

低 (需通过中间模型手动操作)

查询关系

简单直接 (post.tags.all())

简单直接 (student.courses.all())

查询关系详情

无法查询

可以查询 (Enrollment.objects.get(...))

一句话攻略:
如果你的关系是“点赞”、“关注”、“文章-标签”这种轻量级的,用自动中间表,省心省力。
如果你的关系是“订单-商品”(要记录数量、价格)、“学生-课程”(要记录成绩)这种“重量级”的,毫不犹豫地选择自定义中间表

好了,关于Django的“社交达人”——多对多字段,咱们今天就深度八卦到这里。希望这位“月老”能帮你把数据之间的关系打理得明明白白,让你的Django项目从此告别混乱,走向和谐!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值