ContentTypes及其QuerySet查询

  • 发布时间:2017年4月26日 14:35
  • 作者:杨仕航
  • 分类标签: Django
  • 阅读(8951)
  • 评论(1)

有关Django的QuerySet查询,前面已经写了两篇文章:

1)QuerySet查询基础

2)外键的QuerySet查询

本篇博文讲解一下较为特殊的ContentTypes。


1、ContentTypes是什么

ContentTypes是一种表结构。我将从数据库层面直接讲解该类型,直接了解本质。

举个栗子,我有个两个模型博客(Blog)和专题(Subject)。为了方便讲解,简化模型如下:

#coding:utf-8
from django.db import models

class Blog(models.Model):
    caption = models.CharField(max_length=50)
    
class Subject(models.Model):
    caption = models.CharField(max_length=50)


另外,这两个模型的数据我都需要记录阅读次数或者记录其他东西。

为了保证这两个模型干净,不直接添加过多的附加字段在模型中。这种情况一般新建其他模型作为拓展表。那么我们可创建新的模型并加入外键关联到对应的表。如下:

#coding:utf-8
from django.db import models

class BlogView(models.Model):
    blog = models.OneToOneField(Blog) #关联Blog,一对一外键
    view_num = models.IntegerField(default=0)
    
class SubjectView(models.Model):
    subject = models.OneToOneField(Blog) #关联Subject,一对一外键
    view_num = models.IntegerField(default=0)


有没发现,代码重复了!若还有其他模型需要加入阅读次数记录,岂不是还要继续添加一样的代码。

现在该代码形成的表结构如下:

20170426/20170426110902499.png


很明显这种结构冗余了,可以修改为如下结构:

20170426/20170426111047217.png


为了区分View表的记录是对应那个模型。需要加入一个字段记录该条数据对应的模型。

20170426/20170426111446638.png


如上图的结构可以记录什么模型(object_type),该模型哪条数据(object_id)和其他对应该模型的数据。通过这两个字段,我们可以很清楚知道分别对应那个模型的数据。只用了两个字段解决上面冗余的表结构。这个object_type和object_id就是我们这里所说的ContentTypes

创建带有ContentTypes字段的模型如下(目前我使用的版本是django 1.9。1.6之前的版本可能引用的类不同):

#coding:utf-8
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

#阅读数量记录模型
class ViewNum(models.Model):
    #Content Type字段
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    
    content_object = GenericForeignKey(
        ct_field = "content_type",
        fk_field = "object_id"
    )

    #其他字段
    view_num = models.IntegerField(default=0)


此处,真正被创建的字段有content_type、object_id和view_num。而content_object是content_type和object_id的组合,方便我们使用。

ps:创建新模型记得同步数据库。


2、ContentTypes读写

创建模型是为了保存和读取数据。上面我们创建了有ContentTypes类型字段的模型。写入数据方法如下:

#获取对应的博客
blog = Blog.objects.get(id=1)

#创建1条记录
view = ViewNum(content_object=blog, view_num=1)
view.save()


还有另外一种写入方式:

from django.contrib.contenttypes.models import ContentType

#获取对应的博客
blog = Blog.objects.get(id=1)

#获取Blog的ContentType类型
blog_content_type = ContentType.objects.get_for_model(Blog)

#使用具体模型对象也行
#blog_content_type = ContentType.objects.get_for_model(blog)

#创建1条记录
view = ViewNum(content_type=blog_content_type, object_id=blog.id, view_num=1)
view.save()


该方式稍微麻烦点,通常用于读取数据。因为读取数据无法直接使用content_object作为条件。因为content_object字段是GenericForeignKey。该字段没有真正被创建,不是普通意义的字段。无法被解析成SQL语句。所以我们读取对应的数据方法如下:

from django.contrib.contenttypes.models import ContentType

#获取对应的博客
blog = Blog.objects.get(id=1)

#获取Blog的ContentType类型
blog_content_type = ContentType.objects.get_for_model(Blog)

#读取数据
view_nums = ViewNum.objects.filter(content_type=blog_content_type, object_id=blog.id)


如果是简单的读取数据,这种方法倒是无伤大雅。若要查询稍微复杂一点的数据,例如:

