使用 django.db.models.ManyToManyField 类,就可以定义出一个多对多的关联关系。与 ForeignKey 类用法相同,也是在模型中,添加一个值,作为ManyToManyField 类的实例,并且也有一个入参,用于定义想要关联的模型类名。

1 定义模型

例如:一本书可以被定义为多个标签,而一个标签也可以属于多本书,所以书与标签之间属于多对多关系。

在 models.py 中,新建标签与书模型类:

'''

标签

'''

class Tag(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return '%s' % (self.name)
'''
书
'''
class Book(models.Model):
...
tags = models.ManyToManyField(Tag)
...

执行 migrate 迁移命令成功后,就会创建一个新的关系表 chart_book_tags,用于实现多对多关系:


这张关系表建了自有主键,并定义了两个外键,分别对应所需要关联的两张表的主键:


因为一般来说,会在书模型中,看到它所隶属的标签。所以,我们把 ManyToManyField 字段定义在 Book 模型中。

2 使用方法

2.1 新建

2.1.1 add 方法

首先新建标签模型并保存:

>>> from chart.models import *
>>> tag1=Tag(name='治愈')
>>> tag1.save()
>>> tag2=Tag(name='温暖')
>>> tag2.save()
然后,创建图书模型:
book=Book(name='解忧杂货店')
注意: 比如先保存图书模型,才能关联标签模型。否则会抛出 ValueError 错误:
ValueError: Cannot add "": instance is on database "None", value is on database "default"
保存图书模型后,再关联相应的标签:
>>> book1=Book(name='解忧杂货店')
>>> book1.save()
>>> book1.tags.add(tag1)
>>> book1.tags.add(tag1,tag2)
从库表中,可以看到关联关系已经建立好了:

可以多次关联同一关系,但这没有意义,因为并不会对库表关系造成任何影响:
>>> book1.tags.add(tag1,tag2)
如果关联的模型不是定义的那样,就会抛出 TypeError,比如:
book1.tags.add(book1)
TypeError: 'Tag' instance expected, got 
还可以从标签对象中,通过 add 方法创建图书的同时,建立关联关系:
>>> tag3=Tag(name='科幻')
>>> tag3.save()
>>> book3=Book(name='海底两万里')
>>> book3.save()
>>> tag3.book_set.add(book3)
>>> book3.tags.all()
]>
2.1.2 create 方法
也可以直接在 Book 模型的 tags 中,调用 create 方法,直接创建标签:
>>> book1.tags.create(name='小说')
在标签对象中,也可以通过 create 方法关联图书:
>>> tag3.book_set.create(name='呼吸')
>>> tag3.book_set.all()
, ]>
>>> book4=tag3.book_set.all()[1]
>>> book4.tags.all()
]>
2.1.3 set 方法
tags 除了 create 方法,还可以利用 set 方法来建立关系:
>>> book3.tags.all()
>>> book3.tags.set([tag3])
>>> book3.tags.all()
]>
2.2 查询
从 Book 对象访问 Tag 对象:
>>> book1.tags.all()
, , ]>
从 Tag 对象访问 Book 对象:
>>> tag1.book_set.all()
]>
也可以以 Tag 对象作为条件,查询出所对应的 Book 对象:
>>> Book.objects.filter(tags=1)
]>
>>> Book.objects.filter(tags=tag1)
]>
>>> Book.objects.filter(tags__name__startswith='温')
]>
>>> Book.objects.filter(tags__name__startswith='温').distinct()
]>
支持 count()、in 语法:
>>> Book.objects.filter(tags__name__startswith='温').count()
1
>>> Book.objects.filter(tags__in=[1, 2])
, ]>
>>> Book.objects.filter(tags__in=[tag1, tag2])
, ]>
>>> Book.objects.filter(tags__in=[tag1, tag2]).distinct()
]>
还支持反向查询,即以 Book 对象作为条件,查询出所对应的 Tag 对象:
>>> Tag.objects.filter(id=1)
]>
>>> Tag.objects.filter(pk=1)
]>
>>> Tag.objects.filter(book__name__startswith='解')
, , ]>
>>> Tag.objects.filter(book='解忧杂货店')
, , ]>
>>> Tag.objects.filter(book=book1)
, , ]>
>>> Tag.objects.filter(book__in=['解忧杂货店','西中有东']).distinct()
, , ]>
>>> Tag.objects.filter(book__in=[book1,'西中有东']).distinct()
, , ]>
排除掉某些标签:
>>> Book.objects.exclude(tags=tag1)
, ]>
2.3 删除
2.3.1 delete 方法
删除某个标签后,图书实例的 tags 中也就查询不到:
>>> book1.tags.all()
, , ]>
>>> tag1.delete()
(2, {'chart.Book_tags': 1, 'chart.Tag': 1})
>>> book1.tags.all()
, ]>
与此类似,删除了某个图书后,从标签的 book_set 中就查询不到啦。
delete 方法还可以用于批量删除:
>>> tag4=Tag(name='科技')
>>> tag4.save()
>>> Tag.objects.all()
, , , ]>
>>> Tag.objects.filter(name__startswith='科').delete()
(2, {'chart.Book_tags': 0, 'chart.Tag': 2})
>>> Tag.objects.all()
, ]>
2.3.2 remove 方法
也可以通过 A 实例来移除所关联的 B 实例:
>>> tag3.book_set.all()
, ]>
>>> book3.tags.all()
]>
>>> book3.tags.remove(tag3)
>>> tag3.book_set.all()
]>
>>> book3.tags.all()
与此类似,从 Tag 实例的 book_set 中也可以利用 remove() 方法移除 Book 实例。
2.3.3 clear 方法
利用 clear 方法,可以解除关联关系:
>>> tag3.book_set.all()
, ]>
>>> tag3.book_set.clear()
>>> tag3.book_set.all()
与此类似,从 Book 实例的 tags 对象中,也可以调用 clear 方法,来解除关联关系。
3 自定义关联模型
3.1 定义模型
默认的多对多关联模型,只有三个字段,它们分别是 ID以及所关联的两个模型的 ID。有时候,我们需要在关联模型中,记录更多的信息。比如公园游乐设施管理,一种游乐设施被加入公园时,希望记录它加入的时间以及加入缘由。
'''
游乐设施
'''
class Recreation_Facility(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return '%s' % (self.name)
'''
公园
'''
class Park(models.Model):
name = models.CharField(max_length=50)
relations = models.ManyToManyField(Recreation_Facility, through='Park_Facility_Relations')
def __str__(self):
return '%s' % (self.name)
'''
公园与游乐设施之间的关联关系
'''
class Park_Facility_Relations(models.Model):
recreation_facility = models.ForeignKey(Recreation_Facility, on_delete=models.CASCADE)
park = models.ForeignKey(Park, on_delete=models.CASCADE)
joined_date = models.DateField()
invite_reason = models.CharField(max_length=300)
执行 migrate 指令之后,就可以在数据库中看到建好的自定义关系表:

自定义关联模型必须有且仅有一个指向所需关联模型的 ForeignKey,即一个模型一个外键。
3.2 使用模型
因为是自定义的关联模型,所以必须自行编写代码,初始化关联模型实例,并保存:
>>> from datetime import date
>>> rotating_horse=Recreation_Facility.objects.create(name='转马')
>>> fly_chair=Recreation_Facility.objects.create(name='飓风飞椅')
>>> swing_hammer=Recreation_Facility.objects.create(name='大摆锤')
>>> park=Park.objects.create(name='南京公园')
>>> r1=Park_Facility_Relations(park=park,recreation_facility=rotating_horse,
... joined_date=date(2020,1,27),
... invite_reason='给小孩子玩')
>>> r1.save()
>>>
>>> rotating_horse.park_set.all()
]>
>>> park.park_facility_relations_set.all()
]>
>>> park.relations.all()
]>
使用关系对象的 remove() 方法,删除关联关系:
>>> park2=Park(name='颐和园')
>>> park2.save()
>>> Park_Facility_Relations.objects.create(park=park2,recreation_facility=rotating_horse,
... joined_date=date(2020, 1, 27),
... invite_reason='给小孩子玩'
... )
>>>
>>> park2.relations.all()
]>
>>> park2.relations.all()[0]
>>> park2.relations.remove(rotating_horse)
>>> park2.relations.all()
使用关系对象的 clear() 方法,会删除该对象的所有关联关系:
>>> park2.relations.all()
]>
>>> park2.relations.clear()
>>> park2.relations.all()
以游乐设施作为条件,来找出拥有这些设施的公园有哪些:
>>> Park.objects.filter(relations__name__startswith='转')
, ]>
可以通过点语法查询出关联模型中的字段值:
>>> r2=Park_Facility_Relations.objects.get(park=park2,recreation_facility=rotating_horse)
>>> r2.joined_date
datetime.date(2020, 1, 27)
>>> r2.invite_reason
'给小孩子玩'
还可以以公园与游乐设施为条件,找出具体关系:
>>> r3=rotating_horse.park_facility_relations_set.get(park=park2)
>>> r3.joined_date
datetime.date(2020, 1, 27)
>>> r3.invite_reason
'给小孩子玩'