执行查询

一旦创建 数据模型 后,Django 自动给予你一套数据库抽象 API,允许你创建,检索,更新和删除对象。本页介绍如何使用这些 API。参考 数据模型参考 获取所有查询选项的完整细节。

在本指南中(以及在参考资料中),我们将提及以下模型,它们构成了一个博客应用程序:

from datetime import date

from django.db import models


class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def __str__(self):
        return self.name


class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

    def __str__(self):
        return self.name


class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField(default=date.today)
    authors = models.ManyToManyField(Author)
    number_of_comments = models.IntegerField(default=0)
    number_of_pingbacks = models.IntegerField(default=0)
    rating = models.IntegerField(default=5)

    def __str__(self):
        return self.headline

创建对象

为了用 Python 对象展示数据表对象,Django 使用了一套直观的系统:一个模型类代表一张数据表,一个模型类的实例代表数据库表中的一行记录。

要创建一个对象,用关键字参数初始化它,然后调用 save() 将其存入数据库。

假设模型存在于文件 mysite/blog/models.py 中,这里是一个示例:

>>> from blog.models import Blog
>>> b = Blog(name="Beatles Blog", tagline="All the latest Beatles news.")
>>> b.save()

这在幕后执行了 INSERT SQL 语句。Django 在你显式调用 save() 才操作数据库。

save() 方法没有返回值。

参见

save() 接受很多此处未介绍的高级选项。参考文档 save() 获取完整细节。

要一步创建并保存一个对象,使用 create() 方法。

将修改保存至对象

要将修改保存至数据库中已有的某个对象,使用 save()

给定一个已经保存到数据库中的 Blog 实例 b5,这个示例会修改其名称并更新其在数据库中的记录:

>>> b5.name = "New name"
>>> b5.save()

这在幕后执行了 UPDATE SQL 语句。Django 在你显示调用 save() 后才操作数据库。

保存 ForeignKeyManyToManyField 字段

更新 ForeignKey 字段的方法与保存普通字段完全相同——将正确类型的对象分配给相应的字段。这个示例更新了 Entry 实例 entryblog 属性,假设已经保存了适当的 EntryBlog 实例到数据库中(因此我们可以在下面检索到它们):

>>> from blog.models import Blog, Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

更新 ManyToManyField 有一些不同之处——可以使用字段上的 add() 方法来添加一个记录到关系中。这个示例将 Author 实例 joe 添加到 entry 对象中:

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)

要一次性添加多个记录到 ManyToManyField,在调用 add() 时包括多个参数,如下所示:

>>> john = Author.objects.create(name="John")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)

Django 会在添加或指定错误类型的对象时报错。

检索对象

要从数据库检索对象,要通过模型类的 Manager 构建一个 QuerySet

一个 QuerySet 代表来自数据库中对象的一个集合。它可以有 0 个,1 个或者多个 filters. Filters,可以根据给定参数缩小查询结果量。在 SQL 的层面上, QuerySet 对应 SELECT 语句,而*filters*对应类似 WHERELIMIT 的限制子句。

通过使用你的模型的 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,包含的对象 满足给定查询参数。

