內容類型框架

Django 包含一個 contenttypes 應用程式,它可以追蹤您 Django 專案中安裝的所有模型,並提供一個高階、通用的介面來使用您的模型。

概觀

內容類型應用程式的核心是 ContentType 模型,它位於 django.contrib.contenttypes.models.ContentTypeContentType 的實例表示並儲存您專案中已安裝模型的資訊,並且每當安裝新模型時,都會自動建立 ContentType 的新實例。

ContentType 的實例具有方法可以傳回它們所代表的模型類別,以及查詢這些模型的物件。ContentType 還有一個 自訂管理器,它添加了用於處理 ContentType 和取得特定模型的 ContentType 實例的方法。

您的模型和 ContentType 之間的關聯性也可以用來啟用您的某個模型實例與您已安裝的任何模型實例之間的「通用」關係。

安裝 contenttypes 框架

contenttypes 框架包含在 django-admin startproject 建立的預設 INSTALLED_APPS 清單中,但是如果您已將其移除,或您手動設定了 INSTALLED_APPS 清單,您可以將 'django.contrib.contenttypes' 加入 INSTALLED_APPS 設定來啟用它。

通常建議安裝 contenttypes 框架; Django 的其他幾個綑綁應用程式需要它

  • 管理應用程式使用它來記錄透過管理介面新增或變更的每個物件的歷史記錄。

  • Django 的 驗證 框架 使用它將使用者權限連結到特定模型。

ContentType 模型

class ContentType[原始碼]

ContentType 的每個實例都有兩個欄位,它們結合起來可以唯一描述一個已安裝的模型

app_label

模型所屬應用程式的名稱。取自模型的 app_label 屬性,且僅包含應用程式 Python 匯入路徑的最後一部分;例如,django.contrib.contenttypes 會變成 app_labelcontenttypes

model

模型類別的名稱。

此外,還可以使用以下屬性

name[原始碼]

內容類型的易讀名稱。取自模型的 verbose_name 屬性。

讓我們來看一個範例,以了解它是如何運作的。如果您已經安裝了 contenttypes 應用程式,然後將 sites 應用程式 加入您的 INSTALLED_APPS 設定並執行 manage.py migrate 以安裝它,模型 django.contrib.sites.models.Site 將會安裝到您的資料庫中。同時,也會建立一個新的 ContentType 實例,其值如下

  • app_label 將設定為 'sites' (Python 路徑 django.contrib.sites 的最後一部分)。

  • model 將設定為 'site'

ContentType 實例的方法

每個 ContentType 實例都有一些方法,可讓您從 ContentType 實例取得其所代表的模型,或從該模型檢索物件

ContentType.get_object_for_this_type(using=None, **kwargs)[原始碼]

接受模型的一組有效的查詢參數,該模型由ContentType 表示,並對該模型執行get() 查詢,傳回對應的物件。 using 參數可用於指定與預設資料庫不同的資料庫。

在 Django 5.1 中變更

新增了 using 參數。

ContentType.model_class()[原始碼]

傳回此 ContentType 實例所表示的模型類別。

例如,我們可以查詢 ContentType 以取得 User 模型

>>> from django.contrib.contenttypes.models import ContentType
>>> user_type = ContentType.objects.get(app_label="auth", model="user")
>>> user_type
<ContentType: user>

然後使用它來查詢特定的 User,或取得 User 模型類別的存取權

>>> user_type.model_class()
<class 'django.contrib.auth.models.User'>
>>> user_type.get_object_for_this_type(username="Guido")
<User: Guido>

結合 get_object_for_this_type()model_class() 可實現兩個非常重要的使用案例

  1. 使用這些方法,您可以撰寫高階的泛型程式碼,對任何已安裝的模型執行查詢 – 您可以將 app_labelmodel 傳遞到執行階段的 ContentType 查詢中,而不是匯入和使用單個特定的模型類別,然後使用模型類別或從中檢索物件。

  2. 您可以將另一個模型與 ContentType 關聯,將其執行個體與特定模型類別連結,並使用這些方法取得這些模型類別的存取權。

