废话不多说,直接打开你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',
]
这个框架是必备的,因为其他五个模块会依赖他,比如:
重要程度不言而喻,本文详细介绍下文档相关,以及如何在项目中使用。
contentypes
他不是一个中间件,不是视图,也不是模板,而是一张管理全局模型的表,我们所创建的所有model,你需要执行makemigrations
和migrate
操作,为contenttype框架创建它需要的数据表,默认项目是sqlite的,我们来打开看看
表结构如右图所示
一共三个字段
python3 manage.py startapp xxx
就是这里创建的app名字项目中创建了多少模型,这张表中就会出现多少条数据。
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
框架最核心的功能应该就是连表,将两个模型通过外键关联起来。
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的时候,通过中间表的方式实现统一关联
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 "" % self.title
博客和博客类型的关系很简单明了,但是如果你要给博客设计个阅读模型,而且这个阅读模型最好是通用的,可以给其他模型比如评论模型,商品模型使用,那么这个阅读统计的模型,不可能再和Blog绑在一起,这里理所当然的需要用到ContentType
。
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
和需要绑定的modelviews.py
去做逻辑,最终通过模板去渲染数据,那么模板如何做到也从read_statistics_by_every_read
调用接口呢,肯定是有方法的,这里你得熟悉(模板标签和模板过滤器)TODO,我们这里就需要自定义模板标签了,传送门TODOdef 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
,注册模板标签或者过滤器,提供接口以上都是正向查询数据,那么必然有反向查询数据。
既然前面使用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:好多文章>>]>
正向通过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解耦,适应以上的几种场景。
参考文章:
文章1
文章2