查询¶
创建完 数据模型 后,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 语句。在显式调用 save()
之前,Django 不会访问数据库。
方法save()
没有返回值。
保存对对象的更改¶
要保存对已存在于数据库中的对象的更改,请使用save()
.
假设已将Blog
实例b5
保存到数据库,此示例将更改其名称并更新其数据库记录。
>>> b5.name = "New name"
>>> b5.save()
这在幕后执行UPDATE
SQL 语句。在您明确调用save()
之前,Django 不会访问数据库。
保存ForeignKey
和ManyToManyField
字段¶
更新ForeignKey
字段的工作方式与保存普通字段完全相同 - 将正确类型的对象分配给该字段。此示例更新Entry
实例entry
的blog
属性,假设Entry
和Blog
的适当实例已保存到数据库(因此我们可以在下面检索它们)。
>>> 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
代表数据库中的一组对象。它可以有零个、一个或多个过滤器。过滤器根据给定的参数缩小查询结果。用 SQL 术语来说,一个 QuerySet
等同于一个 SELECT
语句,而过滤器是一个限制子句,例如 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
对象。
使用过滤器检索特定对象¶
QuerySet
由 all()
返回,它描述了数据库表中的所有对象。但是,通常您需要仅选择完整对象集的子集。
要创建这样的子集,您需要对初始的 QuerySet
进行细化,添加过滤条件。细化 QuerySet
的两种最常见方法是
filter(**kwargs)
- 返回一个新的
QuerySet
,其中包含与给定查找参数匹配的对象。 exclude(**kwargs)
- 返回一个新的
QuerySet
,其中包含与给定查找参数 *不* 匹配的对象。
查找参数(上述函数定义中的 **kwargs
)应采用 字段查找 中描述的格式。
例如,要获取一个包含 2006 年博客文章的 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))
这将获取数据库中所有条目的初始 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
是独立的。第一个是包含所有标题以“What”开头的条目的基本 QuerySet
。第二个是第一个的子集,它包含一个额外的条件,排除 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
通过访问数据库进行 *评估*。有关评估确切发生时间的更多详细信息,请参阅 何时评估 QuerySets。
使用 get()
获取单个对象¶
filter()
始终会返回一个 QuerySet
,即使只有一个对象匹配查询 - 在这种情况下,它将是一个包含单个元素的 QuerySet
。
如果您知道只有一个对象匹配您的查询,则可以使用 get()
方法在 Manager
上,它直接返回对象。
>>> one_entry = Entry.objects.get(pk=1)
您可以将任何查询表达式与 get()
一起使用,就像使用 filter()
一样 - 同样,请参见下面的 字段查找。
请注意,使用 get()
和使用 filter()
以及 [0]
的切片之间存在差异。如果没有结果匹配查询,get()
将引发 DoesNotExist
异常。此异常是正在执行查询的模型类的属性 - 因此在上面的代码中,如果没有主键为 1 的 Entry
对象,Django 将引发 Entry.DoesNotExist
。
同样,如果多个项目匹配 get()
查询,Django 会报错。在这种情况下,它将引发 MultipleObjectsReturned
,它也是模型类本身的属性。
其他 QuerySet
方法¶
大多数情况下,您会使用 all()
、get()
、filter()
和 exclude()
来从数据库中查找对象。但是,这远非全部;请参阅 QuerySet API 参考 以获取所有 QuerySet
方法的完整列表。
限制 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
。 有关更多详细信息,请参阅 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 能够定义接受任意名称-值参数的函数,这些参数的名称和值在运行时进行评估。 有关更多信息,请参阅官方 Python 教程中的 关键字参数。
在查找中指定的字段必须是模型字段的名称。不过,有一个例外,如果是 ForeignKey
,您可以指定以 _id
结尾的字段名称。在这种情况下,value 参数应包含外键模型主键的原始值。例如
>>> Entry.objects.filter(blog_id=4)
如果您传递了无效的关键字参数,查找函数将引发 TypeError
。
数据库 API 支持大约二十多种查找类型;完整的参考可以在 字段查找参考 中找到。为了让您了解可用的内容,以下是一些您可能会用到的更常见的查找
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
- 分别进行以...开头和以...结尾的搜索。还有不区分大小写的版本,称为
istartswith
和iendswith
。
再次强调,这仅仅是皮毛。完整的参考可以在 字段查找参考 中找到。
跨越关系的查找¶
Django 提供了一种强大且直观的“跟随”关系查找方法,在幕后自动处理 SQL 的 JOIN
。要跨越关系,请使用跨模型的相关字段名称,用双下划线分隔,直到到达您想要的字段。
此示例检索所有 Entry
对象,这些对象具有 Blog
,其 name
为 'Beatles Blog'
>>> Entry.objects.filter(blog__name="Beatles Blog")
这种跨越可以像您想要的那样深。
它也可以反向工作。虽然它 can be customized
,但默认情况下,您在查找中使用模型的小写名称来引用“反向”关系。
此示例检索所有 Blog
对象,这些对象至少有一个 Entry
,其 headline
包含 '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
)时,对多个属性进行过滤会引发一个问题,即是否要求每个属性在同一个相关对象中重合。我们可能想要查找具有 2008 年的条目且标题中包含“Lennon”的博客,或者我们可能想要查找仅仅具有 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()
时的行为不同,这不会根据满足这两个条件的条目来限制博客。为了做到这一点,即选择所有不包含在 2008 年发布的“Lennon” 标题下发布的条目的博客,您需要进行两次查询
Blog.objects.exclude(
entry__in=Entry.objects.filter(
headline__contains="Lennon",
pub_date__year=2008,
),
)
过滤器可以引用模型上的字段¶
在迄今为止给出的示例中,我们构建了将模型字段的值与常量进行比较的过滤器。但是,如果您想将模型字段的值与同一模型上的另一个字段进行比较怎么办?
Django 提供了 F expressions
来允许这种比较。 F()
的实例充当查询中模型字段的引用。然后,这些引用可以在查询过滤器中使用,以比较同一模型实例上的两个不同字段的值。
例如,要查找所有评论数量超过 pingback 数量的博客条目列表,我们构建一个 F()
对象来引用 pingback 计数,并在查询中使用该 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 不支持按位异或运算。
表达式可以引用转换¶
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
查找快捷方式,它代表“主键”。
在示例 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 语句等效的字段查找(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]) # Reuse the cache from the evaluation.
当 QuerySet
未被缓存时¶
Querysets 并不总是缓存其结果。当仅评估 部分 queryset 时,会检查缓存,但如果缓存未填充,则后续查询返回的项目不会被缓存。具体来说,这意味着使用数组切片或索引 限制 queryset 不会填充缓存。
例如,重复获取 queryset 对象中的某个索引将每次查询数据库
>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # Queries the database
>>> print(queryset[5]) # Queries the database again
但是,如果整个 queryset 已经过评估,则会检查缓存
>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # Queries the database
>>> print(queryset[5]) # Uses cache
>>> print(queryset[5]) # Uses cache
以下是一些会导致整个 queryset 被评估并因此填充缓存的操作示例
>>> [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 参考 中查找它是什么类型的函数。
在那里,你会发现 QuerySets 上的方法被分为两部分
- 返回新查询集的方法:这些是非阻塞方法,没有异步版本。你可以在任何情况下自由使用它们,但使用它们之前请阅读关于
defer()
和only()
的说明。 - 不返回查询集的方法:这些是阻塞方法,并且有异步版本 - 每个方法的异步名称在其文档中都有说明,但我们的标准模式是在前面添加一个
a
前缀。
使用这种区别,你可以判断何时需要使用异步版本,何时不需要。例如,这是一个有效的异步查询
user = await User.objects.filter(username=my_input).afirst()
filter()
返回一个查询集,因此在异步环境中继续链式调用它是可以的,而 first()
会进行评估并返回一个模型实例 - 因此,我们改为 afirst()
,并在整个表达式的前面使用 await
,以便以异步友好的方式调用它。
注意
如果你忘记添加 await
部分,你可能会看到类似于“协程对象没有属性 x”或“<协程 …>”字符串代替你的模型实例。如果你看到这些错误,说明你缺少一个 await
来将协程转换为真实值。
查询 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
位于 list
或 dict
中,它将始终被解释为 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
不会违反 null=False
。
添加了使用 Value(None, JSONField())
表达 JSON null
的支持。
自版本 4.2 起已弃用: 传递 Value("null")
来表达 JSON null
已被弃用。
键、索引和路径转换¶
要根据给定的字典键进行查询,请使用该键作为查找名称
>>> 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
查找。键、索引和路径转换也可以与以下内容链接:icontains
、endswith
、iendswith
、iexact
、regex
、iregex
、startswith
、istartswith
、lt
、lte
、gt
和 gte
,以及 包含和键查找。
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>]>
警告
由于任何字符串都可以在 JSON 对象中用作键,因此除了下面列出的查找之外,任何查找都将被解释为键查找。不会引发任何错误。请格外注意输入错误,并始终检查您的查询是否按预期工作。
MariaDB 和 Oracle 用户
在键、索引或路径转换上使用 order_by()
将使用值的字符串表示来对对象进行排序。这是因为 MariaDB 和 Oracle 数据库没有提供将 JSON 值转换为等效 SQL 值的函数。
Oracle 用户
在 Oracle 数据库上,在 exclude()
查询中使用 None
作为查找值将返回在给定路径上没有 null
作为值的那些对象,包括没有该路径的对象。在其他数据库后端,查询将返回具有该路径且值不为 null
的对象。
PostgreSQL 用户
在 PostgreSQL 上,如果只使用一个键或索引,则使用 SQL 运算符 ->
。如果使用多个运算符,则使用 #>
运算符。
SQLite 用户
在 SQLite 上,"true"
、"false"
和 "null"
字符串值将始终分别解释为 True
、False
和 JSON null
。
包含和键查找¶
contains
¶
在 JSONField
上覆盖了 contains
查找。返回的对象是那些在字段的顶层包含给定 dict
的键值对的对象。例如
>>> 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
contained_by
在 Oracle 和 SQLite 上不支持。
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”ed 运算。如果您需要执行更复杂的查询(例如,包含 OR
语句的查询),您可以使用 Q objects
。
Q object
(django.db.models.Q
) 是一个用于封装关键字参数集合的对象。这些关键字参数的指定方式与上面“字段查询”中相同。
例如,此 Q
对象封装了一个单独的 LIKE
查询
from django.db.models import Q
Q(question__startswith="What")
Q
对象可以使用 &
、|
和 ^
运算符组合。当运算符用于两个 Q
对象时,它会生成一个新的 Q
对象。
例如,此语句生成一个单个 Q
对象,它表示两个 "question__startswith"
查询的“或”。
Q(question__startswith="Who") | Q(question__startswith="What")
这等效于以下 SQL WHERE
子句
WHERE question LIKE 'Who%' OR question LIKE 'What%'
您可以通过使用 &
、|
和 ^
运算符组合 Q
对象并使用括号分组来组合任意复杂度的语句。此外,可以使用 ~
运算符对 Q
对象进行否定,从而允许组合查询,这些查询同时包含普通查询和否定 (NOT
) 查询。
Q(question__startswith="Who") | ~Q(pub_date__year=2005)
每个接受关键字参数的查找函数(例如 filter()
、exclude()
、get()
)也可以传递一个或多个 Q
对象作为位置(非命名)参数。如果您向查找函数提供多个 Q
对象参数,这些参数将被“与”在一起。例如
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
的所有成员。
例如,这将删除所有 Entry
对象,其 pub_date
年份为 2005 年
>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})
请记住,这将尽可能地在 SQL 中执行,因此单个对象实例的 delete()
方法在该过程中不一定被调用。如果您在模型类中提供了自定义的 delete()
方法,并且希望确保它被调用,则需要“手动”删除该模型的实例(例如,通过遍历一个 QuerySet
并分别对每个对象调用 delete()
),而不是使用 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()
这种级联行为可以通过 on_delete
参数在 ForeignKey
中进行自定义。
请注意,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
由于继承的工作方式,您必须将 pk
和 id
都设置为 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
对 Author
有一个 ManyToManyField
。复制条目后,您必须为新条目设置多对多关系
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_save
或 post_save
信号(这是调用 save()
的结果),也不会遵守 auto_now
字段选项。如果您想保存 QuerySet
中的每个项目,并确保在每个实例上调用 save()
方法,您不需要任何特殊函数来处理它。遍历它们并调用 save()
for item in my_queryset:
item.save()
对 update 的调用也可以使用 F expressions
来根据模型中另一个字段的值更新一个字段。这对于根据其当前值递增计数器特别有用。例如,要递增博客中每个条目的 pingback 计数
>>> Entry.objects.update(number_of_pingbacks=F("number_of_pingbacks") + 1)
但是,与 filter 和 exclude 子句中的 F()
对象不同,您不能在 update 中使用 F()
对象引入连接 - 您只能引用要更新的模型的本地字段。如果您尝试使用 F()
对象引入连接,则会引发 FieldError
# This will raise a FieldError
>>> Entry.objects.update(headline=F("blog__name"))
回退到原始 SQL¶
如果您发现需要编写一个对 Django 的数据库映射器来说过于复杂的 SQL 查询,您可以回退到手动编写 SQL。Django 提供了几个编写原始 SQL 查询的选项;请参阅 执行原始 SQL 查询。
最后,重要的是要注意,Django 数据库层仅仅是您数据库的接口。您可以通过其他工具、编程语言或数据库框架访问您的数据库;您的数据库没有 Django 特定的内容。