目录
0x00 – 检索对象
要从数据库检索对象,要通过模型类的 Manager 构建一个 QuerySet。
一个 QuerySet 代表来自数据库中对象的一个集合。它可以有 0 个,1 个或者多个 filters. Filters
,可以根据给定参数缩小查询结果量。
在 SQL 的层面上:
QuerySet 对应 SELECT 语句;
filters 对应类似 WHERE 或 LIMIT 的限制子句。
你能通过模型的 Manager 获取 QuerySet。
每个模型至少有一个 Manager,默认名称是 objects。
像这样直接通过模型类使用它:
>>> Blog.objects
<django.db.models.manager.Manager object at ...>
>>> b = Blog(name='Foo', tagline='Bar')
>>> b.objects
Traceback:
...
AttributeError: "Manager isn't accessible via Blog instances."
Managers 只能通过模型类访问,而不是通过模型实例,目的是强制分离 “表级” 操作和 “行级” 操作。
Manager 是模型的 QuerySets 主要来源。例如 Blog.objects.all() 返回了一个 QuerySet,后者包含了数据库中所有的 Blog 对象。
0x01 – 检索全部对象
从数据库中检索对象最简单的方式就是检索全部。
在 Manager 上调用 all() 方法:
>>> all_entries = Entry.objects.all()
方法 all() 返回了一个包含数据库中所有对象的 QuerySet 对象。
返回当前 QuerySet (或 QuerySet 子类)的 副本。 这在以下情况下很有用:你可能想传入一个模型管理器或一个 QuerySet,并对结果做进一步过滤。在任何一个对象上调用 all() 后,你肯定会有一个 QuerySet 可以使用。
当一个 QuerySet 被 执行 时,它通常会缓存其结果。如果数据库中的数据可能在 QuerySet 被评估后发生了变化,你可以通过调用 all() 对以前执行过的 QuerySet 进行更新。
0x02 – 通过过滤器检索指数对象
all()
返回的 QuerySet 包含了数据表中所有的对象。(大多数情况下,你只需要完整对象集合的一个子集。)
要创建一个需要的子集,你需要通过添加过滤条件精炼原始 QuerySet。
两种最常见的精炼 QuerySet 的方式是:
filter(**kwargs)
返回一个新的 QuerySet,包含的对象满足
给定查询参数。exclude(**kwargs)
返回一个新的 QuerySet,包含的对象不满足
给定查询参数。
查询参数(**kwargs)应该符合下面的 Field lookups 的要求。
例如:
# 要包含获取 2006 年的博客条目(entries blog)的 QuerySet,像这样使用 filter():
Entry.objects.filter(pub_date__year=2006)
# 通过默认管理器类也一样:
Entry.objects.all().filter(pub_date__year=2006)
特点:
- 精炼 QuerySet 的结果本身还是一个 QuerySet,所以能串联精炼过程。
- 每次精炼一个 QuerySet,都会获得一个新的 QuerySet,前后两者间毫无关系。
- QuerySet 是惰性的——创建 QuerySet 不会引发任何数据库活动。
0x02 – 1 – 链式过滤器
精炼 QuerySet 的结果本身还是一个 QuerySet,所以能串联精炼过程。
例子:
>>> Entry.objects.filter(
... headline__startswith='What'
... ).exclude(
... pub_date__gte=datetime.date.today()
... ).filter(
... pub_date__gte=datetime.date(2005, 1, 30)
... )
这个先获取包含数据库所有条目(entry)的 QuerySet,然后排除一些,再进入另一个过滤器。最终的 QuerySet 包含标题以 “What” 开头的,发布日期介于 2005 年 1 月 30 日与今天之间的所有条目。
0x02 – 2 – 每个 QuerySet 都是唯一的
每次精炼一个 QuerySet,你就会获得一个全新的 QuerySet,后者与前者毫无关联。
每次精炼都会创建一个单独的、不同的 QuerySet,能被存储,使用和复用。
举例:
>>> q1 = Entry.objects.filter(headline__startswith="What")
>>> q2 = q1.exclude(pub_date__gte=datetime.date.today())
>>> q3 = q1.filter(pub_date__gte=datetime.date.today())
这三个 QuerySets 是独立的。
第一个是基础 QuerySet,包含了所有标题以 “What” 开头的条目。
第二个是第一个的子集,带有额外条件,排除了 pub_date 是今天和今天之后的所有记录。
第三个是第一个的子集,带有额外条件,只筛选 pub_date 是今天或未来的所有记录。
最初的 QuerySet (q1) 不受筛选操作影响。
0x03 – 3 – QuerySet 是惰性的
QuerySet 是惰性的 —— 创建 QuerySet 并不会引发任何数据库活动。
你可以将一整天的过滤器都堆积在一起,Django 只会在 QuerySet 被 计算 时执行查询操作。
例如:
>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q = q.exclude(body_text__icontains="food")
>>> print(q)
虽然这看起来像是三次数据库操作,实际上只在最后一行 print(q)
做了一次。
一般来说, QuerySet 的结果直到你 “要使用” 时才会从数据库中拿出。
当你要用时,才通过数据库 计算 出 QuerySet。
0x03 – 用 get() 检索单个对象
filter() 总是返回一个 QuerySet,即便只有一个对象满足查询条件 —— 这种情况下, QuerySet 只包含了一个元素。
若你知道只会有一个对象满足查询条件,你可以在 Manager 上使用 get()
方法,它会直接返回这个对象
>>> one_entry = Entry.objects.get(pk=1)
你可以对 get() 使用与 filter() 类似的所有查询表达式 —— 同样的,参考下面的 字段查询。
注意:
使用切片 [0] 时的 get() 和 filter() 有点不同。
如果没有满足查询条件的结果, get() 会抛出一个 DoesNotExist 异常。
该异常是执行查询的模型类的一个属性 —— 所有,上述代码中,若没有哪个 Entry 对象的主键是 1,Django 会抛出 Entry.DoesNotExist。
类似的,Django 会在有不止一个记录满足 get() 查询条件时发出警告。
这时,Django 会抛出 MultipleObjectsReturned,这同样也是模型类的一个属性。
0x04 – 其他 QuerySet 方法
大多数情况下,你会在需要从数据库中检索对象时使用 all()
, get()
, filter()
和 exclude()
。然而,这样远远不够;完整的各种 QuerySet 方法请参阅 QuerySet API 参考。
0x05 – 限制 QuerySet 条目数
利用 Python 的数组切片语法将 QuerySet 切成指定长度。
这等价于 SQL 的 LIMIT 和 OFFSET 子句。
例如:
>>> Entry.objects.all()[:5] # 这将返回前 5 个对象 (LIMIT 5):
>>> Entry.objects.all()[5:10] # 这会返回第 6 至第 10 个对象 (OFFSET 5 LIMIT 5):
- 不支持负索引
Entry.objects.all()[-1] # 这种写法是不支持的
- 一般情况下, QuerySet 的切片返回一个新的 QuerySet —— 其并未执行查询。
一个特殊情况是使用了的 Python 切片语法的 “步长”。
# 这将会实际的执行查询命令,为了获取从前 10 个对象中,每隔一个抽取的对象组成的列表
>>> Entry.objects.all()[:10:2]
- 由于对 queryset 切片工作方式的模糊性,禁止对 QuerySet 切片进行进一步的排序或过滤。
要检索 单个 对象而不是一个列表时(例如 SELECT foo FROM bar LIMIT 1),请使用索引,而不是切片。
>>> Entry.objects.order_by('headline')[0] # 这会返回按标题字母排序后的第一个 Entry
# 这大致等价于:
>>> Entry.objects.order_by('headline')[0:1].get()
注意一下,若没有对象满足给定条件,前者会抛出 IndexError
,而后者会抛出 DoesNotExist
。
0x06 – 字段查询
字段查询:即你如何制定 SQL WHERE 子句。
它们以关键字参数的形式传递给 QuerySet 方法 filter()
, exclude()
和 get()
。
基本的查询关键字参数遵照 field__lookuptype=value
。(有个双下划线)
>>> Entry.objects.filter(pub_date__lte='2006-01-01')
转换为 SQL 语句大致如下:
SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';
这是怎么做到的:Python 能定义可接受任意数量 name-value 参数的函数,参数名和值均在运行时计算。
若你传入了无效的关键字参数,查询函数会抛出 TypeError
。
数据库 API 支持两套查询类型;完整参考文档位于 字段查询参考。
以下是一些常见的查询:
exact
# 一个 "exact" 匹配的例子:
>>> Entry.objects.get(headline__exact="Cat bites dog")
# 会生成这些 SQL:
SELECT ... WHERE headline = 'Cat bites dog';
若你未提供查询类型(若关键字参数未包含双下划线) —— 查询类型默认会被指定为 exact。
这是为了方便,因为 exact 查询是最常见的。
# 以下两条语句是等价的:
>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
iexact
不分大小写的匹配
>>> Blog.objects.get(name__iexact="beatles blog")
# 会匹配标题为 "Beatles Blog", "beatles blog", 甚至 "BeAtlES blOG" 的 Blog。
contains
.
大小写敏感的包含测试。
Entry.objects.get(headline__contains='Lennon')
# 粗略地转为 SQL:
SELECT ... WHERE headline LIKE '%Lennon%';
# 注意这将匹配标题 'Today Lennon honored',而不是 'today lennon honored'。
icontains
这是大小写不敏感的版本startswith
,endswith
以……开头和以……结尾的查找。istartswith
和iendswith
。
当然也有大小写不敏感的版本
同样,这只介绍了皮毛。完整的参考能在 field 查询参考 找到。
0x07 – 跨关系查询
Django 提供了一种强大而直观的方式来“追踪”查询中的关系,在幕后自动为你处理 SQL JOIN 关系。
为了跨越关系,跨模型使用关联字段名,字段名由双下划线分割,直到拿到想要的字段
。
# 本例检索出所有的 Entry 对象,其 Blog 的 name 为 'Beatles Blog' :
>>> Entry.objects.filter(blog__name='Beatles Blog')
跨域的深度随你所想。
它也向后工作。 虽然它可以自定义,但默认情况下,您使用模型的小写名称在查找中引用“反向”关系。
# 本例检索的所有 Blog 对象均拥有少一个 标题 含有 'Lennon' 的条目:
>>> Blog.objects.filter(entry__headline__contains='Lennon')
如果你在跨多个关系进行筛选,而某个中间模型的没有满足筛选条件的值,Django 会将它当做一个空的(所有值都是 NULL)但是有效的对象。这样就意味着不会抛出错误。
Blog.objects.filter(entry__authors__name='Lennon')
在这个过滤器中,(假设有个关联的 Author 模型),若某项条目没有任何关联的 author,它会被视作没有关联的 name,而不是因为缺失 author 而抛出错误。大多数情况下,这就是你期望的。唯一可能使你迷惑的场景是在使用 isnull 时。因此:
Blog.objects.filter(entry__authors__name__isnull=True)
将会返回 Blog 对象,包含 author 的 name 为空的对象,以及那些 entry 的 author 为空的对象。若你不想要后面的对象,你可以这样写:
Blog.objects.filter(entry__authors__isnull=False, entry__authors__name__isnull=True)
0x07 – 1 – 跨多值关联
当基于 ManyToManyField
或反向 ForeignKey
筛选某对象时,你可能对两种不同类型的过滤器感兴趣。
假设 Blog/Entry 关联关系(Blog 对 Entry 是一种一对多关联关系)。
我们可能对那些条目标题同时含有 “Lennon” 且发布于 2008 年的博客感兴趣。
或者,我们可能想找到那些条目标题包含 “Lennon” 或发布于 2008 的博客。
由于多个 Entry能同时关联至一个Blog,两种查询都是可行的,且在某些场景下非常有用。
同样的场景也发生在 ManyToManyField。
例如,若有个 Entry 拥有一个叫做 tags 的 ManyToManyField,要从关联至 tags 中的条目中找到名为 “music” 和 “bands” 的条目,或要找到某个标签名为 “music” 且状态为 “public” 的条目。
要处理这两种情况,Django 有一套统一的方式处理 filter()
调用。
配置给某次 filter() 的所有条件会在调用时同时生效,筛选出满足条件的项目。
连续的 filter() 调用进一步限制了对象结果集,但对于多值关联来说,限制条件作用于链接至主模型的对象,而不一定是那些被前置 filter() 调用筛选的对象。
这听起来可能有点迷糊,所以需要一个例子来解释一下。
要筛选出所有关联条目同时满足标题含有 “Lennon” 且发布于 2008 (同一个条目,同时满足两个条件)年的博客,我们会这样写:
Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)
要筛选所有条目标题包含 “Lennon” 或条目发布于 2008 年的博客,我们会这样写:
Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)
假设只有一个博客,拥有的条目同时满足标题含有 “Lennon” 且发布于 2008 年,但是发布于 2008 年的条目的标题均不含有 “Lennon”。第一项查询不会返回任何博客,而第二项查询会返回此博客。
在第二个例子中,第一个过滤器限制结果集为那些关联了标题包含 “Lennon” 的条目的博客。第二个过滤器进一步要求结果集中的博客要发布于 2008 年。第二个过滤器筛选的条目与第一个过滤器筛选的可能不尽相同。我们是用过滤器语句筛选 Blog,而不是 Entry。
filter() 的查询行为会跨越多值关联,就像前文说的那样,并不与 exclude() 相同。相反,一次 exclude() 调用的条件并不需要指向同一项目。
例如,以下查询会排除那些关联条目标题包含 “Lennon” 且发布于 2008 年的博客:Blog.objects.exclude( entry__headline__contains='Lennon', entry__pub_date__year=2008, )
但是,与 filter() 的行为不同,其并不会限制博客同时满足这两种条件。要这么做的话,也就是筛选出所有条目标题不带 “Lennon” 且发布年不是 2008 的博客,你需要做两次查询:
Blog.objects.exclude( entry__in=Entry.objects.filter( headline__contains='Lennon', pub_date__year=2008, ), )
0x08 – 过滤器可以为模型指定字段
在之前的例子中,我们已经构建过的 filter
都是将模型字段值与常量做比较。
但是,要怎么做才能将模型字段值与同一模型中的另一字段做比较呢?
Django 提供了 F 表达式 实现这种比较。
F() 的实例充当查询中的模型字段的引用。
这些引用可在查询过滤器中用于在同一模型实例中比较两个不同的字段。
例如:
# 要查出所有评论数大于 pingbacks 的博客条目,我们构建了一个 F() 对象,指代 pingback 的数量,然后在查询中使用该 F() 对象:
>>> from django.db.models import F
>>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks'))
Django 支持对 F() 对象进行加、减、乘、除、求余和次方,另一操作数既可以是常量,也可以是其它 F() 对象。
# 要找到那些评论数两倍于 pingbacks 的博客条目,我们这样修改查询条件:
>>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks') * 2)
要找出所有评分低于 pingback 和评论总数之和的条目,修改查询条件:
>>> Entry.objects.filter(rating__lt=F('number_of_comments') + F('number_of_pingbacks'))
你也能用双下划线在 F() 对象中通过关联关系查询。
带有双下划线的 F() 对象将引入访问关联对象所需的任何连接。
例如:
# 要检索出所有作者名与博客名相同的博客,这样修改查询条件:
>>> Entry.objects.filter(authors__name=F('blog__name'))
# 对于 date 和 date/time 字段,你可以加上或减去一个 timedelta 对象。以下会返回所有发布 3 天后被修改的条目:
>>> from datetime import timedelta
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))
F() 对象通过 .bitand(), .bitor(), .bitxor(),.bitrightshift() 和 .bitleftshift() 支持位操作。
例子:
>>> F('somefield').bitand(16)
0x09 – 表达式可以引用转换
django 3.2 新功能,参考
0x0a – 主键(pk)查询快捷方式
出于方便的目的,Django 提供了一种 pk 查询快捷方式, pk 表示主键 "primary key"
。
示例 Blog 模型中,主键是 id 字段,所以这 3 个语句是等效的:
>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
>>> Blog.objects.get(pk=14) # pk implies id__exact
# pk 的使用并不仅限于 __exact 查询——任何的查询项都能接在 pk 后面,执行对模型主键的查询:
# Get blogs entries with id 1, 4 and 7
>>> Blog.objects.filter(pk__in=[1,4,7])
# Get all blog entries with id > 14
>>> Blog.objects.filter(pk__gt=14)
# pk 查找也支持跨连接。例如,以下 3 个语句是等效的:
>>> Entry.objects.filter(blog__id__exact=3) # Explicit form
>>> Entry.objects.filter(blog__id=3) # __exact is implied
>>> Entry.objects.filter(blog__pk=3) # __pk implies __id__exact
0x0b – 在 LIKE 语句中转义百分号和下划线
等效于 LIKE SQL 语句的字段查询子句 (iexact, contains, icontains, startswith, istartswith, endswith 和 iendswith) 会将 LIKE 语句中有特殊用途的两个符号,即百分号和下划线自动转义。(在 LIKE 语句中,百分号匹配多个任意字符,而下划线匹配一个任意字符。)
这意味着事情应该直观地工作,这样抽象就不会泄露。
例如:
# 要检索所有包含百分号的条目,就像对待其它字符一样使用百分号:
>>> Entry.objects.filter(headline__contains='%')
# Django 为你小心处理了引号;生成的 SQL 语句看起来像这样:
SELECT ... WHERE headline LIKE '%\%%';
# 同样的处理也包括下划线。百分号和下划线都为你自动处理,你无需担心。
0x0c – 缓存和 QuerySet
- 每个 QuerySet 都带有缓存,尽量减少数据库访问。
理解它是如何工作的能让你编写更高效的代码。
- 新创建的 QuerySet 缓存是空的。
- 一旦要计算 QuerySet 的值,就会执行数据查询,随后,Django 就会将查询结果保存在 QuerySet 的缓存中,并返回这些显式请求的缓存(例如,下一个元素,若 QuerySet 正在被迭代)。
- 后续针对 QuerySet 的计算会复用缓存结果。
牢记这种缓存行为,在你错误使用 QuerySet 时可能会被它咬一下。
例如:
# 以下会创建两个 QuerySet,计算它们,丢掉它们:
>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])
这意味着同样的数据库查询会被执行两次,实际加倍了数据库负载。
同时,有可能这两个列表不包含同样的记录,因为在两次请求间,可能有 Entry 被添加或删除了。
要避免此问题,保存 QuerySet 并复用它:
>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # Evaluate the query set.
>>> print([p.pub_date for p in queryset]) # Re-use the cache from the evaluation.
0x0c – 1 – 当 QuerySet 未被缓存时
查询结果集并不总是缓存结果。
当仅计算查询结果集的部分时,会校验缓存,若没有填充缓存,则后续查询返回的项目不会被缓存。
特别地说,这意味着使用数组切片或索引的 限制查询结果集 不会填充缓存。
例如:
# 重复的从某个查询结果集对象中取指定索引的对象会每次都查询数据库:
>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # Queries the database
>>> print(queryset[5]) # Queries the database again
# 不过,若全部查询结果集已被检出,就会去检查缓存:
>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # Queries the database
>>> print(queryset[5]) # Uses cache
>>> print(queryset[5]) # Uses cache
# 以下展示一些例子,这些动作会触发计算全部的查询结果集,并填充缓存的过程:
>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)
注解:
只是打印查询结果集不会填充缓存。因为调用 repr() 仅返回了完整结果集的一个切片。
2021年10月1日