前言
廢話不多說,直接打開你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,你需要執行makemigrations
和migrate
操作,爲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 makemigrations
和 python 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_type
和object_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。
通過綁定的對象,獲取到ContentType
和object_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
使用總結
contenttype
的存在,提供了一張中間表,解耦了各個模塊,模塊間可以通過這個ContentType
進行萬能組合。- 綁定的模塊(閱讀模塊)接收被綁定模塊(博客模塊)的對象,可以通過對象得到
content_type
和object_id
,可以很容易獲取到綁定的模塊數據 - 被綁定模塊可以在業務中觸發數據綁定,直接引入方法即可,被綁定模塊的對象
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獲取閱讀數量
ForeignKey
— GenericForeignKey
>>> 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
文件的表結構寫好後,通過makemigrations
和migrate
兩條命令遷移數據後,在數據庫中會自動生成一個django_content_type
表:
3.ContentType
只運用於1對多的關係!!!並且多的那張表中有多個ForeignKey字段。比如商品和優惠券,文章博客和閱讀數,優惠券和閱讀數不抽離出來,本身會帶有茫茫多的外鍵做邏輯,因此有了這個中間表來管理多個外鍵的關係。當一張表和多個表ForeignKey關聯,並且多個ForeignKey中只能選擇其中一個或其中n個時,可以利用contenttypes app
,只需定義三個字段就搞定!
4.其實,看到這裏,已經說明了ContentType
模型就是一張中間表!方便App解耦,適應以上的幾種場景。