Django內置模塊之contenttypes框架

前言

廢話不多說,直接打開你Django項目的settings.py文件,6大內置App之contenttypes框架

INSTALLED_APPS = [
    'django.contrib.admin', 
    'django.contrib.auth', 
    'django.contrib.contenttypes',  # 跟蹤Django中所有安裝的model
    'django.contrib.sessions',
    'django.contrib.messages',  
    'django.contrib.staticfiles',  
]

這個框架是必備的,因爲其他五個模塊會依賴他,比如:

  • Django的admin框架用它來記錄添加或更改對象的歷史記錄
  • Django的auth認證框架用他將用戶權限綁定到指定的模型

重要程度不言而喻,本文詳細介紹下文檔相關,以及如何在項目中使用。

這貨是啥?

contentypes他不是一箇中間件,不是視圖,也不是模板,而是一張管理全局模型的表,我們所創建的所有model,你需要執行makemigrationsmigrate操作,爲contenttype框架創建它需要的數據表,默認項目是sqlite的,我們來打開看看
在這裏插入圖片描述
表結構如右圖所示
在這裏插入圖片描述
一共三個字段

  • id:主鍵
  • app_label:模塊對應的app名字,python3 manage.py startapp xxx就是這裏創建的app名字
  • model:具體對應的model的名字

項目中創建了多少模型,這張表中就會出現多少條數據。

contenttypes框架的核心就是ContentType模型,他位於django.contrib.contenttypes.models.ContentType

ContentType模型的實例具有一系列方法,用於返回它們所記錄的模型類以及從這些模型查詢對象。ContentType 還有一個自定義的管理器,用於進行ContentType實例相關的ORM操作

也就是ContentTypeManager。它有很多方法,但是最常用的方法基本上就一個get_for_model(model,for_concrete_model = True),獲取模型或者模型類的實例,並返回表示該模型的ContentType實例。

一般情況下,我們需要兩個參數來相互綁定,一個就是ContentType實例,一個就是ContentType對應的model表中對應的object_id

>>> from django.contrib.auth.models import User
>>> ct0 = ContentType.objects.get_for_model(User)
>>> ct0.model
'user'
>>> ct0.app_label
'auth'
>>> 

通用模型關聯

ContentTypes框架最核心的功能應該就是連表,將兩個模型通過外鍵關聯起來。

  • 三個模型 A,B,C
  • 有一個特殊的模型優惠券需要關聯到其中之一
  • 不可同時關聯兩個及以上,只能有一個
    那麼傳統的做法可能設計出以下模型:
class A(models.Model):
    name = models.CharField(max_length=32)


class B(models.Model):
    name = models.CharField(max_length=32)


class C(models.Model):
    name = models.CharField(max_length=32)


class Tag(models.Model):
    name = models.CharField(max_length=32)
    a = models.ForeignKey(A, blank=True,null=True, on_delete=models.DO_NOTHING)
    b = models.ForeignKey(B, blank=True,null=True, on_delete=models.DO_NOTHING)
    c = models.ForeignKey(C, blank=True,null=True, on_delete=models.DO_NOTHING)

問題是Tag中三個外鍵,他們必須允許爲空,然後再操作的時候,你還得注意,不能同時對a,b,c字段賦值,只能賦值最多一個。

如果是通用Tag,那麼所有的ForeignKey爲null,如果僅限某些模型,那麼對應商品ForeignKey記錄該模型的id,不相關的記錄爲null。但是這樣做是有問題的:實際中模型越來越多,而且很可能還會持續增加,那麼Tag表中的外鍵將越來越多,但是每條記錄僅使用其中的一個或某幾個外鍵字段。

因此這裏就有個contenttype的框架設計,設計代碼如下:

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

class A(models.Model):
    name = models.CharField(max_length=32)


class B(models.Model):
    name = models.CharField(max_length=32)


class C(models.Model):
    name = models.CharField(max_length=32)


class Tag(models.Model):
    name = models.CharField(max_length=32)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

首先A,B,C,Tag這幾個模型會出現的Contenttype表中,相當於在中間表中就存在了。那麼當A,B,C需要關聯Tag的時候,通過中間表的方式實現統一關聯

案例一(模擬通用Tag組件)

1.創建App
python manage.py startapp tags

2.編寫模型

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

# Create your models here.

class TaggedItems(models.Model):
    tag = models.CharField(max_length=10)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