查询参数(**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,因此可以将细化操作链接在一起。例如:

>>> 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。每一次细化都创建了一个独立且不同的 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 是惰性的

QuerySets 是惰性的 —— 创建 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。关于何时才真的执行计算的更多细节,参考 什么时候 QuerySet 被执行

get() 检索单个对象

filter() 总是返回一个 QuerySet,即便只有一个对象满足查询条件 —— 这种情况下, QuerySet 只包含了一个元素。

如果你知道只有一个对象符合你的查询条件,你可以在 Manager 上使用 get() 方法,它会直接返回该对象:

>>> one_entry = Entry.objects.get(pk=1)

你可以对 get() 使用与 filter() 类似的所有查询表达式 —— 同样的,参考下面的 Field lookups

注意, 使用切片 [0] 时的 get()filter() 有点不同。如果没有满足查询条件的结果, get() 会抛出一个 DoesNotExist 异常。该异常是执行查询的模型类的一个属性 —— 所有,上述代码中,若没有哪个 Entry 对象的主键是 1,Django 会抛出 Entry.DoesNotExist

类似了,Django 会在有不止一个记录满足 get() 查询条件时发出警告。这时,Django 会抛出 MultipleObjectsReturned,这同样也是模型类的一个属性。

其它 QuerySet 方法

大多数情况下,你会在需要从数据库中检索对象时使用 all()get()filter()exclude()。然而,这样远远不够;完整的各种 QuerySet 方法请参阅 QuerySet API 参考

限制 QuerySet 条目数

利用 Python 的数组切片语法将 QuerySet 切成指定长度。这等价于 SQL 的 LIMITOFFSET 子句。

例如,这返回前5个对象(LIMIT 5):

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

这返回第六到第十个对象(OFFSET 5 LIMIT 5):

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

不支持负索引 (例如 Entry.objects.all()[-1])

通常,对 QuerySet 进行切片会返回一个新的 QuerySet,它不会评估查询。一个例外是如果使用了 Python 切片语法的 "step" 参数。例如,这会实际执行查询,以返回前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。参考 get() 获取更多细节。

字段查询

字段查询即你如何制定 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 参数的函数,参数名和值均在运行时计算。更多信息,请参考官方 Python 教程中的 Keyword Arguments

查询子句中指定的字段必须是模型的一个字段名。不过也有个例外,在 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
以……开头和以……结尾的查找。当然也有大小写不敏感的版本,名为 istartswithiendswith

同样,这只介绍了皮毛。完整的参考能在 field 查询参考 找到。

跨关系查询

Django 提供了一种强大而直观的方式来“追踪”查询中的关系,在幕后自动为你处理 SQL JOIN 关系。为了跨越关系,跨模型使用关联字段名,字段名由双下划线分割,直到拿到想要的字段。

这个示例检索所有具有 name'Beatles Blog'BlogEntry 对象:

>>> Entry.objects.filter(blog__name="Beatles Blog")

跨域的深度随你所想。

它也可以反向工作。虽然它 可以自定义,默认情况下,你在查找中使用模型的小写名称来引用一个 “反向” 关系。

这个示例检索所有至少有一个 headline 包含 'Lennon'EntryBlog 对象:

>>> 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 对象,包含 authorname 为空的对象,以及那些 entryauthor 为空的对象。若你不想要后面的对象,你可以这样写:

Blog.objects.filter(entry__authors__isnull=False, entry__authors__name__isnull=True)

跨多值关联

当跨越 ManyToManyField 或反查 ForeignKey (例如从 BlogEntry )时,对多个属性进行过滤会产生这样的问题:是否要求每个属性都在同一个相关对象中重合。我们可能会寻找那些在标题中含有 “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() 的实例充当查询中的模型字段的引用。这些引用可在查询过滤器中用于在同一模型实例中比较两个不同的字段。

例如,要找到所有具有比 pingback 更多评论的博客条目的列表,我们构建一个引用 pingback 计数的 F() 对象,并在查询中使用该 F() 对象:

>>> from django.db.models import F
>>> Entry.objects.filter(number_of_comments__gt=F("number_of_pingbacks"))

Django 支持在 F() 对象中使用加法、减法、乘法、除法、取模和幂算术,既可以与常数一起使用,也可以与其他 F() 对象一起使用。要查找所有具有评论数超过 pingback 两倍的博客条目,我们修改查询如下:

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

对于日期和日期/时间字段,你可以添加或减去一个 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)

Oracle

Oracle 不支持按位 XOR 操作。

表达式可以引用变换

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

例如,要找到所有与它们上次修改的年份相同的 Entry 对象:

>>> from django.db.models import F
>>> Entry.objects.filter(pub_date__year=F("mod_date__year"))

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

>>> from django.db.models import Min
>>> Entry.objects.aggregate(first_published_year=Min("pub_date__year"))

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

>>> from django.db.models import OuterRef, Subquery, Sum
>>> 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 字段,因此以下三个语句是等效的:

>>> 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 查询也可以跨越关联进行。例如,以下三个语句是等效的:

>>> 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 语句的字段查询子句 (iexactcontainsicontainsstartswithistartswithendswithiendswith) 会将 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])  # Reuse 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__() 仅返回了完整结果集的一个切片。

异步查询

如果你正在编写异步视图或代码,你不能像我们上面描述的那样完全使用 ORM 进行查询,因为你不能从异步代码中调用 阻塞的 同步代码 —— 它会阻塞事件循环(或者更可能的是,Django 会注意到并引发 SynchronousOnlyOperation 以阻止发生这种情况)。

幸运的是,你可以使用 Django 的异步查询 API 执行许多查询。每个可能阻塞的方法,比如 get()delete(),都有一个异步的变体(aget()adelete()),当你遍历结果时,你可以使用异步迭代(async for)。

