QuerySet查询基础

  • 发布时间:2017年4月24日 15:35
  • 作者:杨仕航
  • 分类标签: Django
  • 阅读(13662)
  • 评论(0)

一开始使用Django的QuerySet获取数据不太习惯。简单的条件可以通过get或filter获取,但复杂的我总是用SQL语句查询得到。随着接触QuerySet越来越多,发现它相当锋利、健全。

QuerySet是Django的查询集,可以通过QuerySet条件查询得到对应模型的对象集合。

相对直接使用SQL而言,QuerySet可以防止大部分SQL注入,而且提高代码可读性。毕竟Django的模型和具体表名不太一样,需要查阅才得知表名,而且写一大串SQL代码可能直接把人看晕。假如碰到模型变动(改名、增删字段等),SQL可能又要重新调整。所以,能使用QuerySet的情况下,最好使用QuerySet。

还有重要一点,QuerySet是懒惰的。

创建一个QuerySet对象,它不会直接返回数据集。等到使用它的时候,才解析该对象得到数据集。而且解析过一次会被缓存起来,下次使用时直接返回缓存中的数据,缓存的使用提高多次查询的效率。

鉴于习惯使用SQL,或者从条件角度出发。我总结QuerySet查询如下(用俯视的视角学习)。


先假设有如下模型,以供下面的QuerySet查询使用:

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

#博客模型
class Blog(models.Model):
    #标题
    caption = models.CharField(max_length=50)
    
    #作者(外键关联User模型)
    author = models.ForeignKey(User)
    
    #内容
    content = models.TextField()
    
    #分类(和Tag模型关联,多对多)
    tags = models.ManyToManyField(Tag,blank=True)
    
    #发表时间
    publish_time = models.DateTimeField(auto_now_add=True)
    
#博客分类标签
class Tag(models.Model):
    #分类名
    tag_name = models.CharField(max_length=20,blank=True)


1、QuerySet和SQL语句

首先讲如何将SQL语句解析成RawQuerySet对象。先让不会使用QuerySet的同学顺利使用SQL语句查询。

不熟悉SQL的可以跳过该部分,如下代码:

sql = 'select * from blog_blog' #需要查询数据库具体Blog对应表名
qs = Blog.objects.raw(sql) #将sql语句转成RawQuerySet对象

该SQL是获取全部记录,相当于QuerySet如下查询:

qs = Blog.objects.all()


以上的方法可将SQL语句转成Blog模型的查询集。

不过这个RawQuerySet和QuerySet稍微有些不同,属性和方法没有QuerySet多。例如我想知道这个查询有多少条记录。QuerySet可以直接使用count方法或者len方法获取,而RawQuerySet没有。这个也有方法处理,如下代码:

#给rawqueryset对象加上len()方法
def add_len_to_raw_query(query):
    from django.db.models.query import RawQuerySet
    def __len__(self): 
        from django.db import connection
        sql = 'select count(*) from (%s) as newsql' % query.raw_query
        with connection.cursor() as cursor:
            cursor.execute(sql)
            row = cursor.fetchone()
        return row[0]
    setattr(RawQuerySet, '__len__', __len__)
    
sql = 'select * from blog_blog'
qs = Blog.objects.raw(sql)

add_len_to_raw_query(qs)  #给qs加上len()方法
print(len(qs))


我们可通过该方法设置并获取相关对象。另外,使用SQL查询,我们还可以通过SQL语句给RawQuerySet对象添加额外的属性。raw方法会解析SQL语句中的字段,将字段转成属性的方式方便调用。例如:

sql = '''
    Select blog_blog.id, blog_blog.caption, count(blog_blog_tags.id) as tag_count
    From blog_blog
    Left join blog_blog_tags on blog_blog.id = blog_blog_tags.blog_id
    Group by blog_blog.id, blog_blog.caption
'''

qs = Blog.objects.raw(sql)
print(qs[0].tag_count)


上面SQL语句是得到每篇Blog对应分类标签的个数,是不是觉得SQL很复杂了。

不过,我们可以在查询结果直接使用tag_count属性。


另外,为了方便测试,我们可以输出QuerySet对象的SQL语句:

qs = Blog.objects.filter(id=1)
print(qs.query)


2、filter和get方法

上面使用了filter。filter是筛选的意思,通过filter筛选得到符合条件的数据集。

例如我分别获取如下情况的数据集。

#获取blog id为1的数据集
qs1 = Blog.objects.filter(id=1)

#获取作者user所发表的博客
user = request.user #先使用当前登录的用户判断
qs2 = Blog.objects.filter(author=user)

#多个条件用逗号隔开
qs3 = Blog.objects.filter(id=1, author=user)


Blog id为1相当于SQL:

select * from blog_blog where id = 1

这里你会发现只有一条记录,而filter返回是一个只有一条记录的数据集。获取该数据还需要进一步获取:

q = qs1[0]

这时可以使用get方法直接获取该条记录:

q = Blog.objects.get(id=1)


不过,若符合条件的没有记录或多条记录会抛出异常。我们可以利用这个特点做一些操作:

try:
    q = Blog.objects.get(caption='test') #找标题为test的博客
except Blog.DoesNotExist:
    q = Blog(caption='test') #创建标题为test的博客
    #...
    q.save()

当然,django有个get_or_create方法可以简化该过程:

#获取标题为test的博客,不存在则创建
q = Blog.objects.get_or_create(caption='test')


3、不等于条件

filter筛选的条件都是符合的条件,也就是我们SQL语句中的等于。有时候,我们希望排除一些条件,也就是不等于或者not条件:

select * from blog_blog where id <> 1
select * from blog_blog where not(id = 1)


这种类型的条件,需要使用exclude方法。例如:

qs = Blog.objects.exclude(id=1)


如果我要实现复杂一些的查询,获取当前登录用户发表的博客,并排除id为1的博客。查询如下:

user = request.user
qs = Blog.objects.filter(author=user).exclude(id=1)

这里延伸QuerySet另一特性,可以在QuerySet上继续查询,即QuerySet支持链式查询


4、大于和小于条件

继续延伸,既然QuerySet可以实现等于和不等于。那么大于和小于应当如何实现?

这种filter和exclude都无能为力。需要借助字段条件修饰,例如:

#查询id大于10的记录
qs1 = Blog.objects.filter(id__gt=10)

#查询id大于等于10的记录
qs2 = Blog.objects.filter(id__gte=10)

#排除id小于10的记录
qs3 = Blog.objects.exclude(id__lt=10)

#排除id小于等于10的记录
qs4 = Blog.objects.exclude(id__lte=10)


从这里我们可以总结一下,objects查询基本就3种方法:get、filter、exclude。

条件再用字段修饰拓展,字段修饰是两个下划线加修饰码。

有个方法可以帮助记忆大于、小于的修饰码。

gt 是 greater than 的缩写,或者你熟悉html可以记忆为"&gt;","&gt;"是大于号的转义。

gte 后面的 e 是等于equal单词的第1个字母。

lt 和 lte 是同样方法,less than 的缩写;"&lt;"小于号的转义。


这里还有两个拓展。

1)范围查询

有时候,不单单是大于或小于。可能是一个范围,例如 1<=id<=9。你可以用两个条件组合。

qs = Blog.objects.filter(id__gte=1).filter(id__lte=9)

或者

qs = Blog.objects.filter(id__gte=1, id__lte=9)

可以使用__range范围修饰:

qs = Blog.objects.filter(id__range=[1, 9])


2)in条件查询

有时候不是简单大于或小于,需要从一个列表的值获取。in条件查询用__in修饰,例如:

#获取id为 1、3、6、7、9的记录
qs = Blog.objects.filter(id__in=[1, 3, 6, 7, 9])


5、字符串模糊匹配

数值有大于小于等判断,那么字符串有模糊匹配判断的需求。例如开头是什么,结尾是什么,包含什么字符等等。这些同样需要借助字段条件修饰。例如:

#标题包含django,若忽略大小写使用__icontains
qs1 = Blog.objects.filter(caption__contains='django')

#标题是django开头的,若忽略大小写使用__istartswith
qs2 = Blog.objects.filter(caption__startswith='django')