如果沒有中間表模型,那麼普通的ForeignKey字段只能指向另外一個唯一的模型,這意味着如果TaggedItems模型使用了ForeignKey,則必須選擇一個且只有一個模型來存儲標籤。
contenttypes框架提供了一個特殊的字段類型(GenericForeignKey),解決了這個問題,可以關聯任何模型,但是該字段不會入庫。

通俗點講:
一個User模型,一個Tag模型,如果User需要關聯對應的Tag,那麼先理解爲一對多,在Tag爲多頭的那邊創建外鍵,如果沒有中間表,就必須指定外鍵給User模型,如果又來一個Subject模型,也需要關聯Tag,要麼你再創建一個Tag模型,要麼你在原來的Tag模型裏面根據上面的錯誤案例中寫多個外鍵關聯,通過代碼去控制到底關聯到User還是Subject,但是你想想如果有茫茫多模型需要關聯Tag功能,這就很糟糕了。所以這就是ContentType中間表的作用。

3.遷移文件,執行操作
python manage.py makemigrationspython manage.py shell

>>> from django.contrib.auth.models import User
>>> mkj = User.objects.all()[0]
>>> mkj.username
'mikejing'
>>> from tags.models import TaggedItems
>>> from django.contrib.contenttypes.models import ContentType
// 方法一
>>> tg, created = TaggedItems.objects.get_or_create(content_type=ct, object_id=mkj.pk)
>>>> tg.tag = '內測玩家'
>>> tg.save()
>>> tg.content_object
<User: mikejing>

// 方法二
>>> mqs = User.objects.all()[2]
>>> mqs.username
'miqishu'
>>> tg3 = TaggedItems(content_object=mqs, tag='神玩家')
>>> tg3
<TaggedItems: TaggedItems object (None)>
>>> tg3.tag = '神玩家'
>>> tg3.save()
>>> tg3.content_object
<User: miqishu>

上面有兩個方法實例化一個標籤類
先看方法二:

tg3 = TaggedItems(content_object=mqs, tag='神玩家'),這裏沒有提供content_type字段關聯的ContentType的id,沒有提供tg3關聯的User的id,提供了一個content_object的對象mqs

再看方法一
tg, created = TaggedItems.objects.get_or_create(content_type=ct, object_id=mkj.pk)這個方法也是我用的最多的方法之一,這裏兩個方法只是做個比較。

這就是在前面我們說的content_object = GenericForeignKey('content_type', 'object_id'),這個字段的作用!它不參與字段的具體內容生成和保存,只是爲了方便ORM操作!它免去了我們通過mqs用戶取查找自己的id,以及查找ContentType表中對應模型的id的過程!雖然方法二看起來參數更少,但是方法一寫起來感覺更容易理解。而且數據庫中確實不會存在content_object,該字段只是方便ORM操作而已。
在這裏插入圖片描述

看到這裏,其實ContentType模型就是一張中間表,方便模型之前解耦,抽離公共的功能,可以通過ContentType關聯任何模型

常用知識點:
正常情況下我們獲取ContentType很常用,而且有三種方式

  • ContentType.objects.get(model='user')注意是小寫的類
  • ContentType.objects.get_for_model(mqs) mqs是User實例對象
  • ContentType.objects.get_for_model(User) User就是模型類

案例二(博客模型和閱讀數關聯)

首先我們建立了一個博客模型,每個博客有博客類型,這個博客類型不會關聯給其他模型,因此就根據上面的經驗,不需要用到ContentType,所以按一般的寫法,寫在一起,models代碼如下:

class BlogType(models.Model):
    type_name = models.CharField(max_length=15)

    def __str__(self):
        return self.type_name


class Blog(models.Model,ReadNumExtension):
    title = models.CharField(max_length=50)
    blog_type = models.ForeignKey(BlogType, on_delete=models.CASCADE)
    content = RichTextUploadingField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    create_time = models.DateTimeField(auto_now_add=True)
    last_update_time = models.DateTimeField(auto_now=True)
    # readDetails = GenericRelation(ReadNumDetail)  # 反向關聯獲取


    def __str__(self):
        return "<Blog:%s>" % self.title

博客和博客類型的關係很簡單明瞭,但是如果你要給博客設計個閱讀模型,而且這個閱讀模型最好是通用的,可以給其他模型比如評論模型,商品模型使用,那麼這個閱讀統計的模型,不可能再和Blog綁在一起,這裏理所當然的需要用到ContentType