Django 的幾個捆綁應用程式使用了後一種技術。例如,Django 驗證框架中的權限系統使用具有指向 ContentType 外鍵的 Permission 模型;這讓 Permission 表示「可以新增部落格條目」或「可以刪除新聞報導」等概念。

ContentTypeManager

class ContentTypeManager[原始碼]

ContentType 也有一個自訂管理器,ContentTypeManager,它新增了以下方法

clear_cache()[原始碼]

清除 ContentType 使用的內部快取,以追蹤已建立 ContentType 執行個體的模型。您可能永遠不需要自己呼叫此方法;Django 會在需要時自動呼叫它。

get_for_id(id)[原始碼]

依 ID 查詢 ContentType。由於此方法使用與 get_for_model() 相同的共用快取,因此最好使用此方法,而不是通常的 ContentType.objects.get(pk=id)

get_for_model(model, for_concrete_model=True)[原始碼]

接受模型類別或模型實例,並傳回表示該模型的 ContentType 執行個體。for_concrete_model=False 允許擷取 Proxy 模型的 ContentType

get_for_models(*models, for_concrete_models=True)[原始碼]

接受可變數量的模型類別,並傳回將模型類別對應至表示它們的 ContentType 執行個體的字典。for_concrete_models=False 允許擷取 Proxy 模型的 ContentType

get_by_natural_key(app_label, model)[原始碼]

傳回由給定的應用程式標籤和模型名稱唯一識別的 ContentType 執行個體。此方法的主要目的是允許在還原序列化期間,透過自然鍵來參照 ContentType 物件。

當您知道需要使用 ContentType,但不希望費力取得模型的元數據來執行手動查詢時,get_for_model() 方法特別有用

>>> from django.contrib.auth.models import User
>>> ContentType.objects.get_for_model(User)
<ContentType: user>

泛型關係

從您自己的模型之一新增一個指向 ContentType 的外鍵,可讓您的模型有效地將自身連結到另一個模型類別,如上述 Permission 模型的範例所示。但是,可以更進一步,並使用 ContentType 來啟用模型之間真正的泛型(有時稱為「多型」)關係。

例如,它可以像這樣用於標籤系統

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"]),
        ]

一般的 ForeignKey 只能「指向」一個其他的模型,這表示如果 TaggedItem 模型使用 ForeignKey,它就必須選擇一個且僅有一個模型來儲存標籤。contenttypes 應用程式提供一種特殊的欄位類型 (GenericForeignKey),可以解決這個問題,並允許關聯到任何模型。

class GenericForeignKey[來源]

設定 GenericForeignKey 有三個步驟:

  1. 為您的模型提供一個指向 ContentTypeForeignKey。此欄位的常用名稱是「content_type」。

  2. 為您的模型提供一個欄位,可以儲存您將關聯的模型的主鍵值。對於大多數模型來說,這表示一個 PositiveIntegerField。此欄位的常用名稱是「object_id」。

  3. 為您的模型提供一個 GenericForeignKey,並傳入上述兩個欄位的名稱。如果這些欄位命名為「content_type」和「object_id」,您可以省略此步驟,因為這些是 GenericForeignKey 將尋找的預設欄位名稱。

ForeignKey 不同,資料庫索引並不會自動在 GenericForeignKey 上建立,因此建議您使用 Meta.indexes 來新增您自己的多欄索引。這個行為未來 可能會改變

for_concrete_model

如果 False,則此欄位可以參照代理模型。預設值為 True。這與 get_for_model()for_concrete_model 引數相同。

主鍵類型相容性

「object_id」欄位不必與相關模型上的主鍵欄位類型相同,但是它們的主鍵值必須能夠透過其 get_db_prep_value() 方法強制轉換為與「object_id」欄位相同的類型。

例如,如果您想允許泛型關係指向具有 IntegerFieldCharField 主鍵欄位的模型,您可以使用 CharField 作為模型上的「object_id」欄位,因為整數可以透過 get_db_prep_value() 強制轉換為字串。

