Django4 中文入门教程 Django4.0 执行查询-检索对象

2024-02-25 开发教程 Django4 中文入门教程 匿名 4

要从数据库检索对象,要通过模型类的 ​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 ​对象。

检索全部对象

从数据库中检索对象最简单的方式就是检索全部。为此,在 ​Manager ​上调用 ​all()​ 方法:

>>> all_entries = Entry.objects.all()

方法 ​all()​ 返回了一个包含数据库中所有对象的 ​QuerySet ​对象。

通过过滤器检索指定对象

all() ​返回的 ​QuerySet ​包含了数据表中所有的对象。虽然,大多数情况下,你只需要完整对象集合的一个子集。要创建一个这样的子集,你需要通过添加过滤条件精炼原始 ​QuerySet​。两种最常见的精炼 ​QuerySet ​的方式是:

  • filter(**kwargs)​返回一个新的 ​QuerySet​,包含的对象满足给定查询参数。
  • exclude(**kwargs)​返回一个新的 ​QuerySet​,包含的对象不满足给定查询参数。

例如,要包含获取 2006 年的博客条目(entries blog)的 ​QuerySet​,像这样使用 ​filter()​:

Entry.objects.filter(pub_date__year=2006)

通过默认管理器类也一样:

Entry.objects.all().filter(pub_date__year=2006)

链式过滤器

精炼 ​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 日与今天之间的所有条目。

每个 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)​ 不受筛选操作影响。

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​。

用 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​,这同样也是模型类的一个属性。

其它 QuerySet 方法

大多数情况下,你会在需要从数据库中检索对象时使用 ​all()​, ​get()​, ​filter()​ 和 ​exclude()​。

限制 QuerySet 条目数

利用 Python 的数组切片语法将 ​QuerySet ​切成指定长度。这等价于 SQL 的 ​LIMIT ​和 ​OFFSET ​子句。
例如,这将返回前 5 个对象 (​LIMIT 5​):

>>> Entry.objects.all()[:5]

这会返回第 6 至第 10 个对象 (​OFFSET 5 LIMIT 5​):

>>> Entry.objects.all()[5:10]

不支持负索引 (例如 ​Entry.objects.all()[-1]​)
一般情况下, ​QuerySet ​的切片返回一个新的 ​QuerySet ​—— 其并未执行查询。一个特殊情况是使用了的 Python 切片语法的步长。例如,这将会实际的执行查询命令,为了获取从前 10 个对象中,每隔一个抽取的对象组成的列表:

>>> Entry.objects.all()[:10:2]

由于对 ​queryset ​切片工作方式的模糊性,禁止对其进行进一步的排序或过滤。
要检索 单个 对象而不是一个列表时(例如 ​SELECT foo FROM bar LIMIT 1​),请使用索引,而不是切片。例如,这会返回按标题字母排序后的第一个 ​Entry​:

>>> Entry.objects.order_by('headline')[0]

这大致等价于:

>>> Entry.objects.order_by('headline')[0:1].get()

然而,注意一下,若没有对象满足给定条件,前者会抛出 ​IndexError​,而后者会抛出 ​DoesNotExist​。

字段查询

字段查询即你如何制定 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';

查询子句中指定的字段必须是模型的一个字段名。不过也有个例外,在 ​ForeignKey ​中,你可以指定以 ​_id​ 为后缀的字段名。这种情况下,​value参数需要包含 ​foreign ​模型的主键的原始值。例子:

>>> Entry.objects.filter(blog_id=4)

若你传入了无效的关键字参数,查询函数会抛出 ​TypeError​。
数据库 API 支持两套查询类型。为了让你了解能干啥,以下是一些常见的查询:

exact

一个 exact​ 匹配的例子:

>>> Entry.objects.get(headline__exact="Cat bites dog")

会生成这些 SQL:

SELECT ... WHERE headline = 'Cat bites dog';

若你为提供查询类型 —— 也就说,若关键字参数未包含双下划线 —— 查询类型会被指定为 ​exact​。
例如,以下两条语句是等价的:

>>> Blog.objects.get(id__exact=14)  # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied

这是为了方便,因为 ​exact ​查询是最常见的。

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

分别以搜索开始和以搜索结束。

还有不区分大小写的版本,称为 ​isstartswith ​和 ​iendswith​。

