QuerySet查询基础

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

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

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

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

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

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

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


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

  1. #coding:utf-8
  2. from django.db import models
  3. from django.contrib.auth.models import User
  4.  
  5. #博客模型
  6. class Blog(models.Model):
  7.     #标题
  8.     caption = models.CharField(max_length=50)
  9.     
  10.     #作者(外键关联User模型)
  11.     author = models.ForeignKey(User)
  12.     
  13.     #内容
  14.     content = models.TextField()
  15.     
  16.     #分类(和Tag模型关联,多对多)
  17.     tags = models.ManyToManyField(Tag,blank=True)
  18.     
  19.     #发表时间
  20.     publish_time = models.DateTimeField(auto_now_add=True)
  21.     
  22. #博客分类标签
  23. class Tag(models.Model):
  24.     #分类名
  25.     tag_name = models.CharField(max_length=20,blank=True)


1、QuerySet和SQL语句

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

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

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

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

  1. qs = Blog.objects.all()


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

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

  1. #给rawqueryset对象加上len()方法
  2. def add_len_to_raw_query(query):
  3.     from django.db.models.query import RawQuerySet
  4.     def __len__(self): 
  5.         from django.db import connection
  6.         sql = 'select count(*) from (%s) as newsql' % query.raw_query
  7.         with connection.cursor() as cursor:
  8.             cursor.execute(sql)
  9.             row = cursor.fetchone()
  10.         return row[0]
  11.     setattr(RawQuerySet, '__len__', __len__)
  12.     
  13. sql = 'select * from blog_blog'
  14. qs = Blog.objects.raw(sql)
  15.  
  16. add_len_to_raw_query(qs)  #给qs加上len()方法
  17. print(len(qs))


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

  1. sql = '''
  2.     Select blog_blog.id, blog_blog.caption, count(blog_blog_tags.id) as tag_count
  3.     From blog_blog
  4.     Left join blog_blog_tags on blog_blog.id = blog_blog_tags.blog_id
  5.     Group by blog_blog.id, blog_blog.caption
  6. '''
  7.  
  8. qs = Blog.objects.raw(sql)
  9. print(qs[0].tag_count)


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

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


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

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


2、filter和get方法

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

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

  1. #获取blog id为1的数据集
  2. qs1 = Blog.objects.filter(id=1)
  3.  
  4. #获取作者user所发表的博客
  5. user = request.user #先使用当前登录的用户判断
  6. qs2 = Blog.objects.filter(author=user)
  7.  
  8. #多个条件用逗号隔开
  9. qs3 = Blog.objects.filter(id=1, author=user)


Blog id为1相当于SQL:

  1. select * from blog_blog where id = 1

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

  1. = qs1[0]

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

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


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

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

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

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


3、不等于条件

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

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


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

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


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

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

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


4、大于和小于条件

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

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

  1. #查询id大于10的记录
  2. qs1 = Blog.objects.filter(id__gt=10)
  3.  
  4. #查询id大于等于10的记录
  5. qs2 = Blog.objects.filter(id__gte=10)
  6.  
  7. #排除id小于10的记录
  8. qs3 = Blog.objects.exclude(id__lt=10)
  9.  
  10. #排除id小于等于10的记录
  11. 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。你可以用两个条件组合。

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

或者

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

可以使用__range范围修饰:

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


2)in条件查询

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

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


5、字符串模糊匹配

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

  1. #标题包含django,若忽略大小写使用__icontains
  2. qs1 = Blog.objects.filter(caption__contains='django')
  3.  
  4. #标题是django开头的,若忽略大小写使用__istartswith
  5. qs2 = Blog.objects.filter(caption__startswith='django')
  6.  
  7. #标题是django结尾的,若忽略大小写使用__iendswith
  8. qs2 = Blog.objects.filter(caption__endswith='django')


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


6、日期时间处理

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

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

  1. import datetime
  2.  
  3. #获取今天的datetime
  4. now = datetime.datetime.now()
  5.  
  6. #找今年发布的博客
  7. qs1 = Blog.objects.filter(publish_time__year=now.year)
  8.  
  9. #找本月发布的博客
  10. qs2 = Blog.objects.filter(publish_time__year=now.year, publish_time__month=now.month)
  11.  
  12. #找到每月1号发布的博客
  13. qs3 = Blog.objects.filter(publish_time__day=1)


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

  1. import datetime
  2.  
  3. #获取前7天发表的博客
  4. now = datetime.datetime.now()
  5. end_date = datetime.datetime(now.year, now.month, now.day, 0, 0) 
  6. start_date = end_date - datetime.timedelta(7)
  7.  
  8. #找今年发布的博客
  9. qs = Blog.objects.filter(publish_time__range=[start_date, end_date])


7、or条件

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

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

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

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


8、limit获取前几条

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

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

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

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


9、外键查找

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

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

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

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


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

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


10、排序

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

  1. #按照日期正序排序
  2. qs1 = Blog.objects.all().order_by('publish_time')
  3.  
  4. #安装日期倒序排序(all可以省略)
  5. qs2 = Blog.objects.order_by('-publish_time')


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

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

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

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


11、其他

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

1)获取值为null的数据

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


2)去重

  1. 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查询   

评论列表

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

新的评论

清空

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

我把服务器搬到国内了,并做了一些优化,速度爆快

2021-09-22

了解更多