执行查询
一旦创建 数据模型 后,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() 后才操作数据库。
保存 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 代表来自数据库中对象的一个集合。它可以有 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,包含的对象 不 满足给定查询参数。
查询参数(**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 的 LIMIT
和 OFFSET
子句。
例如,这返回前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” 匹配。例如:
>>> 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
查询是最常见的。
不区分大小写的匹配。因此,查询:
>>> Blog.objects.get(name__iexact="beatles blog")
会匹配标题为 "Beatles Blog"
, "beatles blog"
, 甚至 "BeAtlES blOG"
的 Blog
。
大小写敏感的包含测试。例子:
Entry.objects.get(headline__contains="Lennon")
粗略地转为 SQL:
SELECT ... WHERE headline LIKE '%Lennon%';
注意这将匹配标题 'Today Lennon honored'
,而不是 'today lennon honored'
。
这也有个大小写不敏感的版本, icontains。
以……开头和以……结尾的查找。当然也有大小写不敏感的版本,名为 istartswith 和 iendswith。
同样,这只介绍了皮毛。完整的参考能在 field 查询参考 找到。
跨关系查询
Django 提供了一种强大而直观的方式来“追踪”查询中的关系,在幕后自动为你处理 SQL JOIN
关系。为了跨越关系,跨模型使用关联字段名,字段名由双下划线分割,直到拿到想要的字段。
这个示例检索所有具有 name
为 'Beatles Blog'
的 Blog
的 Entry
对象:
>>> Entry.objects.filter(blog__name="Beatles Blog")
跨域的深度随你所想。
它也可以反向工作。虽然它 可以自定义,默认情况下,你在查找中使用模型的小写名称来引用一个 “反向” 关系。
这个示例检索所有至少有一个 headline
包含 'Lennon'
的 Entry
的 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)
将会返回 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()
的实例充当查询中的模型字段的引用。这些引用可在查询过滤器中用于在同一模型实例中比较两个不同的字段。
例如,要找到所有具有比 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 语句的字段查询子句 (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
未被缓存时
查询结果集并不总是缓存结果。当仅计算查询结果集的 部分 时,会校验缓存,若没有填充缓存,则后续查询返回的项目不会被缓存。特别地说,这意味着使用数组切片或索引的 限制查询结果集 不会填充缓存。
例如,反复获取查询集对象中的某个索引会每次查询数据库:
>>> 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
被保存在 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
值不违反 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,索引和路径转换也可以用: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>]>
备注
由于键-路径查询的工作方式,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"
将始终被解释为分别为 True
、False
和 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()
这种约束行为由 ForeignKey 的 on_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
由于继承的工作方式,您必须将 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)
然而,与筛选和排除子句中的 F()
对象不同,在更新中使用 F()
对象时,不能引入连接,只能引用模型被更新的字段。如果尝试使用 F()
对象引入连接,将引发 FieldError
错误:
# This will raise a FieldError
>>> Entry.objects.update(headline=F("blog__name"))
关联对象
当你在模型中定义了关联关系(如 ForeignKey, OneToOneField, 或 ManyToManyField),该模型的实例将会自动获取一套 API,能快捷地访问关联对象。
拿本文开始的模型做例子,一个 Entry
对象 e
通过 blog
属性获取其关联的 Blog
对象: e.blog
。
(在幕后,这个函数是由 Python descriptors 实现的。这玩意一般不会麻烦你,但是我们为你指出了注意点。)
Django 也提供了从关联关系 另一边 访问的 API —— 从被关联模型到定义关联关系的模型的连接。例如,一个 Blog
对象 b
能通过 entry_set
属性 b.entry_set.all()
访问包含所有关联 Entry
对象的列表。
本章节中的所有例子都是用了本页开头定义的 Blog
, Author
和 Entry
模型。
一对多关联
正向访问
若模型有个 ForeignKey,该模型的实例能通过其属性访问关联(外部的)对象。
例如:
>>> e = Entry.objects.get(id=2)
>>> e.blog # Returns the related Blog object.
你可以通过外键属性进行获取和设置。正如你可能期望的那样,对外键的更改直到调用 save() 方法后才会保存到数据库。示例:
>>> e = Entry.objects.get(id=2)
>>> e.blog = some_blog
>>> e.save()
如果一个 ForeignKey 字段设置了 null=True
(即允许 NULL
值),你可以将 None
赋值给它以删除关联。示例:
>>> e = Entry.objects.get(id=2)
>>> e.blog = None
>>> e.save() # "UPDATE blog_entry SET blog_id = NULL ...;"
第一次访问与一对多关系的前向访问时,相关对象会被缓存。对同一对象实例上的外键的后续访问也会被缓存。示例:
>>> e = Entry.objects.get(id=2)
>>> print(e.blog) # Hits the database to retrieve the associated Blog.
>>> print(e.blog) # Doesn't hit the database; uses cached version.
请注意,select_related() QuerySet 方法会提前递归地预加载所有一对多关系的缓存。示例:
>>> e = Entry.objects.select_related().get(id=2)
>>> print(e.blog) # Doesn't hit the database; uses cached version.
>>> print(e.blog) # Doesn't hit the database; uses cached version.
“反向” 关联
若模型有 ForeignKey,外键关联的模型实例将能访问 Manager,后者会返回第一个模型的所有实例。默认情况下,该 Manager 名为 FOO_set
, FOO
即源模型名的小写形式。 Manager 返回 QuerySets
,后者能以 “检索对象” 章节介绍的方式进行筛选和操作。
例如:
>>> b = Blog.objects.get(id=1)
>>> b.entry_set.all() # Returns all Entry objects related to Blog.
# b.entry_set is a Manager that returns QuerySets.
>>> b.entry_set.filter(headline__contains="Lennon")
>>> b.entry_set.count()
你可以通过在 ForeignKey 定义中设置 related_name 参数来覆盖 FOO_set
的名称。例如,如果将 Entry
模型更改为 blog = ForeignKey(Blog, on_delete=models.CASCADE, related_name='entries')
,那么上面的示例代码将如下所示:
>>> b = Blog.objects.get(id=1)
>>> b.entries.all() # Returns all Entry objects related to Blog.
# b.entries is a Manager that returns QuerySets.
>>> b.entries.filter(headline__contains="Lennon")
>>> b.entries.count()
使用自定义反向管理器
RelatedManager 反向关联的默认实现是该模型 默认管理器 一个实例。若你想为某个查询指定一个不同的管理器,可以使用如下语法:
from django.db import models
class Entry(models.Model):
# ...
objects = models.Manager() # Default Manager
entries = EntryManager() # Custom Manager
b = Blog.objects.get(id=1)
b.entry_set(manager="entries").all()
若 EntryManager
在其 get_queryset()
方法执行了默认过滤行为,改行为会应用到 all()
调用中。
指定一个自定义反向管理也允许你调用模型自定义方法:
b.entry_set(manager="entries").is_published()
与预获取的互动
在使用反向关系调用 prefetch_related() 时,将使用默认管理器。如果要使用自定义反向管理器预获取相关对象,请使用 Prefetch()。例如:
from django.db.models import Prefetch
prefetch_manager = Prefetch("entry_set", queryset=Entry.entries.all())
Blog.objects.prefetch_related(prefetch_manager)
管理关联对象的额外方法
ForeignKey Manager 还有方法能处理关联对象集合。除了上面的 “检索对象” 中定义的 QuerySet 方法以外,以下是每项的简要介绍,而完整的细节能在 关联对象参考 中找到。
add(obj1, obj2, ...)
将特定的模型对象加入关联对象集合。
create(**kwargs)
创建一个新对象,保存,并将其放入关联对象集合中。返回新创建的对象。
remove(obj1, obj2, ...)
从关联对象集合删除指定模型对象。
clear()
从关联对象集合删除所有对象。
set(objs)
替换关联对象集合
要指定关联集合的成员,调用 set()
方法,并传入可迭代的对象实例集合。例如,若 e1
和 e2
都是 Entry
实例:
b = Blog.objects.get(id=1)
b.entry_set.set([e1, e2])
如果存在 clear()
方法,则在将可迭代对象(在这种情况下是列表)中的所有对象添加到集合之前,将从 entry_set
中删除任何现有对象。如果 没有 可用的 clear()
方法,则将添加可迭代对象中的所有对象,而不会删除任何现有元素。
本节介绍的所有 “反向” 操作对数据库都是立刻生效的。每次的增加,创建和删除都是及时自动地保存至数据库。
多对多关联
多对多关联的两端均自动获取访问另一端的 API。该 API 的工作方式类似上面的 “反向” 一对多关联。
不同点在为属性命名上:定义了 ManyToManyField 的模型使用字段名作为属性名,而 “反向” 模型使用源模型名的小写形式,加上 '_set'
(就像反向一对多关联一样)。
一个更易理解的例子:
e = Entry.objects.get(id=3)
e.authors.all() # Returns all Author objects for this Entry.
e.authors.count()
e.authors.filter(name__contains="John")
a = Author.objects.get(id=5)
a.entry_set.all() # Returns all Entry objects for this Author.
和 ForeignKey 一样, ManyToManyField 能指定 related_name。在上面的例子中,若 Entry
中的 ManyToManyField 已指定了 related_name='entries'
,随后每个 Author
实例会拥有一个 entries
属性,而不是 entry_set
。
另一个与一对多关联不同的地方是,除了模型实例以外,多对多关联中的 add()
, set()
和 remove()
方法能接收主键值。例如,若 e
和 e2
是 Entry
的实例,以下两种 set()
调用结果一致:
a = Author.objects.get(id=5)
a.entry_set.set([e1, e2])
a.entry_set.set([e1.pk, e2.pk])
一对一关联
一对一关联与多对一关联非常类似。若在模型中定义了 OneToOneField,该模型的实例只需通过其属性就能访问关联对象。
例如:
class EntryDetail(models.Model):
entry = models.OneToOneField(Entry, on_delete=models.CASCADE)
details = models.TextField()
ed = EntryDetail.objects.get(id=2)
ed.entry # Returns the related Entry object.
不同点在于 “反向” 查询。一对一关联所关联的对象也能访问 Manager 对象,但这个 Manager 仅代表一个对象,而不是对象的集合:
e = Entry.objects.get(id=2)
e.entrydetail # returns the related EntryDetail object
若未为关联关系指定对象,Django 会抛出 DoesNotExist
异常。
实例能通过为正向关联指定关联对象一样的方式指定给反向关联:
e.entrydetail = ed
反向关联是如何实现的?
其它对象关联映射实现要求你在两边都定义关联关系。而 Django 开发者坚信这违反了 DRY 原则(不要自我重复),故 Django 仅要求你在一端定义关联关系。
但这是如何实现的呢,给你一个模型类,模型类并不知道是否有其它模型类关联它,直到其它模型类被加载?
答案位于 应用注册。 Django 启动时,它会导入 INSTALLED_APPS 列出的每个应用,和每个应用中的 model
模块。无论何时创建了一个新模型类,Django 为每个关联模型添加反向关联。若被关联的模型未被导入,Django 会持续追踪这些关联,并在关联模型被导入时添加关联关系。
出于这个原因,包含你所使用的所有模型的应用必须列在 INSTALLED_APPS 中。否则,反向关联可能不会正常工作。
查询关联对象
涉及关联对象的查询与涉及普通字段的查询遵守同样的规则。未查询条件指定值时,你可以使用对象实例,或该实例的主键。
例如,若有个博客对象 b
,其 id=5
,以下三种查询是一样的:
Entry.objects.filter(blog=b) # Query using object instance
Entry.objects.filter(blog=b.id) # Query using id from instance
Entry.objects.filter(blog=5) # Query using id directly
回归原生 SQL
若你发现需要编写的 SQL 查询语句太过复杂,以至于 Django 的数据库映射无法处理,你可以回归手动编写 SQL。Django 针对编写原生 SQL 有几个选项;参考 执行原生 SQL 查询。
最后,Django 数据库层只是一种访问数据库的接口,理解这点非常重要。你也可以通过其它工具,编程语言或数据库框架访问数据库;Django 并没有对数据库数据库做啥独有的操作。