和上邊一樣,先創建一個App,結構如下
在這裏插入圖片描述
models代碼:

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db.models.fields import exceptions
from django.utils import timezone
from django.db import models


class ReadNum(models.Model):
    read_num = models.IntegerField(default=0)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')


class ReadNumDetail(models.Model):
    read_date = models.DateField(default=timezone.now)
    read_num = models.IntegerField(default=0)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

class ReadNumExtension():
    def get_read_num(self):
        try:
            ct = ContentType.objects.get_for_model(self)
            readnum = ReadNum.objects.get(content_type=ct, object_id=self.pk)
            return readnum.read_num
        except exceptions.ObjectDoesNotExist:
            return 0

ReadNum類
用來記錄總閱讀計數

ReadNumDetail
多了個Date字段,用來記錄不同時段的閱讀技術

ReadNumExtension
方便外部繼承,通過content_typeobject_id來獲取對應關聯的閱讀數

這裏以博客爲例,當我們外部的路由經過mysite路由被轉發到blogs.urls.py,最終指定到views.py的對應的方法,然後進行閱讀標記。

def read_statistics_by_every_read(request, obj):
    ct = ContentType.objects.get_for_model(obj)
    key = "%s_%s_read" % (ct.model, obj.pk)
    if not request.COOKIES.get(key):
        readout, created = ReadNum.objects.get_or_create(content_type=ct, object_id=obj.pk)
        readout.read_num += 1
        readout.save()

        date = timezone.now().date()
        print("當前時間%s" % timezone.now())
        read_detail, created = ReadNumDetail.objects.get_or_create(content_type=ct, object_id=obj.pk, read_date=date)
        read_detail.read_num += 1
        read_detail.save()

    return key

該方法也是提供自read_statistics模塊,外部只需要提供request和model即可。這麼看來,我們現在所有的關聯到閱讀技術的App,用到計數功能都是通過read_statistics模塊提供的API,不需要引入太多模塊和邏輯。

  • 閱讀計數統計,只需要調用read_statistics_by_every_read傳入request和需要綁定的model
  • 閱讀技術展示,我們現在這都是通過views.py去做邏輯,最終通過模板去渲染數據,那麼模板如何做到也從read_statistics_by_every_read調用接口呢,肯定是有方法的,這裏你得熟悉(模板標籤和模板過濾器)TODO,我們這裏就需要自定義模板標籤了,傳送門TODO
def blog_details(request, blog_pk):
    blog = get_object_or_404(Blog, pk=blog_pk)
    key = read_statistics_by_every_read(request, blog)
    pre_blog = Blog.objects.filter(create_time__gt=blog.create_time).last()
    next_blog = Blog.objects.filter(create_time__lt=blog.create_time).first()
    context = {}
    context['blog'] = blog
    context['previous_blog'] = pre_blog
    context['next_blog'] = next_blog
    response = render(request, 'blogs/blog_detail.html', context)
    response.set_cookie(key, True)
    return response

博客視圖渲染html,把博客對象傳進去,不需要關注其他東西,看看模板標籤部分代碼

{% extends 'base.html' %}
# 系統加載靜態資源的標籤
{% load staticfiles %}
# 以下幾個都是自定義模板標籤
{% load comment_tags %}
{% load read_tags %}
{% load like_tags %}
{% block cssstyle %}
    <link rel="stylesheet" href="{% static 'blog_css/blog.css' %}">
    <script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
    <script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
{% endblock %}

......

{% block content %}
    <div class="container">
        <div class="row">
            <div class="col-xs-10 col-xs-offset-1">
                <h3>{{ blog.title }}</h3>
                <ul class="blog_detail_ul">
                    <li>作者:{{ blog.author }}</li>
                    <li>分類:<a href="{% url 'blogs_module:blog_types' blog.blog_type.pk %}">{{ blog.blog_type }}</a></li>
                    <li>日期:{{ blog.create_time|date:"Y-m-d H:i:s" }}</li>
                    <li>閱讀:{% get_total_read_count blog %}</li>
                    <li>評論:({% get_comment_count blog %})</li>
                </ul>
                ......

可以看到一個系統自帶的另一個框架'django.contrib.staticfiles',這裏提供了一個文件夾,按官方文檔介紹,相當於引入了templatetags模塊,裏面就有staticfiles.py,提供瞭如下模板標籤

