Model层QuerySet的使用
1、QuerySet的概念
Django算是标准的MVC框架,虽然因为他的模板以及view的概念有时候被大家戏称“MTV”的开发模式,但是道理都是一样的。Model作为MVC模式中的基础层(也可以称为数据层),负责为整个系统提供数据。因此我们先了解下它是如何提供数据的:
在Model层中,Django通过给Model增加一个objects属性来提供数据操作的接口。
比如:我们以一个博客网站文章查询为例子,如果我们想要查询所有文章的数据,那我们可以这么写:
Post.objects.all()
这样我们就能拿到一个QuerySet的对象。这个对象包含了我们需要的数据,但是注意只有当我们用到它时才会去数据库查询
这时候可能会有点奇怪?为啥不是我在执行**Post.objects.all()**的时候就直接查询呢?
其原因是QuerySet对象要支持链式操作。如果每次执行都要查询数据库的话,会存在性能问题,因为有时候可能你就没用到你执行的代码。
举个例子:
posts = Post.objects.all()
result = posts.filter(status=1)
那么如果这条语句要立即执行的话就会产生两次的SQL查询并且两次查询存在重复数据。当然相信一般情况下大家也不会这么写。
因此,Django的QuerrySet其实在本质上就是一个懒加载对象,上面两句代码执行后不会产生数据库查询操作,只会返回一个QuerySet对象,等你真正用到他的时候它才会去执行查询。
posts = Post.objects.all() # 返回一个QuerySet对象并赋值给posts;
result = posts.filter(status=1) # 继续返回一个QuerySet对象并且赋值给result;
print(result) # 此时会根据上面的两个条件执行数据查询操作,对应的SQL查询语句为:SELECT * FROM blog_post where status=1;
在我们理解了QuerySet对象的懒加载后,可以帮助我们在日常开发中提升我们的系统性能;
另外,上面我们也说到了链式调用,这个又是什么概念?
posts = Post.objects.filter(status=1).filter(category=2).filter(title_icontains='test')
相信看了以上的代码你就豁然开朗了,这就是链式调用,在每个函数(或者方法)的执行结果上可以继续调用同样的方法,因为每个函数的返回值都是他自己,也就是QuerySet。
2、常用的QuerySet接口
2.1、支持链式调用的接口
- all接口,相当于SELECT * FROM table_name 语句用于查询所有数据
- filter接口,顾名思义,根据条件顾虑,常用的条件基本上是字段等于、不等于、大于、小于。当然,还有其他的,比如能改成产生LIKE查询的:Model.objects.filter(content_contains=“条件”)
- exclude接口,同filter,只是相反的逻辑。
- reverse接口,把QuerySet中的结果倒序排列。
- distinct接口,用来进行去重查询,产生 SELECT DISTINCT 这样的SQL查询
- none接口,返回空的QuerySet。
2.2、不支持链式调用的接口
不支持链式调用,即返回值不是QuerySet的接口,具体如下:
- get接口,比如:Post.objects.get(id=1)这个语句就是来查询id为1的文章;如果文章存在则直接返回对应的post实例,如果不存在则抛出DoesNotExist异常。所一般情况下我们会这莫写:
try:
post = Post.objetcs.get(id=1)
except Post.DoesNotExist:
# 做异常情况处理
- create接口,用来直接创建一个model对象,eg:post = Post.objects.create(title=“django”)
- get_or_create接口,根据条件查找,如果没查找到就调用create创建。
- update_or_create接口,同get_or_create,只是用来做更新操作。
- count接口,用于返回QuerySet有多少条记录,相当于 SELECT COUNT(*) FROM table_name
- lastest接口,用于返回最新的一条记录,但是需要在Model中的Meta
- 中定义:get_latest_by = <用来排序的字段>
- earliest接口,同上,返回最早的一条记录
- first接口,从当前的QuerySet记录中获取第一条,
- last接口,同上,获取最后一条。
- exists接口,返回True或者False,在数据库层面执行 SELECT (1) AS “a” FROM table_name LIMIT 1的查询,如果只是需要判断QuerySet是否有数据,用这个接口最合适不过,不要用count或者len(QuerySet)的方式来做判断,这样可以减少一次DB查询请求。
- bulk_create 接口,同create,用来批量创建记录
- in_bluk 接口,批量查询,接受两个参数 id_list 和 filed_name。 可以通过Post.objects.in_bluk([1,2,3])查询出id为1,2,3的数据,返回结果是字典类型,字典类型的key为查询条件。返回结果示例:{1: <Post 实例 1>, 2:<Post案例2>, 3:<Post案例3>}.
- update接口,用来根据条件批量更新记录,比如:Post.objects.filter(owner_name=‘django’).update(title=‘测试更新’)。
- delete接口,同update,这个接口是用来根据条件批量删除记录。需要注意的是,update和delete都会出发django的signal
- values接口,当我们明确知道只需要返回某个字段的值,不需要Model实例时,可以使用它,用法如下:
title_list = Post.objects.filter(category_id=1).values('title')
# 返回结果包含dict的,QuerySet, 类似这样:<QuerySet[{'title':xxxx},]>
- values_list接口,同values,但是直接返回的是包含tuple的QuerySet:
title_list = Post.objects.filter(category=1).values_list('title')
#返回结果类似:<QuerySet[('标题',)]>
如果只是一个字段的话,可以通过增加 flat=True 参数,便于我们后面的处理:
title_list = Post.objects.filter(category=1).values_list('title',flat=True)
for title in title_list:
print(title)
3、进阶接口
在优化django项目时,优先考虑这几种接口的用法。
- defer 接口,把不需要展示的字段做延时加载,比如说:需要获取文章中除了正文外的其他字段,就可以通过posts = Post.object.all().defer(‘content’),这样拿到的记录中就不会包含content部分,但是当我们需要用到这个字段时,在使用时会去加载。下面还是通过代码演示:
posts = Post.objects.all().defer('content')
for post in posts: # 此时会执行数据库查询
print(post.content) # 此时会执行数据查询,获取到content
当不想某个过大的字段时(如text类型的字段),会使用defer,但是上面的演示代码会产生N+1查询问题,在实际使用一定要注意!
注意:上面的代码是一个不太典型的N+1查询问题,一般情况下,由外键查询产生的N+1查询问题比较多,即一条查询请求返回N条数据,当我们操作数据时,又会产生额外的请求。这就是N+1问题,所有的orm框架都存在这样的问题。
- only接口。同defer接口刚好相反,如果只是想获取到所有的title记录,就可以使用only,只获取title的内容,其他值在获取时会产生额外的查询。
- select_related接口,这就是用来解决外键产生的N+1解决方案,我们先来看看什么情况下会产生这个问题:
posts = Post.objects.all()
for post in posts: # 产生数据库查询
print(post.owner) # 产生额外的数据库查询
代码同上类似,只是这里用的是owner
他的解决办法就是用select_related接口:
posts = Post.objects.all().select_related('category')
for post in posts: # 产生数据库查询,category一块跟着查询
print(post.category) # 产生额外的数据库查询
- prefetch_related接口,针对多对多关系,比如,post和tag关系可以通过这种方式来避免:
posts = Post.objects.all().prefetch_related('tag')
for post in posts:
print(post.tag.all())
4、常用的字段查询
这里我们把django常用的关键字查询列一下,更多的还是要查询django文档。
- contains:包含,用来进行相似查询
- icontains:同contains,只是忽略大小写
- exact:精确匹配
- iexact:同exact,忽略大小写
- in:指定某个集合,比如:Post.objetcs.filter(id_in=[1,2,3]),相当于SELECT * FROM blog_post WHERE IN (1,2,3);。
- gt:大于某个值
- gte:大于等于某个值
- lt:小于某个值
- lte:小于等于某个值
- startswith:以某个字符串开头,与contains类似,只是会产生LIKE '<关键字>%'这样的SQL
- istartswith:同startswith,忽略大小写
- endswith:以某个字符串结尾,
- iendswith:同上,忽略大小写
- range:范围查询,多用于时间范围,如:
Post.objects.filter(created_time_range=('2021-05-11','2022-09-21'))
会产生这样的查询:
SELECT ... WHERE created_time BETWEEN '2021-05-11' AND '2022-09-21';
关于日期查询的还有很多,比如:date、year、month等,具体等需要时查看文档即可。
5、进阶查询
- F,F表达式常用来执行数据库层面的计算,从而避免出现竞争状态。就比如需要处理每篇文章的访问量,假设存在post.pv这样的字段,当有用户访问时,我们对其加1:
post = Post.objects.get(id=1)
post.pv = post.pv + 1
post.save()
这在多线程情况下会出现问题,其执行逻辑是先获取到当前的pv值,然后将其加1后赋值给post.pv,最后保存。如果多个线程同时执行了post = Post.objects.get(id=1),那么每个线程里的post.pv值都是一样的,执行完加1和保存后,相当于只执行了一个加1,而不是多个。
其原因在于我们把数据拿到python中转了一圈,然后保存到数据库中。这时通过F表达式就可以方便的解决这个问题:
from django.db.models import F
post = Post.objetcs.get(id=1)
post.pv = F('pv') + 1
post.save()
这种方式最终会产生类似这样的SQL语句:UPDATE blog_post SET pv + 1 WHERE ID = 1。他在数据库层面执行原子性操作。
- Q,Q表达式就是用来解决OR查询的,可以这末用:
from django.db.models import Q
post.objects.filter(Q(id=1) | Q(id=2))
或者进行AND查询:
post.objects.filter(Q(id=1) & Q(id=2))
- Count,用来做聚合查询,比如想得到某个分类下有多少篇文章?简单的做法就是:
category = Category.objects.get(id=1)
posts_count = category.post_set.count()
- Sum, 同Count类似,只是它是用来做合计的。比如想要统计目前所有文章加起来的访问量有多少,可以这么做:
from django.db.models import Sum
Post.objects.aggregate(all_pv=Sum('pv'))
# 输出类似结果:{'all_pv':487}
上面演示了QuerySet的aggregate的用法,用来给QuerySet直接计算结果。
除了Count和Sum外,还有Avg,Min和Max等表达式,均用来满足我们对SQL查询的需求
6、总结
通过上面一系列的介绍,你应该对QuerrySet有了基本的了解。其实简单来说,就是django的ORM为了达到和SQL一样的表达能力,给我们提供了各种各样的接口!
因此,QuerrySet的作用其实就是帮助我们更好的和数据库打交道!
写了半天,真的累死,完全手敲,西巴!
各位看官看在折磨累的份上给个关注或者点赞吧!谢谢!
我的个人博客网站原链接:点击这里
里面还有其他好文,欢迎来访互相讨教。