為了達到最大的彈性,您可以使用沒有定義最大長度的 TextField,但是這可能會根據您的資料庫後端造成顯著的效能損失。

沒有一種適用於所有情況的最佳欄位類型。您應該評估您預期指向的模型,並決定哪種解決方案最適合您的用例。

序列化對 ContentType 物件的參照

如果您正在序列化來自實作泛型關係模型的資料(例如,在產生 fixtures 時),您應該使用自然鍵來唯一識別相關的 ContentType 物件。請參閱 自然鍵dumpdata --natural-foreign 以取得更多資訊。

這將啟用類似於一般 ForeignKey 的 API;每個 TaggedItem 都會有一個 content_object 欄位,該欄位會傳回它所關聯的物件,您也可以在建立 TaggedItem 時指定該欄位或使用它。

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

如果相關物件被刪除,則 content_typeobject_id 欄位會保持設定為其原始值,而 GenericForeignKey 會傳回 None

>>> guido.delete()
>>> t.content_object  # returns None

由於 GenericForeignKey 的實作方式,您無法直接透過資料庫 API 將此類欄位用於篩選器(例如,filter()exclude())。因為 GenericForeignKey 不是一般的欄位物件,所以這些範例將 *無法* 運作。

# This will fail
>>> TaggedItem.objects.filter(content_object=guido)
# This will also fail
>>> TaggedItem.objects.get(content_object=guido)

同樣地,GenericForeignKey 不會出現在 ModelForm 中。

反向泛型關係

class GenericRelation[來源]
related_query_name

預設情況下,相關物件上返回到此物件的關係不存在。設定 related_query_name 會從相關物件建立返回到此物件的關係。這允許從相關物件進行查詢和篩選。

如果您知道最常使用哪些模型,您也可以新增一個「反向」泛型關係來啟用額外的 API。例如:

from django.contrib.contenttypes.fields import GenericRelation
from django.db import models


class Bookmark(models.Model):
    url = models.URLField()
    tags = GenericRelation(TaggedItem)

Bookmark 實例將各自具有一個 tags 屬性,可用於檢索其相關的 TaggedItems

>>> b = Bookmark(url="https://djangoproject.dev.org.tw/")
>>> 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>]>

您也可以使用 add()create()set() 來建立關係。

>>> t3 = TaggedItem(tag="Web development")
>>> b.tags.add(t3, bulk=False)
>>> b.tags.create(tag="Web framework")
<TaggedItem: Web framework>
>>> b.tags.all()
<QuerySet [<TaggedItem: django>, <TaggedItem: python>, <TaggedItem: Web development>, <TaggedItem: Web framework>]>
>>> b.tags.set([t1, t3])
>>> b.tags.all()
<QuerySet [<TaggedItem: django>, <TaggedItem: Web development>]>

remove() 呼叫會批量刪除指定的模型物件。

>>> b.tags.remove(t3)
>>> b.tags.all()
<QuerySet [<TaggedItem: django>]>
>>> TaggedItem.objects.all()
<QuerySet [<TaggedItem: django>]>

clear() 方法可用於批量刪除實例的所有相關物件。

>>> b.tags.clear()
>>> b.tags.all()
<QuerySet []>
>>> TaggedItem.objects.all()
<QuerySet []>

定義設定了 related_query_nameGenericRelation 允許從相關物件進行查詢。

tags = GenericRelation(TaggedItem, related_query_name="bookmark")

這使得可以從 TaggedItemBookmark 進行篩選、排序和其他查詢操作。

>>> # Get all tags belonging to bookmarks containing `django` in the url
>>> TaggedItem.objects.filter(bookmark__url__contains="django")
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>

如果您不加入 related_query_name,您可以使用相同的方式手動進行查詢。

>>> bookmarks = Bookmark.objects.filter(url__contains="django")
>>> bookmark_type = ContentType.objects.get_for_model(Bookmark)
>>> TaggedItem.objects.filter(content_type__pk=bookmark_type.id, object_id__in=bookmarks)
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>