1)获取阅读次数小于等于10的Blog

2)获取Blog标题包含django的阅读记录

3)Blog按照阅读次数倒序排序(由大到小)

这些都是实际可能会碰到的问题,方法见下一节。


3、ContentTypes的QuerySet查询

为了能够顺畅的关联Blog和ViewNum两个模型,可以在Blog模型加入一个特殊的外键。修改Blog模型如下:

#coding:utf-8
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation

class Blog(models.Model):
    caption = models.CharField(max_length=50)
    
    #关联ViewNum
    view_num = GenericRelation(ViewNum, related_query_name='blogs')


该操作无需同步数据库。我们可以使用外键的QuerySet查询提到的获取全部字段的方法查看有ViewNum多了哪些字段。

Print(ViewNum._meta.get_fields())


得到如下结果:

<GenericRel: blog.blog>, 
<django.db.models.fields.AutoField: id>, 
<django.db.models.fields.related.ForeignKey: content_type>, 
<django.db.models.fields.PositiveIntegerField: object_id>, 
<django.db.models.fields.IntegerField: view_num>, 
<django.contrib.contenttypes.fields.GenericForeignKey object at 0x03BE2BF0>


多了第1条GenericRel的引用。我们在Blog模型可以通过加入的view_num获取ViewNum模型的数据。也可以在ViewNum模型通过blogs(related_query_name设定的名称)获取Blog模型的相关数据。

例如,获取Blog id为1的阅读数据:

blog = Blog.objects.get(id=1)
blog.view_num.all() #获取全部的阅读数据



讲到这里,回头看看上一节抛出的3个问题。

1)获取阅读次数小于等于10的Blog

我们已经在Blog模型加入了外键view_num,参考外键的QuerySet查询一文,实现代码如下:

qs = Blog.objects.filter(view_num__view_num__lte=10)

#输出SQL语句
print(qs.query)


第1个view_num是Blog模型的view_num字段,第2个view_num是ViewNum模型的view_num。

通过Blog模型的view_num外键访问ViewNum模型。

__lte是条件修饰,小于等于的意思。具体知识可看QuerySet查询基础

得到SQL语句如下:

SELECT "blog_blog"."id", "blog_blog"."caption"
FROM "blog_blog" 
INNER JOIN "view_record_viewnum" 
    ON (
        "blog_blog"."id" = "view_record_viewnum"."object_id" 
    AND 
        "view_record_viewnum"."content_type_id" = 9
    )
WHERE "view_record_viewnum"."view_num" <= 10


SQL语句看不懂可以略过,其中着重看inner join 和 where条件部分。inner join 将ViewNum模型和Blog模型关联。content_type_id = 9 是对应Blog模型的ContentTypes id值。


2)获取Blog标题包含django的阅读记录

第1小问,通过Blog访问ViewNum。第2小问是演示如何通过ViewNum访问Blog。

上面模型中已经加入ContentTypes引用字段,引用名称为blogs。则使用代码如下:

qs = ViewNum.objects.filter(blogs__caption__icontains='django')


filter中的blogs__caption__icontains意思是判断Blog模型的caption字段,条件为是否包含django字样。

可自行输入SQL语句参考,或看具体的返回结果。


3)Blog按照阅读次数倒序排序(由大到小)

这个相对上面两个问题还要复杂些。

我们从SQL语句角度思考。该问题需要Blog表关联ViewNum表,再对ViewNum分组求和统计。

分组统计在SQL语句中是统计函数加Group py。在QuerySet中需要使用annotate。annotate的知识可参考我前面写的文章:Django用annotate实现联合统计查询

既然可以通过Blog关联到ViewNum模型。分组统计和排序如下:

from django.db import models
qs = Blog.objects.annotate(view_nums=models.Sum('view_num')).order_by('-view_nums')


总结,ContentTypes可检查特殊的组合外键。在对应模型加上引用字段即可和普通外键一样访问和查询。

上一篇:QuerySet查询优化

下一篇:外键的QuerySet查询

相关专题: Django QuerySet查询   

评论列表

HwiLu

HwiLu

好难啊😂

2019-08-30 15:20 回复

新的评论

清空