@register.tag('static')
def do_static(parser, token):
    warnings.warn(
        '{% load staticfiles %} is deprecated in favor of {% load static %}.',
        RemovedInDjango30Warning,
    )
    return _do_static(parser, token)

所以我們看到html模板頂部可以直接{% load staticfiles %}引入,然後可以指定css或者js的文件路徑,引入如下

{% block cssstyle %}	
	// 使用static標籤引入
    <link rel="stylesheet" href="{% static 'blog_css/blog.css' %}">
    <script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
    <script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
{% endblock %}

這裏不詳細介紹模板標籤和模板過濾器了,單獨寫個博客介紹,這裏就直接在我們剛纔的html中,直接用read_statistics提供的模板API。
在這裏插入圖片描述
通過綁定的對象,獲取到ContentTypeobject_id,獲取閱讀信息

from django import template
from django.contrib.contenttypes.models import ContentType
from ..models import ReadNum
from django.db.models.fields import exceptions

register = template.Library()


@register.simple_tag
def get_total_read_count(obj):
    try:
        ct = ContentType.objects.get_for_model(obj)
        readnum = ReadNum.objects.get(content_type=ct, object_id=obj.pk)
        return readnum.read_num
    except exceptions.ObjectDoesNotExist:
        return 0

使用總結

  1. contenttype的存在,提供了一張中間表,解耦了各個模塊,模塊間可以通過這個ContentType進行萬能組合。
  2. 綁定的模塊(閱讀模塊)接收被綁定模塊(博客模塊)的對象,可以通過對象得到content_typeobject_id,可以很容易獲取到綁定的模塊數據
  3. 被綁定模塊可以在業務中觸發數據綁定,直接引入方法即可,被綁定模塊的對象
  4. html模板標籤接收被綁定模塊數據,可以通過在綁定模塊中創建自定義模板標籤模塊templatetags,註冊模板標籤或者過濾器,提供接口

以上都是正向查詢數據,那麼必然有反向查詢數據。

GenericRelation反向查詢

既然前面使用GenericForeignKey字段可以幫我們正向查詢關聯的對象,那麼就必然有一個對應的反向關聯類型,也就是GenericRelation字段類型。

正常的外鍵關係

以我們的項目爲例,博客和博客類型,上面有Model。

正向獲取所有的Blog和對應的BlogType
ForeignKey

>>> from blogs.models import Blog
>>> from blogs.models import Blog, BlogType
>>> blog = Blog.objects.all()[0]
>>> blog.blog_type
<BlogType: 隨筆>

反向通過BlogType獲取所有的Blog
blog_set

>>> from blogs.models import Blog, BlogType
>>> dir(BlogType) # 查看有那個屬性,我們需要 blog_set
>>> bt = BlogType.objects.all()[0]
>>> bt.blog_set.all()
<QuerySet [<Blog: <Blog:每日一圖(01)>>, <Blog: <Blog:測試時區>>, <Blog: <Blog:每日一圖>>, <Blog: <Blog:完成第一階段>>, <Blog: <Blog:好多文章>>]>

Contenttypes模塊正反方式

正向通過Blog獲取閱讀數量
ForeignKeyGenericForeignKey

>>> from django.contrib.contenttypes.fields import GenericForeignKey
>>> from django.contrib.contenttypes.models import ContentType
>>> from read_statistics.models import ReadNum
>>> readNum = ReadNum.objects.get(content_type=contenttype, object_id=blog.pk)
>>> readNum.read_num
24
>>> readNum.content_type
<ContentType: blog>
>>> readNum.content_object
<Blog: <Blog:每日一圖(01)>>

關聯的對象反向查詢對象本身
GenericRelation
很常見的功能,比如你要查出博客對應的7天熱門,這個時候我們就需要用到ReadNumDetail模型,針對每天不同文章的閱讀量進行統計,那麼你根據正向查詢,查出來的都是ReadNumDetail對象,如果要查詢博客信息,還得通過字段在查回去,然後合併相同的博客,這樣子就很繁瑣。

class Blog(models.Model,ReadNumExtension):
    title = models.CharField(max_length=50)
    blog_type = models.ForeignKey(BlogType, on_delete=models.CASCADE)
    content = RichTextUploadingField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    create_time = models.DateTimeField(auto_now_add=True)
    last_update_time = models.DateTimeField(auto_now=True)
    readDetails = GenericRelation(ReadNumDetail)  # 反向關聯獲取

