说说 Python Django 模型之间的多对多关系

使用 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 “<Tag: 治愈>”: 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 <Book: 解忧杂货店>

还可以从标签对象中,通过 add 方法创建图书的同时,建立关联关系:

>>> tag3=Tag(name='科幻')
>>> tag3.save()
>>> book3=Book(name='海底两万里')
>>> book3.save()
>>> tag3.book_set.add(book3)
>>> book3.tags.all()
<QuerySet [<Tag: 科幻>]>

2.1.2 create 方法

也可以直接在 Book 模型的 tags 中,调用 create 方法,直接创建标签:

>>> book1.tags.create(name='小说')
<Tag: 小说>

在标签对象中,也可以通过 create 方法关联图书:

>>> tag3.book_set.create(name='呼吸')
<Book: 呼吸>
>>> tag3.book_set.all()
<QuerySet [<Book: 海底两万里>, <Book: 呼吸>]>
>>> book4=tag3.book_set.all()[1]
>>> book4.tags.all()
<QuerySet [<Tag: 科幻>]>

2.1.3 set 方法

tags 除了 create 方法,还可以利用 set 方法来建立关系:

>>> book3.tags.all()
<QuerySet []>
>>> book3.tags.set([tag3])
>>> book3.tags.all()
<QuerySet [<Tag: 科幻>]>

2.2 查询

从 Book 对象访问 Tag 对象:

>>> book1.tags.all()
<QuerySet [<Tag: 治愈>, <Tag: 温暖>, <Tag: 小说>]>

从 Tag 对象访问 Book 对象:

>>> tag1.book_set.all()
<QuerySet [<Book: 解忧杂货店>]>

也可以以 Tag 对象作为条件,查询出所对应的 Book 对象:

>>> Book.objects.filter(tags=1)
<QuerySet [<Book: 解忧杂货店>]>
>>> Book.objects.filter(tags=tag1)
<QuerySet [<Book: 解忧杂货店>]>
>>> Book.objects.filter(tags__name__startswith='温')
<QuerySet [<Book: 解忧杂货店>]>
>>> Book.objects.filter(tags__name__startswith='温').distinct()
<QuerySet [<Book: 解忧杂货店>]>

支持 count()、in 语法:

>>> Book.objects.filter(tags__name__startswith='温').count()
1
>>> Book.objects.filter(tags__in=[1, 2])
<QuerySet [<Book: 解忧杂货店>, <Book: 解忧杂货店>]>
>>> Book.objects.filter(tags__in=[tag1, tag2])
<QuerySet [<Book: 解忧杂货店>, <Book: 解忧杂货店>]>
>>> Book.objects.filter(tags__in=[tag1, tag2]).distinct()
<QuerySet [<Book: 解忧杂货店>]>

还支持反向查询,即以 Book 对象作为条件,查询出所对应的 Tag 对象:

>>> Tag.objects.filter(id=1)
<QuerySet [<Tag: 治愈>]>
>>> Tag.objects.filter(pk=1)
<QuerySet [<Tag: 治愈>]>
>>> Tag.objects.filter(book__name__startswith='解')
<QuerySet [<Tag: 治愈>, <Tag: 温暖>, <Tag: 小说>]>
>>> Tag.objects.filter(book='解忧杂货店')
<QuerySet [<Tag: 治愈>, <Tag: 温暖>, <Tag: 小说>]>
>>> Tag.objects.filter(book=book1)
<QuerySet [<Tag: 治愈>, <Tag: 温暖>, <Tag: 小说>]>
>>> Tag.objects.filter(book__in=['解忧杂货店','西中有东']).distinct()
<QuerySet [<Tag: 治愈>, <Tag: 温暖>, <Tag: 小说>]>
>>> Tag.objects.filter(book__in=[book1,'西中有东']).distinct()
<QuerySet [<Tag: 治愈>, <Tag: 温暖>, <Tag: 小说>]>

排除掉某些标签:

>>> Book.objects.exclude(tags=tag1)
<QuerySet [<Book: 猫的桌子>, <Book: 西中有东>]>

2.3 删除

2.3.1 delete 方法

删除某个标签后,图书实例的 tags 中也就查询不到:

>>> book1.tags.all()
<QuerySet [<Tag: 治愈>, <Tag: 温暖>, <Tag: 小说>]>
>>> tag1.delete()
(2, {'chart.Book_tags': 1, 'chart.Tag': 1})
>>> book1.tags.all()
<QuerySet [<Tag: 温暖>, <Tag: 小说>]>

与此类似,删除了某个图书后,从标签的 book_set 中就查询不到啦。

delete 方法还可以用于批量删除:

>>> tag4=Tag(name='科技')
>>> tag4.save()
>>> Tag.objects.all()
<QuerySet [<Tag: 温暖>, <Tag: 小说>, <Tag: 科幻>, <Tag: 科技>]>
>>> Tag.objects.filter(name__startswith='科').delete()
(2, {'chart.Book_tags': 0, 'chart.Tag': 2})
>>> Tag.objects.all()
<QuerySet [<Tag: 温暖>, <Tag: 小说>]>

2.3.2 remove 方法

也可以通过 A 实例来移除所关联的 B 实例:

>>> tag3.book_set.all()
<QuerySet [<Book: 海底两万里>, <Book: 呼吸>]>
>>> book3.tags.all()
<QuerySet [<Tag: 科幻>]>
>>> book3.tags.remove(tag3)
>>> tag3.book_set.all()
<QuerySet [<Book: 呼吸>]>
>>> book3.tags.all()
<QuerySet []>

与此类似,从 Tag 实例的 book_set 中也可以利用 remove() 方法移除 Book 实例。

2.3.3 clear 方法

利用 clear 方法,可以解除关联关系:

>>> tag3.book_set.all()
<QuerySet [<Book: 呼吸>, <Book: 海底两万里>]>
>>> tag3.book_set.clear()
>>> tag3.book_set.all()
<QuerySet []>

与此类似,从 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()
<QuerySet [<Park: 南京公园>]>
>>> park.park_facility_relations_set.all()
<QuerySet [<Park_Facility_Relations: Park_Facility_Relations object (1)>]>
>>> park.relations.all()
<QuerySet [<Recreation_Facility: 转马>]>

使用关系对象的 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='给小孩子玩'
...                                        )
<Park_Facility_Relations: Park_Facility_Relations object (2)>
>>>
>>> park2.relations.all()
<QuerySet [<Recreation_Facility: 转马>]>
>>> park2.relations.all()[0]
<Recreation_Facility: 转马>
>>> park2.relations.remove(rotating_horse)
>>> park2.relations.all()
<QuerySet []>

使用关系对象的 clear() 方法,会删除该对象的所有关联关系:

>>> park2.relations.all()
<QuerySet [<Recreation_Facility: 转马>]>
>>> park2.relations.clear()
>>> park2.relations.all()

以游乐设施作为条件,来找出拥有这些设施的公园有哪些:

>>> Park.objects.filter(relations__name__startswith='转')
<QuerySet [<Park: 南京公园>, <Park: 颐和园>]>

可以通过点语法查询出关联模型中的字段值:

>>> 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
'给小孩子玩'

发布了626 篇原创文章 · 获赞 705 · 访问量 91万+

猜你喜欢

转载自blog.csdn.net/deniro_li/article/details/104093992