model 查询/检索操作


# model 查询/检索操作

# 查询结果集

# QuerySet 是什么

想要从数据库内检索对象,需要基于模型类,通过管理器(Manager)操作数据库并返回一个查询结果集(QuerySet)。

每个 QuerySet 代表一些数据库对象的集合。它可以包含零个、一个或多个过滤器(filters)。Filters 能够缩小查询结果的范围。在 SQL 语法中,一个 QuerySet 相当于一个 SELECT 语句,而 filter 则相当于 WHERE 或者 LIMIT 一类的子句。

每个模型至少具有一个 Manager,默认情况下,Django 自动为我们提供了一个,也是最常用最重要的一个,99%的 情况下我们都只使用它。它被称作 objects,可以通过模型类直接调用它,但不能通过模型类的实例调用它,以此实现「表级别」操作和「记录级别」操作的强制分离。

# QuerySet 惰性特点

QuerySet 都是懒惰的,创建 QuerySet 的动作不会立刻导致任何的数据库行为。你可以不断地进行 filter 动作一整天,Django 不会运行任何实际的数据库查询动作,直到 QuerySet 被提交(evaluated)。

简而言之就是,只有碰到某些特定的操作,Django 才会将所有的操作体现到数据库内,否则它们只是保存在内存和 Django 的层面中。这是一种提高数据库查询效率,减少操作次数的优化设计。

比如下面的例子:

q = Article.objects.filter(title__startswith="What")
q = q.filter(pub_date__lte=datetime.date.today())
q = q.exclude(body__icontains="food")
print(q)
1
2
3
4

上面的例子,看起来执行了 3 次数据库访问,实际上只是在 print 语句时才执行 1 次访问。通常情况,QuerySet 的检索不会立刻执行实际的数据库查询操作,直到出现类似 print 的请求,也就是所谓的 evaluated。

# QuerySet 何时被提交

在内部,创建、过滤、切片和传递一个 QuerySet 不会真实操作数据库,在你对查询集提交之前,不会发生任何实际的数据库操作。

那么如何判断哪种操作会触发真正的数据库操作呢?简单的逻辑思维如下:

  • 第一次需要真正操作数据的值的时候。比如上面 print(q),如果你不去数据库拿 q,print 什么呢?
  • 落实修改动作的时候。你不操作数据库,怎么落实?

了解了 QuerySet 的概念,下面详细介绍 Django model select 的用法,配以对应的 MySQL 查询语句,理解起来更轻松。

# 基本操作

# 获取所有数据
# 对应 SQL:select * from User;
User.objects.all()

# 匹配
# 对应 SQL:select * from User where name = '张三';
User.objects.filter(name='张三')

# 不匹配
# 对应 SQL:select * from User where name != '张三';
User.objects.exclude(name='张三')

# 获取单条数据(有且仅有一条,id 唯一)
# 对应 SQL:select * from User where id = 123;
User.objects.get(id=123)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

需要注意:

  • filter 方法始终返回的是 QuerySet,那怕只有一个对象符合过滤条件,返回的也是包含一个对象的 QuerySet,这是一个集合类型对象,你可以类比 Python列表,可迭代可循环可索引。
  • get 方法只会返回一个对象,但是如果在查询时没有匹配到对象,那么将抛出 DoesNotExist 异常;如果结果超过 1 个,则会抛出 MultipleObjectsReturned 异常。
  • 所以,比起使用 filter 方法然后通过 [0] 的方式分片,要慎用 get 方法。除非你确定你的检索必定只会获得一个对象。

# 常用操作

# 操作语法

# 获取总数
# 对应 SQL:select count(1) from User;
User.objects.count()
User.objects.filter(name='张三').count()

# 比较,gt:>,gte:>=,lt:<,lte:<=
# 对应 SQL:select * from User where id > 724;
User.objects.filter(id__gt=724)
User.objects.filter(id__gt=1, id__lt=10)

# 包含,in
# 对应 SQL:select * from User where id in (11,22,33);
User.objects.filter(id__in=[11, 22, 33])
User.objects.exclude(id__in=[11, 22, 33])

# isnull:isnull=True 为空,isnull=False 不为空
# 对应 SQL:select * from User where pub_date is null;
User.objects.filter(pub_date__isnull=True)

# like,contains 大小写敏感,icontains 大小写不敏感,相同用法的还有 startswith、endswith
User.objects.filter(name__contains="sre")
User.objects.exclude(name__contains="sre")

# 范围,between and
# 对应 SQL:select * from User where id between 3 and 8;
User.objects.filter(id__range=[3, 8])

# 排序,order by,'id' 按 id 正序,'-id' 按 id 倒序
User.objects.filter(name='张三').order_by('id')
User.objects.filter(name='张三').order_by('-id')

