关于本站
1、基于Django+Bootstrap开发
2、主要发表本人的技术原创博客
3、本站于 2015-12-01 开始建站
一开始使用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)
首先讲如何将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)
上面使用了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')
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支持链式查询。
继续延伸,既然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可以记忆为">",">"是大于号的转义。
gte 后面的 e 是等于equal单词的第1个字母。
lt 和 lte 是同样方法,less than 的缩写;"<"小于号的转义。
这里还有两个拓展。
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])
数值有大于小于等判断,那么字符串有模糊匹配判断的需求。例如开头是什么,结尾是什么,包含什么字符等等。这些同样需要借助字段条件修饰。例如:
- #标题包含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啦。
上面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])
上面多个条件出现大多是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'))
有时页面显示只需要显示前10条或前20条记录。这个在SQL语句中可以用limit限制返回的记录数。同样,在QuerySet查询中也可以实现该功能。
- qs = Blog.objects.all()[:10]
上面代码是获取全部博客的前10条记录,和切片一样,但索引不能为负数。
另外,这个切片器也不会让QuerySet直接执行,直到使用该查询。
我上面写的示例模型,有两个外键。
假如我需要获取某一篇博客有什么分类标签,或分类标签下有哪些博客,应当如何获取?
获取某一篇博客有什么分类标签,由于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个分类标签全部博客
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]
有些知识过于零碎,没有和上面放在一起讲。随便也罗列出来:
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类型的查询,我还在研究中。研究完成在总结发文。
相关专题: Django QuerySet查询