查询迭代

默认情况下,通过 for 遍历查询会在幕后导致阻塞的数据库查询,因为 Django 在迭代时加载结果。要解决这个问题,你可以切换到 async for

async for entry in Authors.objects.filter(name__startswith="A"):
    ...

请注意,你也不能执行其他可能会遍历查询集的操作,比如用 list() 包装它来强制评估它(如果需要,可以在推导式中使用 async for)。

因为 QuerySet 方法,比如 filter()exclude(),实际上不会运行查询 —— 它们设置了查询集,以便在迭代时运行 —— 所以你可以在异步代码中自由使用它们。关于哪些方法可以继续像这样使用,哪些有异步版本的指南,请阅读下一节。

QuerySet 和管理器方法

管理器和查询集上的一些方法 - 比如 get()first() - 强制执行查询集并会阻塞。而一些方法,比如 filter()exclude(),不会强制执行查询集,因此可以从异步代码中安全运行。但是你应该如何区分它们呢?

虽然你可以查看是否有方法的名称前缀是 a (例如,我们有 aget(),但没有 afilter()),但有一种更合理的方法是查看 QuerySet 参考文档,了解方法的类型。

在这里,你会发现查询集的方法分成了两个部分:

  • 返回新查询集的方法:这些方法是非阻塞的,并且没有异步版本。你可以在任何情况下自由使用它们,不过在使用之前请阅读关于 defer()only() 的注释。
  • 不返回查询集的方法:这些方法是阻塞的,并且有异步版本 - 在每个方法的文档中都有它的异步名称,尽管我们的标准模式是在名称前加上 a 前缀。

使用这个区别,你可以确定何时需要使用异步版本,何时不需要。例如,以下是一个有效的异步查询示例:

user = await User.objects.filter(username=my_input).afirst()

filter() 返回一个查询集,因此在异步环境中可以继续链式调用它,而 first() 会计算并返回一个模型实例 - 因此,我们改用 afirst(),并在整个表达式的前面使用 await 以异步友好的方式调用它。

备注

如果你忘记了加上 await,你可能会看到类似 "coroutine object has no attribute x" 的错误,或者在你的模型实例位置看到 "<coroutine …>" 字符串。如果你看到这些错误,说明你忘记了在某个地方添加 await 来将协程转换为真实值。

事务

目前,异步查询和更新 支持事务。如果尝试在异步查询中使用事务,会引发 SynchronousOnlyOperation 错误。

如果希望使用事务,建议将 ORM 代码编写在单独的同步函数中,然后使用 sync_to_async 来调用它 - 参阅 异步支持 获取更多信息。

查询 JSONField

JSONField 里的查找实现是不一样的,主要因为存在键转换。为了演示,我们将使用下面这个例子:

from django.db import models


class Dog(models.Model):
    name = models.CharField(max_length=200)
    data = models.JSONField(null=True)

    def __str__(self):
        return self.name

保存和查询 None

与其他字段一样,将 None 存储为字段的值将其存储为 SQL NULL。虽然不推荐这样做,但可以通过使用 Value(None, JSONField()) 来存储 JSON 标量的 null,而不是 SQL NULL

无论存储哪种值,当从数据库检索时,JSON 标量 null 的 Python 表示法与 SQL 的 NULL 相同,即 None。因此,可能很难区分它们。

这只适用于 None 值作为字段的顶级值。如果 None 被保存在 listdict 中,它将始终被解释为 JSON 的 null 值。

在查询时,None 值将始终被解释为 JSON null。要查询 SQL NULL,请使用 isnull

>>> Dog.objects.create(name="Max", data=None)  # SQL NULL.
<Dog: Max>
>>> Dog.objects.create(name="Archie", data=Value(None, JSONField()))  # JSON null.
<Dog: Archie>
>>> Dog.objects.filter(data=None)
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data=Value(None, JSONField()))
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data__isnull=True)
<QuerySet [<Dog: Max>]>
>>> Dog.objects.filter(data__isnull=False)
<QuerySet [<Dog: Archie>]>

除非你确定要使用 SQL 的 NULL 值,否则请考虑设置 null=False 并为空值提供合适的默认值,例如 default=dict

备注

保存 JSON 的 null 值不违反 Django 的 null=False

键、索引和路径转换

要基于给定的字典键进行查询,请将该键用作查找名称:

