Model层QuerySet的使用

本文详细介绍了Django中QuerySet的使用方法,包括常见的查询接口、链式调用及懒加载特性,同时还深入探讨了进阶接口如defer、only、select_related等的应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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的作用其实就是帮助我们更好的和数据库打交道!

写了半天,真的累死,完全手敲,西巴!

各位看官看在折磨累的份上给个关注或者点赞吧!谢谢!
我的个人博客网站原链接:点击这里
里面还有其他好文,欢迎来访互相讨教。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值