跨关系查询

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)

跨多值关联

当跨越 ​ManyToManyField ​或反查 ​ForeignKey ​(例如从 ​Blog ​到 ​Entry ​)时,对多个属性进行过滤会产生这样的问题:是否要求每个属性都在同一个相关对象中重合。我们可能会寻找那些在标题中含有 ​“Lennon”​ 的 2008 年的博客,或者我们可能会寻找那些仅有 2008 年的任何条目以及一些在标题中含有 ​“Lennon”​ 的较新或较早的条目。
要选择所有包含 2008 年至少一个标题中有​"Lennon"​ 的条目的博客(满足两个条件的同一条目),我们要写:

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"​ 。第一个查询不会返回任何博客,但第二个查询会返回那一个博客。(这是因为第二个过滤器选择的条目可能与第一个过滤器中的条目相同,也可能不相同)。我们是用每个过滤器语句来过滤 ​Blog ​项,而不是 ​Entry ​项)。简而言之,如果每个条件需要匹配相同的相关对象,那么每个条件应该包含在一个 ​filter()​ 调用中。

由于第二个查询链接了多个过滤器,它对主模型进行了多次连接,可能会产生重复的结果。

>>> from datetime import date
>>> beatles = Blog.objects.create(name='Beatles Blog')
>>> pop = Blog.objects.create(name='Pop Music Blog')
>>> Entry.objects.create(
... blog=beatles,
... headline='New Lennon Biography',
... pub_date=date(2008, 6, 1),
... )
<Entry: New Lennon Biography>
>>> Entry.objects.create(
... blog=beatles,
... headline='New Lennon Biography in Paperback',
... pub_date=date(2009, 6, 1),
... )
<Entry: New Lennon Biography in Paperback>
>>> Entry.objects.create(
... blog=pop,
... headline='Best Albums of 2008',
... pub_date=date(2008, 12, 15),
... )
<Entry: Best Albums of 2008>
>>> Entry.objects.create(
... blog=pop,
... headline='Lennon Would Have Loved Hip Hop',
... pub_date=date(2020, 4, 1),
... )
<Entry: Lennon Would Have Loved Hip Hop>
>>> Blog.objects.filter(
... entry__headline__contains='Lennon',
... entry__pub_date__year=2008,
... )
<QuerySet [<Blog: Beatles Blog>]>
>>> Blog.objects.filter(
... entry__headline__contains='Lennon',
... ).filter(
... entry__pub_date__year=2008,
... )
<QuerySet [<Blog: Beatles Blog>, <Blog: Beatles Blog>, <Blog: Pop Music Blog]>

注解:

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,
),
)

过滤器可以为模型指定字段

在之前的例子中,我们已经构建过的 ​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)

表达式可以引用转换

Django 支持在表达式中使用转换。

例如,要查找与上次修改同一年发布的所有条目对象:

>>> Entry.objects.filter(pub_date__year=F('mod_date__year'))

要查找条目发布的最早年份,我们可以发出查询:

>>> Entry.objects.aggregate(first_published_year=Min('pub_date__year'))

此示例查找最高评分条目的值以及每年所有条目的评论总数:

>>> Entry.objects.values('pub_date__year').annotate(
... top_rating=Subquery(
... Entry.objects.filter(
... pub_date__year=OuterRef('pub_date__year'),
... ).order_by('-rating').values('rating')[:1]
... ),
... total_comments=Sum('number_of_comments'),
... )

主键(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

在LIKE语句中转义百分号和下划线

等效于 ​LIKE ​SQL 语句的字段查询子句 (​iexact​, ​contains​, ​icontains​, ​startswith​, ​istartswith​, ​endswith ​和 ​iendswith​) 会将 ​LIKE ​语句中有特殊用途的两个符号,即百分号和下划线自动转义。(在 ​LIKE ​语句中,百分号匹配多个任意字符,而下划线匹配一个任意字符。)
例如,要检索所有包含百分号的条目:

>>> Entry.objects.filter(headline__contains='%')

Django 为你处理了引号;生成的 SQL 语句看起来像这样:

SELECT ... WHERE headline LIKE '%\%%';

同样的处理也包括下划线。百分号和下划线都为你自动处理,你无需担心。

缓存和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.

当 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__() ​仅返回了完整结果集的一个切片。