如同 GenericForeignKey 接受內容類型和物件 ID 欄位的名稱作為引數一樣,GenericRelation 也是如此;如果具有通用外鍵的模型使用非預設的欄位名稱,您必須在設定指向它的 GenericRelation 時傳遞這些欄位的名稱。例如,如果上面提到的 TaggedItem 模型使用名為 content_type_fkobject_primary_key 的欄位來建立其通用外鍵,那麼返回到它的 GenericRelation 需要像這樣定義:

tags = GenericRelation(
    TaggedItem,
    content_type_field="content_type_fk",
    object_id_field="object_primary_key",
)

另請注意,如果您刪除具有 GenericRelation 的物件,任何具有指向它的 GenericForeignKey 的物件也會被刪除。在上面的例子中,這表示如果刪除一個 Bookmark 物件,任何指向它的 TaggedItem 物件也會同時被刪除。

ForeignKey 不同,GenericForeignKey 不接受 on_delete 引數來自訂此行為;如果需要,您可以透過不使用 GenericRelation 來避免級聯刪除,並且可以透過 pre_delete 訊號提供替代行為。

通用關聯和聚合

Django 的資料庫聚合 API 可以與 GenericRelation 搭配使用。例如,您可以找出所有書籤有多少標籤。

>>> Bookmark.objects.aggregate(Count("tags"))
{'tags__count': 3}

表單中的通用關聯

django.contrib.contenttypes.forms 模組提供

class BaseGenericInlineFormSet[原始碼]
generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field='content_type', fk_field='object_id', fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True, min_num=None, validate_min=False, absolute_max=None, can_delete_extra=True)[原始碼]

使用 modelformset_factory() 返回一個 GenericInlineFormSet

如果 ct_fieldfk_field 與預設值不同,則必須提供它們,分別為 content_typeobject_id。其他參數與 modelformset_factory()inlineformset_factory() 中記錄的參數類似。

for_concrete_model 引數對應於 GenericForeignKey 上的 for_concrete_model 引數。

管理介面中的通用關聯

django.contrib.contenttypes.admin 模組提供了 GenericTabularInlineGenericStackedInline ( GenericInlineModelAdmin 的子類別 )

這些類別和函數使得在表單和管理介面中使用通用關聯成為可能。有關更多資訊,請參閱模型表單集管理介面文件。

class GenericInlineModelAdmin[原始碼]

GenericInlineModelAdmin 類別繼承自 InlineModelAdmin 類別的所有屬性。但是,它為處理通用關聯添加了一些自己的屬性

ct_field

模型上 ContentType 外鍵欄位的名稱。預設值為 content_type

ct_fk_field

表示相關物件 ID 的整數欄位的名稱。預設值為 object_id

class GenericTabularInline[原始碼]
class GenericStackedInline[原始碼]

分別具有堆疊式和表格式佈局的 GenericInlineModelAdmin 子類別。

GenericPrefetch()

Django 5.0 中的新增功能。
class GenericPrefetch(lookup, querysets, to_attr=None)[原始碼]

此查詢方式與 Prefetch() 相似,且僅應使用於 GenericForeignKeyquerysets 參數接受一個查詢集列表,每個查詢集對應一個不同的 ContentType。這對於具有非同質結果集的 GenericForeignKey 非常有用。

>>> from django.contrib.contenttypes.prefetch import GenericPrefetch
>>> bookmark = Bookmark.objects.create(url="https://djangoproject.dev.org.tw/")
>>> animal = Animal.objects.create(name="lion", weight=100)
>>> TaggedItem.objects.create(tag="great", content_object=bookmark)
>>> TaggedItem.objects.create(tag="awesome", content_object=animal)
>>> prefetch = GenericPrefetch(
...     "content_object", [Bookmark.objects.all(), Animal.objects.only("name")]
... )
>>> TaggedItem.objects.prefetch_related(prefetch).all()
<QuerySet [<TaggedItem: Great>, <TaggedItem: Awesome>]>
回到頂部