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

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页