# 多级排序,order by,先按 name 进行正序排列,如果 name 一致则再按照 id 倒序排列
User.objects.filter(name='张三').order_by('name','-id')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 字段查询参数

上面好几个语法用到了字段查询,字段查询是指如何指定 SQL WHERE 子句的内容。它们用作 QuerySet 的 filter(),exclude() 和 get() 方法的关键字参数。

其基本格式是:field__lookuptype=value,注意其中是双下划线。

如果不加 __lookuptype,默认查找类型为 exact(精确匹配)。

其中的字段必须是模型中定义的字段之一。但是有一个例外,那就是 ForeignKey 字段,你可以为其添加一个 _id 后缀(单下划线)。这种情况下键值是外键模型的主键原生值。例如:

# 模型中定义的字段
# 对应 SQL:SELECT * FROM blog_entry WHERE pub_date <= '2022-01-01';
Article.objects.filter(pub_date__lte='2022-01-01')

# ForeignKey 字段
Article.objects.filter(category_id=2)
1
2
3
4
5
6

Django 的数据库 API 支持 20 多种查询类型,下表列出了所有的字段查询参数:

  字段名  说明  示例  exact  精确匹配  Article.objects.get(id__exact=3)  iexact  不区分大小写的精确匹配  Article.objects.get(title__iexact="first article")  contains  包含匹配  Article.objects.get(title__contains='first')  icontains  不区分大小写的包含匹配  contains 的大小写不敏感模式  in  在 ... 之内的匹配  Article.objects.filter(id__in=[1, 3, 4])  gt  大于  Article.objects.filter(id__gt=2)  gte  大于等于  同上  lt  小于  同上  lte  小于等于  同上  startswith  从开头匹配  大小写敏感  istartswith  不区分大小写从开头匹配  不区分大小写  endswith  从结尾处匹配  大小写敏感  iendswith  不区分大小写从结尾处匹配  不区分大小写  range  范围匹配  import datetime  start_date = datetime.date(2022, 1, 1)  end_date = datetime.date(2022, 3, 31)  Article.objects.filter(pub_date__range=(start_date, end_date))  date  日期匹配  compare_date = datetime.date(2022, 1, 1)  Article.objects.filter(pub_date__date=compare_date)  Article.objects.filter(pub_date__date__gt=compare_date)  year  年份  Article.objects.filter(pub_date__year=2022)  Article.objects.filter(pub_date__year__gte=2022)  iso_year  以 ISO 8601 标准确定的年份  Article.objects.filter(pub_date__iso_year=2022)  month  月份,取整数 1~12  Article.objects.filter(pub_date__month=12)  day  日期  Article.objects.filter(pub_date__day=3)  week  第几周  Article.objects.filter(pub_date__week=52)  week_day  周几,星期日为 1,星期六为 7  Article.objects.filter(pub_date__week_day=2)  iso_week_day  以 ISO 8601 标准确定的星期几  Article.objects.filter(pub_date__iso_week_day=1)  quarter  季度,取值范围 1~4  Article.objects.filter(pub_date__quarter=2)  time  时间,将字段的值转为  datetime.time 格式并进行对比  Article.objects.filter(pub_date__time=datetime.time(14, 30))  hour  小时,取 0~23 之间的整数  Event.objects.filter(timestamp__hour=23)  minute  分钟,取 0~59 之间的整数  Event.objects.filter(timestamp__minute=29)  second  秒,取 0~59 之间的整数  Event.objects.filter(timestamp__second=31)  regex  区分大小写的正则匹配  Article.objects.get(title__regex=r'^(An?|The) +')  iregex  不区分大小写的正则匹配  Article.objects.get(title__iregex=r'^(an?|the) +')

# 进阶操作

# limit
# 对应 SQL:select * from User limit 3;
User.objects.all()[:3]

# limit,取第三条以后的数据
# 没有对应的 SQL,类似的如:select * from User limit 3,10000000;
# 从第 3 条开始取数据,取 10000000 条(10000000 大于表中数据条数)
User.objects.all()[3:]

# offset,取出结果的第 10-20 条数据(不包含 10,包含 20)
# 也没有对应 SQL,参考上边的 SQL 写法
User.objects.all()[10:20]

# 分组,group by
# 对应 SQL:select username,count(1) from User group by username;
from django.db.models import Count
User.objects.values_list('username').annotate(Count('id'))

# 去重 distinct
# 对应 SQL:select distinct(username) from User;
User.objects.values('username').distinct().count()

# filter 多列、查询多列
# 对应 SQL:select username,fullname from accounts_user;
User.objects.values_list('username', 'fullname')

