![](/media/images/IMG_1012.jpg)
Django's Content Type
A Generic Relation Between a Model and other Models
What is Content Type?
Content Type Framework是Django提供的一種Application,其目的是為了讓你追蹤所有安裝在Django Project下面的Application的model們。它提供了一種更高階層的通用介面讓你在model之間互動。
官方文件描述第一次看的時候很抽象,但是實際認識背後的機制後會發現這是很有意思的功能。
Content Type的本質也是一個Model,可以透過觀察`django.contrib.contenttypes.models.ContentType
得到他的定義:
class ContentType(models.Model):
app_label = models.CharField(max_length=100) : CharField
model = models.CharField(_("python model class name"), max_length=100) : CharField
objects = ContentTypeManager() : ContentTypeManager
...
用Sqlite偷看一下他在資料庫的樣子
sqlite> select * from django_content_type;
id app_label model
-- ------------ -----------
1 admin logentry
2 auth permission
3 auth group
4 auth user
5 contenttypes contenttype
6 sessions session
7 YourApp model_1_of_your_app
8 YourApp model_2_of_your_app
可以看到ContentType
Model內紀錄了每一個已安裝的Application下的所有Model。每當你安裝一個新的Model時,ContentType
也會自動的在資料庫內初始化一個該model的Instance。
ContentType
的Instance本身除了可以返回他所代表的Model Class以外,甚至可以Query該模型所擁有的物件們,到這邊應該已經不難想像那句「更高層級的通用介面」的意思。
class ContentType(models.Model):
...
@property
def name(self):
...
@property
def app_labeled_name(self):
...
def model_class(self): -> (Unknown | None)
...
def get_object_for_this_type(self, **kwargs): -> Unknown
...
def get_all_objects_for_this_type(self, **kwargs): -> Unknown
...
def natural_key(self): -> None
...
另外ContentType
自帶了一個客製化的Model Manager,提供了跟ContentType互動的一些方法。
以上圖為例子:
今天App1-Model1
就是建立了一個Generic Relation
到App2-Model2
。
透過content_type
屬性,id為99的Model 1 Instance知道了,他跟content type id 為3的Model(也就是App2-Model2
)有關聯。至於是跟該Model下面哪一個Instance有關聯?那就是看object_id
。
Generic Relation
接下來就要帶出跟ContentType
有高度密切的關聯另外兩個東西,分別是GenericRelation
跟GenericForeignKey
。
GenericRelation
指的就是你的Model跟ContentType
Model之間的關聯。
(或者是說,你當前正在用的Model跟其他你已經安裝的Model之間的關聯)
Generic Foreign Key
這裡使用官方文件的例子,如果今天我有個Model稱為TaggedItem
,這是一種通用型的Tag物件,可以幫任何的東西貼上標籤例如User
, Post
, Image
…之類的。但如果只使用Foreign Key是不夠的(因為只能Tag到一個Model而已)。但我們可以使用GenericForeignKey
來實現在不同的Model上都貼上這種通用標籤,只要在TagItem Model裡面加入些屬性即可:
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
class TaggedItem(models.Model):
tag = models.SlugField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey("content_type", "object_id")
def __str__(self):
return self.tag
class Meta:
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
- 我們在
TaggedItem
裡面多了幾個屬性:content_type
:一把指向ContentType
Instance的ForeignKey。object_id
:你想建立關係的物件的primary key。- 在這個範例中,就會是某篇文章的pk
content_object
:一把GenericForeignKey
,把他視為可以直接訪問被你關聯的Instance的一個捷徑即可。
有了這樣的設定後,就可以像普通的ForeignKey一樣做使用:
>>> from django.contrib.auth.models import User
>>> guido = User.objects.get(username="Guido")
>>> t = TaggedItem(content_object=guido, tag="bdfl")
>>> t.save()
>>> t.content_object
<User: Guido>
- 產生一個名稱為
Guido
的使用者 - 透過Generic Relation幫他上一個通用型的標籤
bdfl
但基於GenericForeignKey
的實作方式,我們沒有辦法透過filter()
或是exclude()
之類的指令來反向的使用content_object
找到使用者,例如想找出用戶是guido的Tag們時,下面的指令會失敗:
# This will fail
>>> TaggedItem.objects.filter(content_object=guido)
# This will also fail
>>> TaggedItem.objects.get(content_object=guido)
Reverse Generic Relation
為了解決上面提到的反向搜尋的那個問題,我們可以在目標Model上面添加一個反向的關聯(reverse generic realtionship),這裡用一個新的目標Model Bookmark
為例:
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
class Bookmark(models.Model):
url = models.URLField()
tags = GenericRelation(TaggedItem)
- tags屬性,被設定成了GenericRelation
我們用類似上面幫用戶貼標籤的方式,也幫BookMark貼個標籤:
>>> b = Bookmark(url="https://www.djangoproject.com/")
>>> b.save()
>>> t1 = TaggedItem(content_object=b, tag="django")
>>> t1.save()
>>> t2 = TaggedItem(content_object=b, tag="python")
>>> t2.save()
>>> b.tags.all()
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>
然後原本不能用的filter之類的Query指令就可以使用:
>>> # Get all tags belonging to bookmarks containing `django` in the url
>>> TaggedItem.objects.filter(bookmark__url__contains="django")
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>
總結
最後稍微做個總結,剛理解完ContentType
跟Generic Relation
後會感覺他提供的功能跟ManyToManyField
好像有點類似,但實際上用途不同。ManyToMany
針對的是兩個Model之間的多對多關係。GenericRelation
針對的是一個Model與其他多個Model之間的關係(同時可以用它實現ManyToMany
的行為)。
如果今天想設計一個通用型的Model,讓他可以在其他不同的Model間作為一種屬性存在的話,那就可以考慮使用Generic Relation
來做實現他的功能。