打開反向關聯,可以直接使用該字段。

>>> from blogs.models import Blog
>>> from blogs.models import Blog, BlogType
>>> blog = Blog.objects.all()[0]
>>> blog.readDetails.all() # 這裏就是反向了,因此不能直接blog.readDetails拿
<QuerySet [<ReadNumDetail: ReadNumDetail object (43)>, <ReadNumDetail: ReadNumDetail object (44)>, <ReadNumDetail: ReadNumDetail object (49)>, <ReadNumDetail: ReadNumDetail object (53)>, <ReadNumDetail: ReadNumDetail object (60)>, <ReadNumDetail: ReadNumDetail object (63)>, <ReadNumDetail: ReadNumDetail object (64)>]>
>>> blog.readDetails.all().values()
<QuerySet [{'id': 43, 'read_date': datetime.date(2019, 9, 26), 'read_num': 3, 'content_type_id': 8, 'object_id': 44}, {'id': 44, 'read_date': datetime.date(2019, 9, 27), 'read_num': 2, 'content_type_id': 8, 'object_id': 44}, {'id': 49, 'read_date': datetime.date(2019, 9, 29), 'read_num': 15, 'content_type_id': 8, 'object_id': 44}, {'id': 53, 'read_date': datetime.date(2019, 9, 30), 'read_num': 1, 'content_type_id': 8, 'object_id': 44}, {'id': 60, 'read_date': datetime.date(2019, 10, 4), 'read_num': 1, 'content_type_id': 8, 'object_id': 44}, {'id': 63, 'read_date': datetime.date(2019, 10, 27), 'read_num': 1, 'content_type_id': 8, 'object_id': 44}, {'id': 64, 'read_date': datetime.date(2019, 10, 29), 'read_num': 1, 'content_type_id': 8, 'object_id': 44}]>

這樣就能拿出對應Blog所關聯到的所有ReadNumDetail模型。

再來個案例:拿到博客七天數據

import datetime
from django.utils import timezone
from django.db.models import Sum
def get_seven_hots_read_statistics():
    today = timezone.now().date()
    seven_days = today - datetime.timedelta(days=7)
    results = Blog.objects \
        .filter(readDetails__read_date__lt=today, readDetails__read_date__gte=seven_days) \
        .values('id', 'title') \
        .annotate(read_num_sum=Sum('readDetails__read_num')) \
        .order_by("-read_num_sum")
    print('七天數據:%s' % results)
    return results[:7]

這裏我們由於用了GenericRelation,可以用作字段進行過濾,上面的意思可以直接翻譯成SQL就很清晰

>>> str(results.query)
'SELECT "blogs_blog"."id", "blogs_blog"."title",
SUM("read_statistics_readnumdetail"."read_num") AS "read_num_sum" FROM
"blogs_blog" INNER JOIN "read_statistics_readnumdetail" ON ("blogs_blog"."id" =
"read_statistics_readnumdetail"."object_id" AND
("read_statistics_readnumdetail"."content_type_id" = 8)) WHERE
("read_statistics_readnumdetail"."read_date" >= 2019-10-23 AND
"read_statistics_readnumdetail"."read_date" < 2019-10-30) GROUP BY
"blogs_blog"."id", "blogs_blog"."title" ORDER BY "read_num_sum" DESC'

總結

1.contenttypes 是Django內置的一個應用,可以追蹤項目中所有app和model的對應關係,並記錄在ContentType表中。

2.models.py文件的表結構寫好後,通過makemigrationsmigrate兩條命令遷移數據後,在數據庫中會自動生成一個django_content_type表:

3.ContentType只運用於1對多的關係!!!並且多的那張表中有多個ForeignKey字段。比如商品和優惠券,文章博客和閱讀數,優惠券和閱讀數不抽離出來,本身會帶有茫茫多的外鍵做邏輯,因此有了這個中間表來管理多個外鍵的關係。當一張表和多個表ForeignKey關聯,並且多個ForeignKey中只能選擇其中一個或其中n個時,可以利用contenttypes app,只需定義三個字段就搞定!

4.其實,看到這裏,已經說明了ContentType模型就是一張中間表!方便App解耦,適應以上的幾種場景。

參考文章:
文章1
文章2

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