django_models_关系多对多
多对多关系
ManyToManyField
用来定义多对多关系, 和使用其它Field
类型一样:在模型当中把它做为一个类属性包含进来。
ManyToManyField
需要一个位置参数:和该模型关联的类。
例如,一个Pizza
可以有多种Topping
即一种Topping
也可以位于多个Pizza上,而且每个Pizza
有多个topping,下面是如何表示这个关系:
from django.db import models class Topping(models.Model): # ... pass class Pizza(models.Model): # ... toppings = models.ManyToManyField(Topping)
与ForeignKey
一样,你还可以创建递归关联关系(与其自身具有多对多关系的对象)和与尚未定义的模型的关联关系。
建议你以被关联模型名称的复数形式做为ManyToManyField
的名字(例如上例中的toppings
)。
在哪个模型中设置 ManyToManyField
并不重要,在两个模型中任选一个即可 —— 不要两个模型都设置。
注意:
一般来说,ManyToManyField
实例应该放在要在表单中被编辑的对象中。 在上面的例子中,Topping
位于Pizza
中(而不是在 toppings
里面设置pizzas
的ManyToManyField
字段),因为设想一个Pizza 有多种Topping 比一个Topping 位于多个Pizza 上要更加自然。 按照上面的方式,在Pizza
的表单中将允许用户选择不同的Toppings。
提示:
完整的示例参见多对多关联关系模型示例。
ManyToManyField
字段还接受别的参数,在模型字段参考中有详细介绍。 这些选项有助于确定关系如何工作;都是可选的。
多对多关系的额外字段¶
处理类似搭配 pizza 和 topping 这样简单的多对多关系时,使用标准的ManyToManyField
就可以了。 但是,有时你可能需要关联数据到两个模型之间的关系上。
例如,有这样一个应用,它记录音乐家所属的音乐小组。 我们可以用一个ManyToManyField
表示小组和成员之间的多对多关系。 但是,有时你可能想知道更多成员关系的细节,比如成员是何时加入小组的。
对于这些情况,Django 允许你指定一个中介模型来定义多对多关系。 你可以将其他字段放在中介模型里面。 源模型的ManyToManyField
字段将使用through
参数指向中介模型。 对于上面的音乐小组的例子,代码如下:
from django.db import models class Person(models.Model): name = models.CharField(max_length=128) def __str__(self): # __unicode__ on Python 2 return self.name class Group(models.Model): name = models.CharField(max_length=128) members = models.ManyToManyField(Person, through='Membership') def __str__(self): # __unicode__ on Python 2 return self.name class Membership(models.Model): person = models.ForeignKey(Person, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE) date_joined = models.DateField() invite_reason = models.CharField(max_length=64)
在设置中介模型时,要显式地指定外键并关联到多对多关系涉及的模型。 这个显式声明定义两个模型之间是如何关联的。
中介模型有一些限制:
- 中介模型必须有且只有一个外键到源模型(上面例子中的
Group
),或者你必须使用ManyToManyField.through_fields
显式指定Django 应该在关系中使用的外键。 如果你的模型中存在不止一个外键,并且through_fields
没有指定,将会触发一个无效的错误。 对目标模型的外键有相同的限制(上面例子中的Person
)。 - 对于通过中介模型与自己进行多对多关联的模型,允许存在到同一个模型的两个外键,但它们将被当做多对多关联中一个关系的两边。 如果有超过两个外键,同样你必须像上面一样指定
through_fields
,否则将引发一个验证错误。 - 使用中介模型定义与自身的多对多关系时,你必须设置
symmetrical=False
(详见模型字段参考)。
既然你已经设置好ManyToManyField
来使用中介模型(在这个例子中就是Membership
),接下来你要开始创建多对多关系。 你要做的就是创建中介模型的实例:
>>> ringo = Person.objects.create(name="Ringo Starr") >>> paul = Person.objects.create(name="Paul McCartney") >>> beatles = Group.objects.create(name="The Beatles") >>> m1 = Membership(person=ringo, group=beatles, ... date_joined=date(1962, 8, 16), ... invite_reason="Needed a new drummer.") >>> m1.save() >>> beatles.members.all() <QuerySet [<Person: Ringo Starr>]> >>> ringo.group_set.all() <QuerySet [<Group: The Beatles>]> >>> m2 = Membership.objects.create(person=paul, group=beatles, ... date_joined=date(1960, 8, 1), ... invite_reason="Wanted to form a band.") >>> beatles.members.all() <QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
与常规的多对多字段不同,不能使用add()
,create()
或set()
创建关系:
>>> # 下列语句都是无法工作的 >>> beatles.members.add(john) >>> beatles.members.create(name="George Harrison") >>> beatles.members.set([john, paul, ringo, george])
为什么不能这样做? 这是因为你不能只创建 Person
和 Group
之间的关联关系,你还要指定 Membership
模型中所需要的所有信息; 而简单的add
、create
和赋值语句是做不到这一点的。 所以它们不能在使用中介模型的多对多关系中使用。 此时,唯一的办法就是创建中介模型的实例。
remove()
方法被禁用也是出于同样的原因。 例如,如果通过中介模型定义的表没有在(model1, model2)
对上强制执行唯一性,则remove()
调用将不能提供足够的信息,说明应该删除哪个中介模型实例:
>>> Membership.objects.create(person=ringo, group=beatles, ... date_joined=date(1968, 9, 4), ... invite_reason="You've been gone for a month and we miss you.") >>> beatles.members.all() <QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]> >>> # This will not work because it cannot tell which membership to remove >>> beatles.members.remove(ringo)
但是clear() 方法却是可用的。它可以清空某个实例所有的多对多关系: >>> # Beatles have broken up >>> beatles.members.clear() >>> # Note that this deletes the intermediate model instances >>> Membership.objects.all()
通过创建中介模型的实例来建立对多对多关系后,你就可以执行查询了。 和普通的多对多字段一样,你可以直接使用被关联模型的属性进行查询: # Find all the groups with a member whose name starts with 'Paul' >>> Group.objects.filter(members__name__startswith='Paul') <QuerySet [<Group: The Beatles>]>
如果你使用了中介模型,你也可以利用中介模型的属性进行查询: # Find all the members of the Beatles that joined after 1 Jan 1961 >>> Person.objects.filter( ... group__name='The Beatles', ... membership__date_joined__gt=date(1961,1,1)) <QuerySet [<Person: Ringo Starr]>
如果你需要访问一个成员的信息,你可以直接获取Membership模型: >>> ringos_membership = Membership.objects.get(group=beatles, person=ringo) >>> ringos_membership.date_joined datetime.date(1962, 8, 16) >>> ringos_membership.invite_reason 'Needed a new drummer.'
另一种获取相同信息的方法是,在Person对象上查询多对多反向关系: >>> ringos_membership = ringo.membership_set.get(group=beatles) >>> ringos_membership.date_joined datetime.date(1962, 8, 16) >>> ringos_membership.invite_reason 'Needed a new drummer.'