>>> Dog.objects.create(
...     name="Rufus",
...     data={
...         "breed": "labrador",
...         "owner": {
...             "name": "Bob",
...             "other_pets": [
...                 {
...                     "name": "Fishy",
...                 }
...             ],
...         },
...     },
... )
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": None})
<Dog: Meg>
>>> Dog.objects.filter(data__breed="collie")
<QuerySet [<Dog: Meg>]>

可以将多个键链接在一起以形成路径查找:

>>> Dog.objects.filter(data__owner__name="Bob")
<QuerySet [<Dog: Rufus>]>

如果键是整数,则将其解释为数组中的索引变换:

>>> Dog.objects.filter(data__owner__other_pets__0__name="Fishy")
<QuerySet [<Dog: Rufus>]>

如果要查询的键与另一个查询的键名冲突,请改用 contains 来查询。

要查询缺少的键,请使用 isnull 查找:

>>> Dog.objects.create(name="Shep", data={"breed": "collie"})
<Dog: Shep>
>>> Dog.objects.filter(data__owner__isnull=True)
<QuerySet [<Dog: Shep>]>

备注

上面给出的例子隐式地使用了 exact 查找。Key,索引和路径转换也可以用:icontainsendswithiendswithiexactregexiregexstartswithistartswithltltegtgte 以及 包含与键查找

KT() 表达式

class KT(lookup)

表示 JSONField 的键、索引或路径变换的文本值。您可以在 lookup 中使用双下划线符号来链接字典键和索引变换。

例如:

>>> from django.db.models.fields.json import KT
>>> Dog.objects.create(
...     name="Shep",
...     data={
...         "owner": {"name": "Bob"},
...         "breed": ["collie", "lhasa apso"],
...     },
... )
<Dog: Shep>
>>> Dogs.objects.annotate(
...     first_breed=KT("data__breed__1"), owner_name=KT("data__owner__name")
... ).filter(first_breed__startswith="lhasa", owner_name="Bob")
<QuerySet [<Dog: Shep>]>

备注

由于键-路径查询的工作方式,exclude()filter() 不能保证产生详尽的集合。如果你想包含没有路径的对象,请添加 isnull 查找。

警告

由于任何字符串都可以成为 JSON 对象中的一个键,除了下面列出的那些之外,任何查询都将被解释为一个键查询。不会出现错误。要格外小心打字错误,并经常检查你的查询是否按你的意图进行。

MariaDB 和 Oracle 用户

在键、索引或路径转换上使用 order_by() 将使用值的字符串表示法对对象进行排序。这是因为 MariaDB 和 Oracle 数据库没有提供将 JSON 值转换为其等价的 SQL 值的函数。

Oracle 用户

在 Oracle 数据库中,在 exclude() 查询中使用 None 作为查询值,将返回没有 null 作为指定路径的对象,包括没有路径的对象。在其他数据库后端,该查询将返回具有该路径且其值不是 null 的对象。

PostgreSQL 用户

在 PostgreSQL 上,如果只使用一个键或索引,那么会使用 SQL 运算符 -> 。如果使用多个操作符,则会使用 #> 运算符。

SQLite 用户

在SQLite上,字符串值 "true""false""null" 将始终被解释为分别为 TrueFalse 和 JSON null

包含与键查找

contains

contains 查询在 JSONField 上被覆盖。返回的对象是那些包含给定键值对的顶层字段的对象。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador", "owner": "Bob"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.create(name="Fred", data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contains={"owner": "Bob"})
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>
>>> Dog.objects.filter(data__contains={"breed": "collie"})
<QuerySet [<Dog: Meg>]>

Oracle 和 SQLite

Oracle 和 SQLite 不支持 contains

contained_by

这是 contains 查询的反向 - 返回的对象将是那些对象,其上的键值对是传递值中的子集。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador", "owner": "Bob"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.create(name="Fred", data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contained_by={"breed": "collie", "owner": "Bob"})
<QuerySet [<Dog: Meg>, <Dog: Fred>]>
>>> Dog.objects.filter(data__contained_by={"breed": "collie"})
<QuerySet [<Dog: Fred>]>

Oracle 和 SQLite

Oracle 和 SQLite 不支持 contained_by

has_key

返回具有给定键位于数据的顶层的对象。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.filter(data__has_key="owner")
<QuerySet [<Dog: Meg>]>

has_keys

返回所有给定键位于数据的顶层的对象。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.filter(data__has_keys=["breed", "owner"])
<QuerySet [<Dog: Meg>]>

