我们都知道对于ManyToMany字段,Django采用的是第三张中间表的方式。通过这第三张表,来关联ManyToMany的双方。下面我们根据一个具体的例子,详细解说中间表的使用。

一、默认中间表

首先,模型是这样的:

  1. class Person(models.Model):
  2. name = models.CharField(max_length=128)
  3. def __str__(self):
  4. return self.name
  5. class Group(models.Model):
  6. name = models.CharField(max_length=128)
  7. members = models.ManyToManyField(Person)
  8. def __str__(self):
  9. return self.name

在Group模型中,通过members字段,以ManyToMany方式与Person模型建立了关系。
让我们到数据库内看一下实际的内容,Django为我们创建了三张数据表,其中的app1是应用名。
多对多中间表详解 - 图1
然后我在数据库中添加了下面的Person对象:
多对多中间表详解 - 图2
再添加下面的Group对象:
多对多中间表详解 - 图3
让我们来看看,中间表是个什么样子的:
多对多中间表详解 - 图4
首先有一列id,这是Django默认添加的,没什么好说的。然后是Group和Person的id列,这是默认情况下,Django关联两张表的方式。如果你要设置关联的列,可以使用to_field参数。
可见在中间表中,并不是将两张表的数据都保存在一起,而是通过id的关联进行映射。

二、自定义中间表

一般情况,普通的多对多已经够用,无需自己创建第三张关系表。但是某些情况可能更复杂一点,比如如果你想保存某个人加入某个分组的时间呢?想保存进组的原因呢?
Django提供了一个through参数,用于指定中间模型,你可以将类似进组时间,邀请原因等其他字段放在这个中间模型内。例子如下:

  1. from django.db import models
  2. class Person(models.Model):
  3. name = models.CharField(max_length=128)
  4. def __str__(self):
  5. return self.name
  6. class Group(models.Model):
  7. name = models.CharField(max_length=128)
  8. members = models.ManyToManyField(Person, through='Membership')
  9. def __str__(self):
  10. return self.name
  11. class Membership(models.Model):
  12. person = models.ForeignKey(Person, on_delete=models.CASCADE)
  13. group = models.ForeignKey(Group, on_delete=models.CASCADE)
  14. date_joined = models.DateField() # 进组时间
  15. invite_reason = models.CharField(max_length=64) # 邀请原因

在中间表中,我们至少要编写两个外键字段,分别指向关联的两个模型。在本例中就是‘Person’和‘group’。 这里,我们额外增加了‘date_joined’字段,用于保存人员进组的时间,‘invite_reason’字段用于保存邀请进组的原因。
下面我们依然在数据库中实际查看一下(应用名为app2):
多对多中间表详解 - 图5
注意中间表的名字已经变成“app2_membership”了。
多对多中间表详解 - 图6
多对多中间表详解 - 图7
Person和Group没有变化。
多对多中间表详解 - 图8
但是中间表就截然不同了!它完美的保存了我们需要的内容。

三、使用中间表

针对上面的中间表,下面是一些使用例子(以欧洲著名的甲壳虫乐队成员为例):

  1. >>> ringo = Person.objects.create(name="Ringo Starr")
  2. >>> paul = Person.objects.create(name="Paul McCartney")
  3. >>> beatles = Group.objects.create(name="The Beatles")
  4. >>> m1 = Membership(person=ringo, group=beatles,
  5. ... date_joined=date(1962, 8, 16),
  6. ... invite_reason="Needed a new drummer.")
  7. >>> m1.save()
  8. >>> beatles.members.all()
  9. <QuerySet [<Person: Ringo Starr>]>
  10. >>> ringo.group_set.all()
  11. <QuerySet [<Group: The Beatles>]>
  12. >>> m2 = Membership.objects.create(person=paul, group=beatles,
  13. ... date_joined=date(1960, 8, 1),
  14. ... invite_reason="Wanted to form a band.")
  15. >>> beatles.members.all()
  16. <QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>

可以使用 add(), create(), 或 set() 创建关联对象,只需指定 through_defaults 参数:

  1. >>> beatles.members.add(john, through_defaults={'date_joined': date(1960, 8, 1)})
  2. >>> beatles.members.create(name="George Harrison", through_defaults={'date_joined': date(1960, 8, 1)})
  3. >>> beatles.members.set([john, paul, ringo, george], through_defaults={'date_joined': date(1960, 8, 1)})

也可以直接创建中间模型实例。并且如果自定义中间模型没有强制设定 (model1, model2) 对的唯一性,调用 remove() 方法会删除所有中间模型的实例:

  1. >>> Membership.objects.create(person=ringo, group=beatles,
  2. ... date_joined=date(1968, 9, 4),
  3. ... invite_reason="You've been gone for a month and we miss you.")
  4. >>> beatles.members.all()
  5. <QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]>
  6. >>> # remove方法同时删除了两个 Ringo Starr
  7. >>> beatles.members.remove(ringo)
  8. >>> beatles.members.all()
  9. <QuerySet [<Person: Paul McCartney>]>
  10. clear()方法能清空所有的多对多关系。
  11. >>> # 甲壳虫乐队解散了
  12. >>> beatles.members.clear()
  13. >>> # 删除了中间模型的对象
  14. >>> Membership.objects.all()
  15. <QuerySet []>

一旦你通过创建中间模型实例的方法建立了多对多的关联,你立刻就可以像普通的多对多那样进行查询操作:
# 查找组内有Paul这个人的所有的组(以Paul开头的名字)

  1. >>> Group.objects.filter(members__name__startswith='Paul')
  2. <QuerySet [<Group: The Beatles>]>

可以使用中间模型的属性进行查询:
# 查找甲壳虫乐队中加入日期在1961年1月1日之后的成员

  1. >>> Person.objects.filter(
  2. ... group__name='The Beatles',
  3. ... membership__date_joined__gt=date(1961,1,1))
  4. <QuerySet [<Person: Ringo Starr]>

可以像普通模型一样使用中间模型:

  1. >>> ringos_membership = Membership.objects.get(group=beatles, person=ringo)
  2. >>> ringos_membership.date_joined
  3. datetime.date(1962, 8, 16)
  4. >>> ringos_membership.invite_reason
  5. 'Needed a new drummer.'
  6. >>> ringos_membership = ringo.membership_set.get(group=beatles)
  7. >>> ringos_membership.date_joined
  8. datetime.date(1962, 8, 16)
  9. >>> ringos_membership.invite_reason
  10. 'Needed a new drummer.'

这一部分内容,需要结合后面的模型query,如果暂时看不懂,没有关系。


对于中间表,有一点要注意(在前面章节已经介绍过,再次重申一下),默认情况下,中间模型只能包含一个指向源模型的外键关系,上面例子中,也就是在Membership中只能有Person和Group外键关系各一个,不能多。否则,你必须显式的通过ManyToManyField.through_fields参数指定关联的对象。参考下面的例子:

  1. from django.db import models
  2. class Person(models.Model):
  3. name = models.CharField(max_length=50)
  4. class Group(models.Model):
  5. name = models.CharField(max_length=128)
  6. members = models.ManyToManyField(
  7. Person,
  8. through='Membership',
  9. through_fields=('group', 'person'),
  10. )
  11. class Membership(models.Model):
  12. group = models.ForeignKey(Group, on_delete=models.CASCADE)
  13. person = models.ForeignKey(Person, on_delete=models.CASCADE)
  14. inviter = models.ForeignKey(
  15. Person,
  16. on_delete=models.CASCADE,
  17. related_name="membership_invites",
  18. )
  19. invite_reason = models.CharField(max_length=64)