1. 程式人生 > >多對多中間表詳解 -- Django從入門到精通系列教程

多對多中間表詳解 -- Django從入門到精通系列教程

該系列教程繫個人原創,並完整發布在個人官網劉江的部落格和教程

所有轉載本文者,需在頂部顯著位置註明原作者及www.liujiangblog.com官網地址。

我們都知道對於ManyToMany欄位,Django採用的是第三張中間表的方式。通過這第三張表,來關聯ManyToMany的雙方。下面我們根據一個具體的例子,詳細解說中間表的使用。

一、預設中間表

首先,模型是這樣的:

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name


class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person)

    def __str__(self):
        return self.name

在Group模型中,通過members欄位,以ManyToMany方式與Person模型建立了關係。

讓我們到資料庫內看一下實際的內容,Django為我們建立了三張資料表,其中的app1是應用名。

image.png-6.4kB

然後我在資料庫中添加了下面的Person物件:

image.png-16.7kB

再新增下面的Group物件:

image.png-13.9kB

讓我們來看看,中間表是個什麼樣子的:

image.png-23.7kB

首先有一列id,這是Django預設新增的,沒什麼好說的。然後是Group和Person的id列,這是預設情況下,Django關聯兩張表的方式。如果你要設定關聯的列,可以使用to_field引數。

可見在中間表中,並不是將兩張表的資料都儲存在一起,而是通過id的關聯進行對映。

二、自定義中間表

一般情況,普通的多對多已經夠用,無需自己建立第三張關係表。但是某些情況可能更復雜一點,比如如果你想儲存某個人加入某個分組的時間呢?想儲存進組的原因呢?

Django提供了一個through引數,用於指定中間模型,你可以將類似進組時間,邀請原因等其他欄位放在這個中間模型內。例子如下:

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=128)
    def __str__(self): 
        return self.name
        
class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')
    def __str__(self): 
        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)  # 邀請原因

在中間表中,我們至少要編寫兩個外來鍵欄位,分別指向關聯的兩個模型。在本例中就是‘Person’和‘group’。
這裡,我們額外增加了‘date_joined’欄位,用於儲存人員進組的時間,‘invite_reason’欄位用於儲存邀請進組的原因。

下面我們依然在資料庫中實際檢視一下(應用名為app2):

image.png-3.8kB

注意中間表的名字已經變成“app2_membership”了。

image.png-16.5kB

image.png-13.8kB

Person和Group沒有變化。

image.png-42.6kB

但是中間表就截然不同了!它完美的儲存了我們需要的內容。

三、使用中間表

針對上面的中間表,下面是一些使用例子(以歐洲著名的甲殼蟲樂隊成員為例):

>>> 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(),remove(),和set()方法來建立、刪除關係,看下面:

>>> # 無效
>>> beatles.members.add(john)
>>> # 無效
>>> beatles.members.create(name="George Harrison")
>>> # 無效
>>> beatles.members.set([john, paul, ringo, george])

為什麼?因為上面的方法無法提供加入時間、邀請原因等中間模型需要的欄位內容。唯一的辦法只能是通過建立中間模型的例項來建立這種型別的多對多關聯。但是,clear()方法是有效的,它能清空所有的多對多關係。

>>> # 甲殼蟲樂隊解散了
>>> beatles.members.clear()
>>> # 刪除了中間模型的物件
>>> Membership.objects.all()
<QuerySet []>

一旦你通過建立中間模型例項的方法建立了多對多的關聯,你立刻就可以像普通的多對多那樣進行查詢操作:

# 查詢組內有Paul這個人的所有的組(以Paul開頭的名字)
>>> Group.objects.filter(members__name__startswith='Paul')
<QuerySet [<Group: The Beatles>]>

可以使用中間模型的屬性進行查詢:

# 查詢甲殼蟲樂隊中加入日期在1961年1月1日之後的成員
>>> Person.objects.filter(
... group__name='The Beatles',
... membership__date_joined__gt=date(1961,1,1))
<QuerySet [<Person: Ringo Starr]>

可以像普通模型一樣使用中間模型:

>>> 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.'
>>> 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.'

這一部分內容,需要結合後面的模型query,如果暫時看不懂,沒有關係。

對於中間表,有一點要注意(在前面章節已經介紹過,再次重申一下),預設情況下,中間模型只能包含一個指向源模型的外來鍵關係,上面例子中,也就是在Membership中只能有Person和Group外來鍵關係各一個,不能多。否則,你必須顯式的通過ManyToManyField.through_fields引數指定關聯的物件。參考下面的例子:

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=50)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(
    Person,
    through='Membership',
    through_fields=('group', 'person'),
    )

class Membership(models.Model):
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    inviter = models.ForeignKey(
    Person,
    on_delete=models.CASCADE,
    related_name="membership_invites",
    )
    invite_reason = models.CharField(max_length=64)