has_any_keys

返回任何给定键位于数据的顶层的对象。例如:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
<Dog: Rufus>
>>> Dog.objects.create(name="Meg", data={"owner": "Bob"})
<Dog: Meg>
>>> Dog.objects.filter(data__has_any_keys=["owner", "breed"])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

通过 Q 对象完成复杂查询

在类似 filter() 中,查询使用的关键字参数是通过 "AND" 连接起来的。如果你要执行更复杂的查询(例如,由 OR 语句连接的查询),你可以使用 Q 对象

一个 Q 对象 (django.db.models.Q) 用于压缩关键字参数集合。这些关键字参数由前文 "Field lookups" 指定。

例如,该 Q 对象压缩了一个 LIKE 查询:

from django.db.models import Q

Q(question__startswith="What")

Q 对象可以使用 &|^ 运算符组合。当在两个 Q 对象上使用运算符时,它会产生一个新的 Q 对象。

例如,该语句生成一个 Q 对象,表示两个 "question_startswith" 查询语句之间的 "OR" 关系:

Q(question__startswith="Who") | Q(question__startswith="What")

这等同于以下 SQL 的 WHERE 子句:

WHERE question LIKE 'Who%' OR question LIKE 'What%'

您可以使用 &|^ 运算符组合任意复杂的语句,并使用括号进行分组。此外,可以使用 ~ 运算符对 Q 对象进行取反,从而允许组合查询,既包括正常查询又包括取反(NOT)查询:

Q(question__startswith="Who") | ~Q(pub_date__year=2005)

每个接受关键字参数的查询函数 (例如 filter()exclude()get()) 也同时接受一个或多个 Q 对象作为位置(未命名的)参数。若你为查询函数提供了多个 Q 对象参数,这些参数会通过 "AND" 连接。例子:

Poll.objects.get(
    Q(question__startswith="Who"),
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
)

...粗略地转为 SQL:

SELECT * from polls WHERE question LIKE 'Who%'
    AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

查询函数能混合使用 Q 对象和关键字参数。所有提供给查询函数的参数(即关键字参数或 Q 对象)均通过 "AND" 连接。然而,若提供了 Q 对象,那么它必须位于所有关键字参数之前。例子:

Poll.objects.get(
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
    question__startswith="Who",
)

……会是一个有效的查询,等效于前文的例子;但是:

# INVALID QUERY
Poll.objects.get(
    question__startswith="Who",
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
)

……却是无效的。

参见

Django 单元测试中的 OR 查询实例 展示了 Q 的用法。

比较对象

要比较两个模型实例,使用标准的 Python 比较操作符,两个等号: ==。实际上,这比较了两个模型实例的主键值。

使用上面的 Entry 示例,下面的两个语句是等效的:

>>> some_entry == other_entry
>>> some_entry.id == other_entry.id

如果模型的主键不叫 id,也没有问题。比较操作将始终使用主键,无论它叫什么。例如,如果一个模型的主键字段叫 name,下面两个语句是等效的:

>>> some_obj == other_obj
>>> some_obj.name == other_obj.name

删除对象

删除方法方便地被命名为 delete()。这个方法立即删除对象并返回被删除的对象数以及一个包含每种对象类型的删除数的字典。例如:

>>> e.delete()
(1, {'blog.Entry': 1})

你也能批量删除对象。所有 QuerySet 都有个 delete() 方法,它会删除 QuerySet 中的所有成员。

例如,这将删除所有具有 pub_date 年份为 2005 的 Entry 对象:

>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})

请记住,只要有机会的话,这会通过纯 SQL 语句执行,所以就无需在过程中调用每个对象的删除方法了。若你为模型类提供了自定义的 delete() 方法,且希望确保调用了该方法,你需要 “手动” 删除该模型的实例(例如,如,遍历 QuerySet,在每个对象上分别调用 delete() 方法),而不是使用 QuerySet 的批量删除方法 delete()

当 Django 删除某个对象时,默认会模仿 SQL 约束 ON DELETE CASCADE 的行为——换而言之,某个对象被删除时,关联对象也会被删除。例子:

b = Blog.objects.get(pk=1)
# This will delete the Blog and all of its Entry objects.
b.delete()

这种约束行为由 ForeignKeyon_delete 参数指定。

注意 delete() 是唯一未在 Manager 上暴漏的 QuerySet 方法。这是一种安全机制,避免你不小心调用了 Entry.objects.delete(),删除了 所有的 条目。若你 确实 想要删除所有对象,你必须显示请求完整结果集合:

