理解Django的通用外鍵 -> GenericForeignKey, GenericRelation

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章