Django用annotate实现联合统计查询

  • 发布时间:2017年2月10日 14:12
  • 作者:杨仕航
  • 分类标签: Django
  • 阅读(13236)
  • 评论(1)

之前刚开始学习和使用Django时,遇到需要多个表联合一起查询的情况。

碰到这种情况,每次都使用原始SQL语句的方法去实现。使用模型的raw方法执行SQL语句。

该方法唯一的好处是灵活!只要会写SQL语句就可以轻松实现你想要的功能。

缺点也很明显,需要打开数据库,查询具体的表名、字段名等等。而且若模型修改了,又需要重新写一个复杂的SQL语句。


Django的ORM已经帮我们实现这个功能。多看Django文档,还有很多东西等待被挖掘。

如下两个简单模型:

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

#标签模型
class Tag(models.Model):
    tag_name = models.CharField(max_length=20)
    
#博客模型
class Blog(models.Model):
    caption = models.CharField(max_length=50)
    tags = models.ManyToManyField(Tag, blank=True)

其中,博客模型有个多对多字段tags和Tag模型关联。

一个博客可能有多个标签,一个标签可能对应多个博客。


那么,问题来了。我如何在获取Tag对象列表的时候,同时获取博客的个数呢?

Tag标签模型中没有和相关字段和Blog模型对象。

这种情况很常见,例如我需要显示一个标签列表给访客点击。顺便把该标签中有多少篇文章也显示出来。下面列举有3种方法,你就可以看出优劣。


1、tag模型反向统计
#coding:utf-8
from models import Blog, Tag

def get_tag_blogs_count():
    tags = []
    for tag in Tag.objects.all():
        #tag反向获取blogs,再用count方法获取个数
        blogs_count = tag.blog_set.count()
        
        #把数量赋给tag临时添加的属性
        tag.blogs_count = blogs_count
        
        #添加到tags集合中
        tags.append(tag)
    return tags

利用该方法可以获取一个带有blog数量的tags列表。将数据返回给前端页面。

可能你会说,你可以在模版文件直接用{{tag.blog_set.count}}得到数据,无须这么费劲。

那么,再加个需求:我还需要按照博客的个数对Tag标签排序。

这种情况,就无法直接在模版文件中处理。需要在后台代码中排序处理。

#coding:utf-8
from .models import Blog, Tag

def get_tag_blogs_count():
    tags = []
    for tag in Tag.objects.all():
        #tag反向获取blogs,再用count方法获取个数
        blogs_count = tag.blog_set.count()
        
        #把数量赋给tag临时添加的属性
        tag.blogs_count = blogs_count
        
        #添加到tags集合中
        tags.append(tag)
        
    #倒序排序处理
    tags = sorted(tags, key=lambda x: x.blogs_count, reverse=True)
    return tags

这段代码,怎么看怎么都觉得执行效率低。


2、使用原始SQL语句查询

使用原始SQL语句查询,需要打开数据库查看表名和字段名。

20170210/20170210121300419.png

和这两个模型相关的表有3个表。因为多对多字段生成了一个blog_blog_tags表。

测试SQL语句没问题之后,在代码中使用。这里排序需求也实现了。

#coding:utf-8
from .models import Tag

def get_tag_blogs_count():
    sql = """
        select blog_tag.*, count(blog_blog_tags.id) as blogs_count
        from blog_tag
            left join blog_blog_tags
            on blog_tag.id = blog_blog_tags.tag_id
        group by blog_tag.id
        order by blogs_count desc
    """
    
    #使用raw执行sql语句
    tags = Tag.objects.raw(sql)
    return tags

该tags中的每个对象都有blogs_count属性。


3、使用annotate(推荐)

annotate这个单词本身是注释的意思。可理解为我们可以给该模型加个字段作为注释,如下代码。

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

def get_tag_blogs_count():
    tags = Tag.objects.annotate(blogs_count=models.Count('blog')).order_by('-blogs_count')
    return tags

此处的annotate只用了一个参数。用Count方法关联到Blog模型,并计数统计。

同样,tags中的每个对象都多出了一个blogs_count属性。

我们可以如下代码输出SQL语句:

print(str(tags.query))

得到如下SQL语句:

SELECT "blog_tag"."id", "blog_tag"."tag_name", COUNT("blog_blog_tags"."blog_id") AS "blogs_count" 
FROM "blog_tag" 
    LEFT OUTER JOIN "blog_blog_tags" 
    ON ("blog_tag"."id" = "blog_blog_tags"."tag_id") 
GROUP BY "blog_tag"."id", "blog_tag"."tag_name"
ORDER BY "blogs_count" DESC

发现该SQL和上面第2种方法的SQL差不多,相当的酷!

使用annotate需要用django.db.models的统计方法。除了Count统计方法之外,还有Max、Min、Sum、Avg等方法。这么看来annotate相当于SQL语句的Group by分组统计。

统计方法的参数是一个模型名称小写。该模型一定要和当前模型有关联的字段(一对多外键或多对多外键等)。

可通过 Tag._meta.get_fields() 方法查得有哪些字段。相关知识可以参考外键的QuerySet查询

那么,annotate也可以对当前模型进行分组统计


ps,我自己网站的标签之前是默认排序:

20170210/20170210141015017.png

按照每个标签对应博客数量降序排序如下:

20170210/20170210141136713.png

看起来舒服一些,有些发文量少的分类会慢慢地排到后面。

上一篇:我的网站搭建(第43天) 删除未使用的图片

下一篇:Python日志管理(logging模块)

相关专题: Django QuerySet查询   

评论列表

杨仕航

杨仕航

这里讲的应用是比较基本的。还可以结合Blog.objects.values方法进行表内的分组统计

2017-02-17 14:51 回复

新的评论

清空