Entry.objects.all().delete()

复制模型实例

虽然没有内置的方法来复制模型实例,但可以轻松地创建新的实例,并复制所有字段的值。在最简单的情况下,您可以将 pk 设置为 None 并将 _state.adding 设置为 True。使用我们的博客示例:

blog = Blog(name="My blog", tagline="Blogging is easy")
blog.save()  # blog.pk == 1

blog.pk = None
blog._state.adding = True
blog.save()  # blog.pk == 2

若你使用了集成,事情会更复杂。考虑下 Blog 的一个子类:

class ThemeBlog(Blog):
    theme = models.CharField(max_length=200)


django_blog = ThemeBlog(name="Django", tagline="Django is easy", theme="python")
django_blog.save()  # django_blog.pk == 3

由于继承的工作方式,您必须将 pkid 都设置为 None,并将 _state.adding 设置为 True

django_blog.pk = None
django_blog.id = None
django_blog._state.adding = True
django_blog.save()  # django_blog.pk == 4

该方法不会拷贝不是模型数据表中的关联关系。例如, Entry 有一个对 AuthorManyToManyField 关联关系。在复制条目后,你必须为新条目设置多对多关联关系。

entry = Entry.objects.all()[0]  # some previous entry
old_authors = entry.authors.all()
entry.pk = None
entry._state.adding = True
entry.save()
entry.authors.set(old_authors)

对于 OneToOneField 关联,你必须拷贝关联对象,并将其指定给新对象的关联字段,避免违反一对一唯一性约束。例如,指定前文复制的 entry:

detail = EntryDetail.objects.all()[0]
detail.pk = None
detail._state.adding = True
detail.entry = entry
detail.save()

一次修改多个对象

有时候,你想统一设置 QuerySet 中的所有对象的某个字段。你可以通过 update() 达到目的。例子:

# Update all the headlines with pub_date in 2007.
Entry.objects.filter(pub_date__year=2007).update(headline="Everything is the same")

您只能使用此方法设置非关联字段和 ForeignKey 字段。要更新非关联字段,请将新值提供为常数。要更新 ForeignKey 字段,请将新值设置为要指向的新模型实例。例如:

>>> b = Blog.objects.get(pk=1)

# Change every Entry so that it belongs to this Blog.
>>> Entry.objects.update(blog=b)

update() 方法会立即应用,并返回查询匹配的行数(如果某些行已经具有新值,则可能不等于更新的行数)。对于要更新的 QuerySet 唯一的限制是它只能访问一个数据库表:模型的主表。您可以基于相关字段进行过滤,但只能更新模型的主表中的列。例如:

>>> b = Blog.objects.get(pk=1)

# Update all the headlines belonging to this Blog.
>>> Entry.objects.filter(blog=b).update(headline="Everything is the same")

要认识到 update() 方法是直接转为 SQL 语句的。这是一种用于直接更新的批量操作。它并不会调用模型的 save() 方法,或发射 pre_savepost_save 信号(调用 save() 会触发信号),或使用 auto_now 字段选项。若想保存 QuerySet 中的每项,并确保调用了每个实例的 save() 方法,你并不需要任何特殊的函数来处理此问题。迭代它们,并调用它们的 save() 方法:

for item in my_queryset:
    item.save()

对于调用 update 的情况,还可以使用 F 表达式 来根据模型中另一个字段的值来更新字段。这在根据当前值增加计数器时特别有用。例如,要增加博客中每篇文章的 pingback 计数:

>>> Entry.objects.update(number_of_pingbacks=F("number_of_pingbacks") + 1)

然而,与筛选和排除子句中的 F() 对象不同,在更新中使用 F() 对象时,不能引入连接,只能引用模型被更新的字段。如果尝试使用 F() 对象引入连接,将引发 FieldError 错误:

# This will raise a FieldError
>>> Entry.objects.update(headline=F("blog__name"))

回归原生 SQL

若你发现需要编写的 SQL 查询语句太过复杂,以至于 Django 的数据库映射无法处理,你可以回归手动编写 SQL。Django 针对编写原生 SQL 有几个选项;参考 执行原生 SQL 查询

最后,Django 数据库层只是一种访问数据库的接口,理解这点非常重要。你也可以通过其它工具,编程语言或数据库框架访问数据库;Django 并没有对数据库数据库做啥独有的操作。

Back to Top