执行查询¶
创建完数据模型后,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()
将其保存到数据库。
假设模型位于 models.py
文件中,该文件位于 Django 应用 blog
内部,以下是一个示例
>>> 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()
返回一个包含数据库中所有 Blog
对象的 QuerySet
。
检索所有对象¶
从表中检索对象的最简单方法是获取所有对象。为此,请在 Manager
上使用 all()
方法
>>> all_entries = Entry.objects.all()
使用过滤器检索特定对象¶
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
,添加一个过滤器,然后是一个排除,然后是另一个过滤器。最终结果是一个包含标题以“What”开头的所有条目,并且发布时间在2005年1月30日至今天之间的QuerySet
。
已过滤的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
通过访问数据库被评估。有关何时进行评估的更多详细信息,请参阅何时评估 QuerySet。
使用get()
检索单个对象¶
filter()
始终会为您提供一个QuerySet
,即使只有一个对象匹配查询——在这种情况下,它将是一个包含单个元素的QuerySet
。
如果您知道只有一个对象匹配您的查询,则可以在Manager
上使用get()
方法,该方法直接返回对象
>>> 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
。要跨越关系,请使用跨模型的相关字段的字段名称,用双下划线分隔,直到到达所需的字段。
此示例检索所有 Blog
的 name
为 'Beatles Blog'
的 Entry
对象
>>> Entry.objects.filter(blog__name="Beatles Blog")
这种跨越可以像您想要的那样深入。
它也可以反向工作。虽然它 可以 被 定制
,但默认情况下,您使用模型的小写名称在查找中引用“反向”关系。
此示例检索所有至少有一个 Entry
的 headline
包含 'Lennon'
的 Blog
对象
>>> 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)
将返回在 author
上具有空 name
的 Blog
对象,以及在 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 表达式
以允许此类比较。F()
的实例充当查询中模型字段的引用。然后,这些引用可以在查询过滤器中用于比较同一模型实例上两个不同字段的值。
例如,要查找评论数超过pingback数的所有博客条目的列表,我们构建一个F()
对象来引用pingback计数,并在查询中使用该F()
对象。
>>> from django.db.models import F
>>> Entry.objects.filter(number_of_comments__gt=F("number_of_pingbacks"))
例如,要查找评论数超过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)
注意
仅打印 Queryset 不会填充缓存。这是因为对__repr__()
的调用仅返回整个 Queryset 的一部分。
异步查询¶
如果您正在编写异步视图或代码,则不能以我们上面描述的方式完全使用 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"):
...
请注意,您也不能执行可能迭代 Queryset 的其他操作,例如在其周围包装list()
以强制其评估(如果需要,您可以在推导式中使用async for
)。
因为QuerySet
方法(如filter()
和exclude()
)实际上不会运行查询——它们设置了在迭代时运行的 Queryset——您可以在异步代码中自由使用它们。有关哪些方法可以像这样继续使用以及哪些方法具有异步版本,请阅读下一节。
QuerySet
和管理器方法¶
管理器和 Queryset 上的一些方法(如get()
和first()
)会强制执行 Queryset 并进行阻塞。有些方法(如filter()
和exclude()
)不会强制执行,因此可以在异步代码中安全地运行。但是您如何区分它们呢?
虽然您可以四处查看是否存在该方法的以a
为前缀的版本(例如,我们有aget()
但没有afilter()
),但有一种更合乎逻辑的方法——在QuerySet 参考中查找它是什么类型的方法。
在那里,您会发现 Queryset 上的方法分为两个部分。
返回新 Queryset 的方法:这些是非阻塞方法,并且没有异步版本。您可以在任何情况下自由使用这些方法,但在使用之前请阅读有关
defer()
和only()
的说明。不返回 Queryset 的方法:这些是阻塞方法,并且具有异步版本——每个异步名称在其文档中都有说明,尽管我们的标准模式是在前面添加
a
前缀。
使用这种区别,您可以确定何时需要使用异步版本,以及何时不需要。例如,这是一个有效的异步查询。
user = await User.objects.filter(username=my_input).afirst()
filter()
返回一个 Queryset,因此在异步环境中继续对其进行链式操作是可以的,而first()
评估并返回一个模型实例——因此,我们更改为afirst()
,并在整个表达式的前面使用await
以便以异步友好的方式调用它。
注意
如果您忘记添加await
部分,您可能会看到类似“协程对象没有属性 x”或“<协程 …>”字符串代替您的模型实例。如果您看到这些,则表示您在某个地方缺少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
作为字段的值存储会将其存储为 SQLNULL
。虽然不推荐,但可以通过使用Value(None, JSONField())
来存储 JSON 标量null
而不是 SQLNULL
。
无论存储哪个值,当从数据库中检索时,JSON 标量null
的 Python 表示形式与 SQLNULL
相同,即None
。因此,很难区分它们。
这仅适用于字段的顶级值为None
。如果None
在list
或dict
内部,它将始终被解释为 JSONnull
。
在查询时,None
值将始终被解释为 JSONnull
。要查询 SQLNULL
,请使用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>]>
除非您确定要使用 SQLNULL
值,否则请考虑设置null=False
并为空值提供合适的默认值,例如default=dict
。
注意
存储 JSON 标量null
不会违反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
查找。键、索引和路径转换也可以与以下内容链接: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
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
)是用于封装关键字参数集合的对象。这些关键字参数的指定方式与上面“字段查找”中的相同。
例如,此 Q
对象封装单个 LIKE
查询
from django.db.models import Q
Q(question__startswith="What")
Q
对象可以使用 &
、|
和 ^
运算符组合。当在两个 Q
对象上使用运算符时,它会生成一个新的 Q
对象。
例如,此语句生成一个表示两个 "question__startswith"
查询的“OR”的单个 Q
对象
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
对象参数,这些参数将被“与”(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
的所有成员。
例如,这将删除所有 Entry
对象,其 pub_date
年份为 2005 年
>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})
请记住,这将尽可能地纯粹以 SQL 执行,因此在过程中不一定调用单个对象实例的 delete()
方法。如果你在模型类上提供了自定义的 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()
此级联行为可以通过 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 表达式
来根据模型中另一个字段的值更新一个字段。这对于根据其当前值递增计数器特别有用。例如,要递增博客中每个条目的 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 特定的内容。