#标题是django结尾的,若忽略大小写使用__iendswith
qs2 = Blog.objects.filter(caption__endswith='django')


那"不包含"、"开头不是"、"结尾不是"用什么?当然用exclude啦。


6、日期时间处理

上面Blog模型有个发表日期publish_time字段。该字段是datetime类型。

日期时间类型要不数值、字符串复杂得多。同样使用字段条件修饰实现相应查询:

import datetime

#获取今天的datetime
now = datetime.datetime.now()

#找今年发布的博客
qs1 = Blog.objects.filter(publish_time__year=now.year)

#找本月发布的博客
qs2 = Blog.objects.filter(publish_time__year=now.year, publish_time__month=now.month)

#找到每月1号发布的博客
qs3 = Blog.objects.filter(publish_time__day=1)


我们还可以使用使用__range获取某范围内的数据:

import datetime

#获取前7天发表的博客
now = datetime.datetime.now()
end_date = datetime.datetime(now.year, now.month, now.day, 0, 0) 
start_date = end_date - datetime.timedelta(7)

#找今年发布的博客
qs = Blog.objects.filter(publish_time__range=[start_date, end_date])


7、or条件

上面多个条件出现大多是and逻辑。那我可能使用or逻辑。例如我想模糊匹配标题为"django" 或 "python"。这时只能采用or逻辑处理。如下代码:

qs = Blog.objects.filter(caption__icontains='django')|Blog.objects.filter(caption__icontains='python')

若你嫌弃代码过长,可以使用Q函数:

from django.models import Q
qs = Blog.objects.filter(Q(caption__icontains='django')|Q(caption__icontains='python'))


8、limit获取前几条

有时页面显示只需要显示前10条或前20条记录。这个在SQL语句中可以用limit限制返回的记录数。同样,在QuerySet查询中也可以实现该功能。

qs = Blog.objects.all()[:10]

上面代码是获取全部博客的前10条记录,和切片一样,但索引不能为负数。

另外,这个切片器也不会让QuerySet直接执行,直到使用该查询。


9、外键查找

我上面写的示例模型,有两个外键。

假如我需要获取某一篇博客有什么分类标签,或分类标签下有哪些博客,应当如何获取?

获取某一篇博客有什么分类标签,由于Blog有个tags外键关联Tag模型。可直接获取:

blog = Blog.objects.all()[0]
tags = blog.tags.all() #获取第1篇博客全部标签


而分类标签没有直接写一个外键和Blog模型关联,但可以用set通过tag获取全部blog:

tag = Tag.objects.all()[0]
blogs = tag.blog_set.all() #获取第1个分类标签全部博客


10、排序

SQL语句可以使用Order by排序。QuerySet查询也有order_by与之对应:

#按照日期正序排序
qs1 = Blog.objects.all().order_by('publish_time')

#安装日期倒序排序(all可以省略)
qs2 = Blog.objects.order_by('-publish_time')


针对哪个排序,就写该字段名称即可。默认正序排序,若加个负号表示倒序排序。

正序一般是数值由小到大,日期由远到近。

排序还有一个神奇的用法,在SQL我还没见过:随机排序。通过该功能,我们可以随机获取记录:

#随机获取前10条记录
qs = Blog.objects.order_by('?')[:10]


11、其他

有些知识过于零碎,没有和上面放在一起讲。随便也罗列出来:

1)获取值为null的数据

qs = Blog.objects.filter(content__isnull=True)


2)去重

qs = Tag.objects.filter(id__in=[1, 3]).blog_set().distinct()

因为这里通过Tag获取的博客可能重复,可以用distinct去重。


3)annotate

参考我前面的文章:Django用annotate实现联合统计查询


4)aggregate

参考我前面的文章:Django的aggregate究竟是何方妖孽


还有一个ContentType类型的查询,我还在研究中。研究完成在总结发文。

上一篇:外键的QuerySet查询

下一篇:我的网站搭建(第52天) 使用Redis缓存提速

相关专题: Django QuerySet查询   

评论列表

智慧如你,不想发表一下意见吗?

新的评论

清空