Django中的contenttypes框架
使用django-admin startproject {項目名}後,
# settings.py
DJANGO_APPS = [
...
"django.contrib.contenttypes",
...
]
並且在生成數據庫時,會默認生成一張django_content_type表,如下所示
這張表記錄了所有 模型類名字 與 所屬的應用
id | app_label | model |
---|---|---|
1 | 應用名 | 模型類 類名 |
2 | … | … |
3 | … | … |
來看一下此模型類的 源碼(重要部分):
from django.contrib.contenttypes.models import ContentType
class ContentType(models.Model):
# 應用名
app_label = models.CharField(max_length=100)
# 模型類名
model = models.CharField(_('python model class name'), max_length=100)
# 自定義的 模型類的管理器
objects = ContentTypeManager()
# 元數據
class Meta:
verbose_name = _('content type')
verbose_name_plural = _('content types')
db_table = 'django_content_type'
unique_together = (('app_label', 'model'),)
def __str__(self):
return self.name
@property # 將函數裝華爲屬性
def name(self):
'''獲取 指定self.app_label, self.model 模型類的 類名'''
model = self.model_class()
if not model:
return self.model
# 獲取到 model 則將 model元數據的 verbose_name 返回
return str(model._meta.verbose_name)
def model_class(self):
"""Return the model class for this type of content."""
try:
# django.apps 模塊的public的方法,返回與app_label, model對應的模型類
return apps.get_model(self.app_label, self.model)
except LookupError:
return None
PS: 在Python代碼中,可以使用django.apps.apps引用上述settings.py中的INSTALLED_APPS變量。django.apps.apps也被稱爲應用註冊器
綜上所述可以概括爲:
ContentType 是由Djnago框架提供的一個核心功能,對當前項目中所有基於Django驅動的model(繼承自models.Model並且寫在modles.py中)提供了更高層次的model接口
那麼生成這張表有什麼作用呢?
-
Django權限管理中的Permission藉助ContentType 實現了對任意models的權限操作
-
ContentType的通用類型 - GenericRelation
ContentType的通用類型 - GenericRelation
什麼是GenericRelation和GenericForeignKey
假設現在有一個 博客項目 開發模型類時,有文章、圖片等等 都需要可評論(comment)
簡單代碼如下:
from django.db import models
from django.contrib.auth.models import User
# 博客
class Post(models.Model):
author = models.ForeignKey(User,on_deleter=models.CASCADE)
body = models.TextField(blabk=True)
# 文章
class Articles(models.Model):
author = models.ForeignKey(User,on_deleter=models.CASCADE)
body = models.TextField(blabk=True)
# 圖片
class Pirture(models.Model):
author = models.ForeignKey(User,on_deleter=models.CASCADE)
body = models.TextField(blabk=True)
# 評論
class Comment(models.Model):
author = models.ForeignKey(User,on_deleter=models.CASCADE)
body = models.TextField(blabk=True)
# 一一關聯到外鍵 這種定義,任意生效其他處需要設置爲空 只有一個字段有值(評論post時,pic和article爲空),
post = models.ForeignKey(Post,on_deleter=models.CASCADE,null=True)
pic = models.ForeignKey(Pirture,on_deleter=models.CASCADE,null=True)
article = models.ForeignKey(Articles,on_deleter=models.CASCADE,null=True)
此時如果增加模型類如視頻等,Comment模型類中又要增加外鍵字段 -> 擴展性差且無法做到模型類中每一字段都有意義
如果將每一種評論都單獨分離出來,變爲一個個模型類,這樣做解決了模型類中每一字段都有意義的問題,但同樣擴展性很差
使用ContentType 將Comment模型類變爲通用模型類
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
# 要找到一條表記錄,需要模型類名,模型名,和主鍵id名
# 而剛好content_type 都可以實現
# 將Comment 變爲通用模型類
class Comment(models.Model):
author = models.ForeignKey(User,on_delete=models.CASCADE)
body = models.TextField(blabk=True)
# 外鍵關聯到ContentType,獲得app_lable ,model 數據
content_type = models.ForeignKey(ContentType,models.CASCADE)
# 獲取主鍵id - 其他表的主鍵id 要考慮類型
object_id = models.CharField()
# "content_type","object_id" 可省略
# !!注意:GenericForeignKey 默認爲刪除級聯,但不支持on_delete參數
content_object = GenericForeignKey("content_type","object_id")
# 之後再文章、圖片等模型類中添加comments = GenericRelation()
from django.contrib.contenttypes.fields import GenericRelation
class Post(models.Model):
author = models.ForeignKey(User,on_deleter=models.CASCADE)
body = models.TextField(blabk=True)
# 關聯到Comment 模型類
comments = GenericRelation(Comment)
class Articles(models.Model):
author = models.ForeignKey(User,on_deleter=models.CASCADE)
body = models.TextField(blabk=True)
comments = GenericRelation(Comment)
class Pirture(models.Model):
author = models.ForeignKey(User,on_deleter=models.CASCADE)
body = models.TextField(blabk=True)
comments = GenericRelation(Comment)
# 總結:
# 想讓那張表變爲通用模型類類
# 在模型類中定義
content_type = models.ForeignKey(ContentType,models.CASCADE)
object_id = models.IntegerField()
content_object = GenericForeignKey("content_type","object_id")
# 再讓其模型類使用 GenericRelation 關聯到 通用模型類類
字段名 = GenericRelation(通用模型類名)
來看官方文檔說明:
https://docs.djangoproject.com/en/2.2/ref/contrib/contenttypes/
並用於 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
一個常見的ForeignKey只能“指向”另一個模型,這意味着如果該TaggedItem模型使用ForeignKey, 則必須選擇有且只有一個模型來存儲標籤。contenttypes應用程序提供了一個特殊的字段類型(GenericForeignKey),可以解決該問題,並允許與任何模型建立關係:
類GenericForeignKey
設置三部分 GenericForeignKey:
-
將您的模型ForeignKey 設爲ContentType。該字段的常用名稱是“content_type”。
-
給您的模型設置一個字段,該字段可以存儲您將要關聯的模型中的主鍵值。對於大多數模型,這意味着 PositiveIntegerField。該字段的常用名稱是“ object_id”。
- PositiveIntegerField,但僅允許在特定點(與數據庫有關)下的值。從0到的值32767在Django支持的所有數據庫中都是安全的。
-
給您的模型一個 GenericForeignKey,並向其傳遞上述兩個字段的名稱。如果將這些字段分別命名爲“ content_type”和“ object_id”,則可以忽略這些-這些是默認字段名稱 GenericForeignKey。
-
for_concrete_model
如果爲False,則該字段將能夠引用代理模型。默認值爲True。這將for_concrete_model論點反映到 get_for_model() -
主鍵類型兼容性
“ object_id”字段不必與相關模型上的主鍵字段具有相同的類型,但通過其get_db_prep_value()方法,其主鍵值必須可強制爲與“ object_id”字段相同的類型 。
例如,如果要允許具有主鍵字段IntegerField或 CharField主鍵字段的模型的通用關係 ,則可以將其CharField
用於模型上的“ object_id”字段,因爲可以將整數強制爲get_db_prep_value()。
總結: 推薦使用CharField 作爲object_id 字段類型
爲了獲得最大的靈活性,您可以使用 TextField未定義最大長度的,但這可能會導致嚴重的性能損失,具體取決於您的數據庫後端。
沒有最適合領域類型的“一刀切”解決方案。您應該評估預期要指向的模型,並確定哪種解決方案對您的用例最有效。
與正常使用的API類似的API ForeignKey; 每個對象TaggedItem都有一個content_object返回與其相關的對象的字段,您也可以分配給該字段或在創建時使用TaggedItem:
# 導入User模型類
>>> from django.contrib.auth.models import User
# 在User表中創建一個用戶 username='Guido' 返回查詢集 並賦值給變量guido
>>> guido = User.objects.get(username='Guido')
# 創建TaggedItem 並將content_object關聯爲guido
>>> t = TaggedItem(content_object=guido, tag='bdfl')
# 數據庫保存
>>> t.save()
# 通過外鍵查詢 關聯的數據
>>> t.content_object
<User: Guido>
如果刪除了相關對象,則content_type和object_id字段將保持設置爲原始值,並GenericForeignKey返回 None:
>>> guido.delete()
>>> t.content_object # returns None
由於GenericForeignKey ,不能直接使用的過濾器(filter() 以及exclude() 等database API)。由於 GenericForeignKey是一個不常見的文件對象,這些例子將不工作:
# This will fail
>>> TaggedItem.objects.filter(content_object=guido)
# This will also fail
>>> TaggedItem.objects.get(content_object=guido)
逆向通用關聯
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://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>]>
定義GenericRelation用 related_query_nameset 允許從相關對象中查詢:
tags = GenericRelation(TaggedItem, related_query_name='bookmark')
使用TaggedItem filtering, ordering,或者其他查詢對bookmark 進行操作
>>> # 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 接受content-type和object-ID字段的名稱作爲參數一樣, 如果具有通用外鍵的模型爲這些字段使用非默認名稱,則必須在GenericRelation爲其設置時傳遞字段名稱 。例如,如果TaggedItem上面提到的模型使用了名爲的字段content_type_fk並 object_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 信號提供替代行爲。
蹩腳的翻譯,如妨礙閱讀請移步官方文檔:https://docs.djangoproject.com/en/2.2/ref/contrib/contenttypes/
!!!