# filter 单列、查询单列
# 正常 values_list 给出的结果是个列表,里边的每条数据对应一个元组
# 当只查询一列时,可以使用 flat 标签去掉元组,将每条数据的结果以字符串的形式存储在列表中,从而避免解析元组的麻烦
User.objects.values_list('username', flat=True)

# int 字段取最大值、最小值、综合、平均数
from django.db.models import Sum,Count,Max,Min,Avg

User.objects.aggregate(Count('id'))
User.objects.aggregate(Sum('age'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 时间字段

# 匹配日期,date
User.objects.filter(create_time__date=datetime.date(2018, 8, 1))
User.objects.filter(create_time__date__gt=datetime.date(2018, 8, 2))

# 匹配年,year
# 相同用法的还有匹配月 month,匹配日 day,匹配周 week_day,匹配时 hour,匹配分 minute,匹配秒 second
User.objects.filter(create_time__year=2018)
User.objects.filter(create_time__year__gte=2018)

# 按天统计归档
today = datetime.date.today()
select = {'day': connection.ops.date_trunc_sql('day', 'create_time')}
deploy_date_count = Task.objects.filter(
    create_time__range=(today - datetime.timedelta(days=7), today)
).extra(select=select).values('day').annotate(number=Count('id'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Q 对象

Q 对象可以对关键字参数进行封装,从而更好的应用多个查询,可以组合 &(and)|(or)~(not) 操作符。

例如下面的语句:

from django.db.models import Q

User.objects.filter(
    Q(role__startswith='sre_'),
    Q(name='张三') | Q(name='李四')
)
1
2
3
4
5
6

转换成 SQL 语句如下:

select * from User where role like 'sre_%' and (name='张三' or name='李四');
1

通常更多的时候我们用 Q 来做搜索逻辑,比如前台搜索框输入一个字符,后台去数据库中检索标题或内容中是否包含:

_s = request.GET.get('search')

_t = Article.objects.all()
if _s:
    _t = _t.filter(
        Q(title__icontains=_s) |
        Q(body__icontains=_s)
    )

return _t
1
2
3
4
5
6
7
8
9
10

# F 表达式

到目前为止的例子中,我们都是将模型字段与常量进行比较。但是,如果你想将模型的一个字段与同一个模型的另外一个字段进行比较该怎么办?

使用 Django 提供的 F 表达式即可。

例如,为了查找评论数目多于点赞数目的文章,可以构造一个 F() 对象来引用点赞数目,并在查询中使用该 F() 对象:

from django.db.models import F

Article.objects.filter(number_of_comment__gt=F('number_of_like'))
1
2
3

Django 支持对 F() 对象进行加、减、乘、除、求余以及幂运算等算术操作。两个操作数可以是常数和其它 F() 对象。

# 外键:ForeignKey

  • 表结构:
class Role(models.Model):
    name = models.CharField(max_length=16, unique=True)


class User(models.Model):
    username = models.EmailField(max_length=255, unique=True)
    role = models.ForeignKey(Role, on_delete=models.CASCADE)
1
2
3
4
5
6
7
  • 正向查询:
# 查询用户的角色名
_t = User.objects.get(username='张三')
_t.role.name
1
2
3
  • 反向查询:
# 查询角色下包含的所有用户
_t = Role.objects.get(name='Role03')
_t.user_set.all()
1
2
3
  • 另一种反向查询的方法:
_t = Role.objects.get(name='Role03')

# 这种方法比上一种 _set 的方法查询速度要快
User.objects.filter(role=_t)
1
2
3
4
  • 第三种反向查询的方法:

如果外键字段有 related_name 属性,例如 models 如下:

class User(models.Model):
    username = models.EmailField(max_length=255, unique=True)
    role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name='roleUsers')
1
2
3

那么可以直接用 related_name 属性取到某角色的所有用户:

_t = Role.objects.get(name='Role03')
_t.roleUsers.all()
1
2

# M2M:ManyToManyField

  • 表结构:
class Group(models.Model):
    name = models.CharField(max_length=16, unique=True)

class User(models.Model):
    username = models.CharField(max_length=255, unique=True)
    groups = models.ManyToManyField(Group, related_name='groupUsers')
1
2
3
4
5
6
  • 正向查询:
# 查询用户隶属组
_t = User.objects.get(username='张三')
_t.groups.all()
1
2
3
  • 反向查询:
# 查询组包含用户
_t = Group.objects.get(name='groupC')
_t.user_set.all()
1
2
3
  • 同样 M2M 字段如果有 related_name 属性,那么可以直接用下面的方式反查:
_t = Group.objects.get(name='groupC')
_t.groupUsers.all()
1
2

# get_object_or_404

正常如果我们要去数据库里搜索某一条数据时,通常使用下面的方法:

_t = User.objects.get(id=734)
1

但当 id=724 的数据不存在时,程序将会抛出一个错误:

abcer.models.DoesNotExist: User matching query does not exist.
1

为了程序兼容和异常判断,我们可以使用下面两种方式:

  • 方式一:get 改为 filter
_t = User.objects.filter(id=724)
# 取出 _t 之后再去判断 _t 是否存在
1
2
  • 方式二:使用 get_object_or_404
from django.shortcuts import get_object_or_404

_t = get_object_or_404(User, id=724)
# get_object_or_404 方法,它会先调用 Django 的 get 方法,如果查询的对象不存在的话,则抛出一个 Http404 的异常
1
2
3
4

实现方法类似于下面这样:

from django.http import Http404

try:
    _t = User.objects.get(id=724)
except User.DoesNotExist:
    raise Http404
1
2
3
4
5
6

# get_or_create

顾名思义,查找一个对象如果不存在则创建,如下:

object, created = User.objects.get_or_create(username='张三')
1

返回一个由 objectcreated 组成的元组,其中 object 就是一个查询到的或者是被创建的对象,created 是一个表示是否创建了新对象的布尔值。

实现方式类似于下面这样:

try:
    object = User.objects.get(username='张三')

    created = False
exception User.DoesNoExist:
    object = User(username='张三')
    object.save()

    created = True

returen object, created
1
2
3
4
5
6
7
8
9
10
11

# 执行原生 SQL

Django 中能用 ORM 的就用 ORM 吧,不建议执行原生 SQL,可能会有一些安全问题。如果实在是 SQL 太复杂 ORM 实现不了,那就看看下面执行原生 SQL 的方法,跟直接使用 pymysql 基本一致:

from django.db import connection

with connection.cursor() as cursor:
    cursor.execute('select * from accounts_User')
    row = cursor.fetchall()

return row
1
2
3
4
5
6
7

注意这里表名字要用 app名+下划线+model名 的方式(如果你在模型的元数据中指定了 db_table 字段,就另说了)。

# pk

pk 就是 primary key 的缩写。通常情况下,一个模型的主键为 id,所以下面三个语句的效果一样:

Article.objects.get(id__exact=14)
Article.objects.get(id=14)
Article.objects.get(pk=14)
1
2
3

# 转义百分符号和下划线

在原生 SQL 语句中 % 符号有特殊的作用。Django 帮你自动转义了百分符号和下划线,你可以和普通字符一样使用它们,如下所示:

Article.objects.filter(title__contains='%')
# 它和下面的一样
# SELECT ... WHERE title LIKE '%\%%';
1
2
3

# 缓存与查询集

每个 QuerySet 都包含一个缓存,用于减少对数据库的实际操作。理解这个概念,有助于你提高查询效率。

对于新创建的 QuerySet,它的缓存是空的。当 QuerySet 第一次被提交后,数据库执行实际的查询操作,Django 会把查询的结果保存在 QuerySet 的缓存内,随后的对于该 QuerySet 的提交将重用这个缓存的数据。

要想高效的利用查询结果,降低数据库负载,你必须善于利用缓存。看下面的例子,这会造成 2 次实际的数据库操作,加倍数据库的负载,同时由于时间差的问题,可能在两次操作之间数据被删除或修改或添加,导致脏数据的问题:

print([obj.title for obj in Article.objects.all()])
print([obj.pub_date for obj in Article.objects.all()])
1
2

为了避免上面的问题,好的使用方式如下,这只产生一次实际的查询操作,并且保持了数据的一致性:

queryset = Article.objects.all()
print([obj.title for obj in queryset])     # 提交查询
print([obj.pub_date for obj in queryset])  # 重用查询缓存
1
2
3

何时不会被缓存呢?

有一些操作不会缓存 QuerySet,例如切片和索引。这就导致这些操作没有缓存可用,每次都会执行实际的数据库查询操作。例如:

queryset = Article.objects.all()
print(queryset[5])  # 查询数据库
print(queryset[5])  # 再次查询数据库
1
2
3

但是,如果已经遍历过整个 QuerySet,那么就相当于缓存过,后续的操作则会使用缓存,例如:

queryset = Article.objects.all()
[obj for obj in queryset]  # 查询数据库
print(queryset[5])         # 使用缓存
print(queryset[5])         # 使用缓存
1
2
3
4

下面的这些操作都将遍历 QuerySet 并建立缓存:

[obj for obj in queryset]
bool(queryset)
obj in queryset
list(queryset)
1
2
3
4

注意:简单地打印 QuerySet 并不会建立缓存,因为 __repr__() 调用只返回全部查询集的一个切片。

(完)