执行查询

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

在本指南中(以及相关参考资料中),我们将引用以下模型,这些模型包含了一个 Webblog 应用:

  1. from django.db import models
  2. class Blog(models.Model):
  3. name = models.CharField(max_length=100)
  4. tagline = models.TextField()
  5. def __str__(self):
  6. return self.name
  7. class Author(models.Model):
  8. name = models.CharField(max_length=200)
  9. email = models.EmailField()
  10. def __str__(self):
  11. return self.name
  12. class Entry(models.Model):
  13. blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
  14. headline = models.CharField(max_length=255)
  15. body_text = models.TextField()
  16. pub_date = models.DateField()
  17. mod_date = models.DateField()
  18. authors = models.ManyToManyField(Author)
  19. number_of_comments = models.IntegerField()
  20. number_of_pingbacks = models.IntegerField()
  21. rating = models.IntegerField()
  22. def __str__(self):
  23. return self.headline

创建对象

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

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

假设模型都位于文件 mysite/blog/models.py 中,这是一个例子:

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

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

save() 方法没有返回值。

参见

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

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

将修改保存至对象

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

有一个已被存入数据库中的 Blog 实例 b5,本例将其改名,并在数据库中更新其记录:

  1. >>> b5.name = 'New name'
  2. >>> b5.save()

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

保存 ForeignKeyManyToManyField 字段

更新 ForeignKey 字段的方式与保存普通字段的方式相同——只需将正确类型的实例分配给相关字段。本例为 Entry 类的实例 entry 更新了 blog 属性,假设 EntryBlog 的实例均已保存在数据库中(因此能在下面检索它们):

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

更新 ManyToManyField 字段有点不同——在字段上使用 add() 方法为关联关系添加一条记录。本例将 Author 实例 joe 添加至 entry 对象:

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

要一次添加多行记录至 ManyToManyField 字段,在一次调用 add() 时传入多个参数,像这样:

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

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

检索对象

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

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

你能通过模型的 Manager 获取 QuerySet。每个模型至少有一个 Manager,默认名称是 objects。像这样直接通过模型类使用它:

  1. >>> Blog.objects
  2. <django.db.models.manager.Manager object at ...>
  3. >>> b = Blog(name='Foo', tagline='Bar')
  4. >>> b.objects
  5. Traceback:
  6. ...
  7. AttributeError: "Manager isn't accessible via Blog instances."

注解

Managers 只能通过模型类访问,而不是通过模型实例,目的是强制分离 “表级” 操作和 “行级” 操作。

Manager 是模型的 QuerySets 主要来源。例如 Blog.objects.all() 返回了一个 QuerySet,后者包含了数据库中所有的 Blog 对象。

检索全部对象

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

  1. >>> 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():

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

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

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

链式过滤器

精炼 QuerySet 的结果本身还是一个 QuerySet,所以能串联精炼过程。例子:

  1. >>> Entry.objects.filter(
  2. ... headline__startswith='What'
  3. ... ).exclude(
  4. ... pub_date__gte=datetime.date.today()
  5. ... ).filter(
  6. ... pub_date__gte=datetime.date(2005, 1, 30)
  7. ... )

这个先获取包含数据库所有条目(entry)的 QuerySet,然后排除一些,再进入另一个过滤器。最终的 QuerySet 包含标题以 “What” 开头的,发布日期介于 2005 年 1 月 30 日与今天之间的所有条目。

每个 QuerySet 都是唯一的

每次精炼一个 QuerySet,你就会获得一个全新的 QuerySet,后者与前者毫无关联。每次精炼都会创建一个单独的、不同的 QuerySet,能被存储,使用和复用。

举例:

  1. >>> q1 = Entry.objects.filter(headline__startswith="What")
  2. >>> q2 = q1.exclude(pub_date__gte=datetime.date.today())
  3. >>> q3 = q1.filter(pub_date__gte=datetime.date.today())

这三个 QuerySets 是独立的。第一个是基础 QuerySet,包含了所有标题以 “What” 开头的条目。第二个是第一个的子集,带有额外条件,排除了 pub_date 是今天和今天之后的所有记录。第三个是第一个的子集,带有额外条件,只筛选 pub_date 是今天或未来的所有记录。最初的 QuerySet (q1) 不受筛选操作影响。

QuerySet 是惰性的

QuerySet 是惰性的 —— 创建 QuerySet 并不会引发任何数据库活动。你可以将一整天的过滤器都堆积在一起,Django 只会在 QuerySet计算 时执行查询操作。来瞄一眼这个例子:

  1. >>> q = Entry.objects.filter(headline__startswith="What")
  2. >>> q = q.filter(pub_date__lte=datetime.date.today())
  3. >>> q = q.exclude(body_text__icontains="food")
  4. >>> print(q)

虽然这看起来像是三次数据库操作,实际上只在最后一行 (print(q)) 做了一次。一般来说, QuerySet 的结果直到你 “要使用” 时才会从数据库中拿出。当你要用时,才通过数据库 计算QuerySet。关于何时才真的执行计算的更多细节,参考 When QuerySets are evaluated

get() 检索单个对象

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

若你知道只会有一个对象满足查询条件,你可以在 Manager 上使用 get() 方法,它会直接返回这个对象:

  1. >>> 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):

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

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

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

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

一般情况下, QuerySet 的切片返回一个新的 QuerySet —— 其并未执行查询。一个特殊情况是使用了的 Python 切片语法的 “步长”。例如,这将会实际的执行查询命令,为了获取从前 10 个对象中,每隔一个抽取的对象组成的列表:

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

由于对 queryset 切片工作方式的模糊性,禁止对其进行进一步的排序或过滤。

要检索 单个 对象而不是一个列表时(例如 SELECT foo FROM bar LIMIT 1),请使用索引,而不是切片。例如,这会返回按标题字母排序后的第一个 Entry:

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

这大致等价于:

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

然而,注意一下,若没有对象满足给定条件,前者会抛出 IndexError,而后者会抛出 DoesNotExist。参考 get() 获取更多细节。

字段查询

字段查询即你如何制定 SQL WHERE 子句。它们以关键字参数的形式传递给 QuerySet 方法 filter()exclude()get()

基本的查询关键字参数遵照 field__lookuptype=value。(有个双下划线)。例如:

  1. >>> Entry.objects.filter(pub_date__lte='2006-01-01')

转换为 SQL 语句大致如下:

  1. SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

这是怎么做到的

Python 能定义可接受任意数量 name-value 参数的函数,参数名和值均在运行时计算。更多信息,请参考官方 Python 教程中的 Keyword Arguments

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

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

若你传入了无效的关键字参数,查询函数会抛出 TypeError

数据库 API 支持两套查询类型;完整参考文档位于 字段查询参考。为了让你了解能干啥,以下是一些常见的查询:

exact

一个 “exact” 匹配的例子:

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

会生成这些 SQL:

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

若你为提供查询类型 —— 也就说,若关键字参数未包含双下划线 —— 查询类型会被指定为 exact

例如,以下两条语句是等价的:

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

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

iexact

不分大小写的匹配,查询语句:

  1. >>> Blog.objects.get(name__iexact="beatles blog")

会匹配标题为 "Beatles Blog""beatles blog", 甚至 "BeAtlES blOG"Blog

contains

大小写敏感的包含测试。例子:

  1. Entry.objects.get(headline__contains='Lennon')

粗略地转为 SQL:

  1. SELECT ... WHERE headline LIKE '%Lennon%';

注意这将匹配标题 'Today Lennon honored',而不是 'today lennon honored'

这也有个大小写不敏感的版本, icontains

startswith, endswith

以……开头和以……结尾的查找。当然也有大小写不敏感的版本,名为 istartswithiendswith

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

跨关系查询

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

本例检索出所有的 Entry 对象,其 Blog``的 ``name'Beatles Blog'

  1. >>> Entry.objects.filter(blog__name='Beatles Blog')

跨域的深度随你所想。

反向操作也能行。要指向一个“反向的”关联关系,需使用模型名的小写。

本例检索的所有 Blog 对象均拥有少一个 标题 含有 'Lennon' 的条目:

  1. >>> Blog.objects.filter(entry__headline__contains='Lennon')

如果你在跨多个关系进行筛选,而某个中间模型的没有满足筛选条件的值,Django 会将它当做一个空的(所有值都是 NULL)但是有效的对象。这样就意味着不会抛出错误。例如,在这个过滤器中:

  1. Blog.objects.filter(entry__authors__name='Lennon')

(假设有个关联的 Author 模型),若某项条目没有任何关联的 author,它会被视作没有关联的 name,而不是因为缺失 author 而抛出错误。大多数情况下,这就是你期望的。唯一可能使你迷惑的场景是在使用 isnull 时。因此:

  1. Blog.objects.filter(entry__authors__name__isnull=True)

将会返回 Blog 对象,包含 authorname 为空的对象,以及那些 entryauthor 为空的对象。若你不想要后面的对象,你可以这样写:

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

跨多值关联

当基于 ManyToManyField 或反向 ForeignKey 筛选某对象时,你可能对两种不同类型的过滤器感兴趣。假设 Blog/Entry 关联关系(BlogEntry 是一种一对多关联关系)。我们可能对那些条目标题同时含有 “Lennon” 且发布于 2008 年的博客感兴趣。或者,我们可能想找到那些条目标题包含 “Lennon” 或发布于 2008 的博客。由于多个 Entry``能同时关联至一个 ``Blog,两种查询都是可行的,且在某些场景下非常有用。

同样的场景也发生在 ManyToManyField。例如,若有个 Entry 拥有一个叫做 tagsManyToManyField,要从关联至 tags 中的条目中找到名为 “music”“bands” 的条目,或要找到某个标签名为 “music” 且状态为 “public” 的条目。

要处理这两种情况,Django 有一套统一的方式处理 filter() 调用。配置给某次 filter() 的所有条件会在调用时同时生效,筛选出满足条件的项目。连续的 filter() 调用进一步限制了对象结果集,但对于多值关联来说,限制条件作用于链接至主模型的对象,而不一定是那些被前置 filter() 调用筛选的对象。

这听起来可能有点迷糊,所以需要一个例子来解释一下。要筛选出所有关联条目同时满足标题含有 “Lennon” 且发布于 2008 (同一个条目,同时满足两个条件)年的博客,我们会这样写:

  1. Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

要筛选所有条目标题包含 “Lennon” 或条目发布于 2008 年的博客,我们会这样写:

  1. Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

假设只有一个博客,拥有的条目同时满足标题含有 “Lennon” 且发布于 2008 年,但是发布于 2008 年的条目的标题均不含有 “Lennon”。第一项查询不会返回任何博客,而第二项查询会返回此博客。

在第二个例子中,第一个过滤器限制结果集为那些关联了标题包含 “Lennon” 的条目的博客。第二个过滤器进一步要求结果集中的博客要发布于 2008 年。第二个过滤器筛选的条目与第一个过滤器筛选的可能不尽相同。我们是用过滤器语句筛选 Blog,而不是 Entry

注解

filter() 的查询行为会跨越多值关联,就像前文说的那样,并不与 exclude() 相同。相反,一次 exclude() 调用的条件并不需要指向同一项目。

例如,以下查询会排除那些关联条目标题包含 “Lennon” 且发布于 2008 年的博客:

  1. Blog.objects.exclude(
  2. entry__headline__contains='Lennon',
  3. entry__pub_date__year=2008,
  4. )

但是,与 filter() 的行为不同,其并不会限制博客同时满足这两种条件。要这么做的话,也就是筛选出所有条目标题不带 “Lennon” 且发布年不是 2008 的博客,你需要做两次查询:

  1. Blog.objects.exclude(
  2. entry__in=Entry.objects.filter(
  3. headline__contains='Lennon',
  4. pub_date__year=2008,
  5. ),
  6. )

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

在之前的例子中,我们已经构建过的 filter 都是将模型字段值与常量做比较。但是,要怎么做才能将模型字段值与同一模型中的另一字段做比较呢?

Django 提供了 F 表达式 实现这种比较。 F() 的实例充当查询中的模型字段的引用。这些引用可在查询过滤器中用于在同一模型实例中比较两个不同的字段。

例如,要查出所有评论数大于 pingbacks 的博客条目,我们构建了一个 F() 对象,指代 pingback 的数量,然后在查询中使用该 F() 对象:

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

Django 支持对 F() 对象进行加、减、乘、除、求余和次方,另一操作数既可以是常量,也可以是其它 F() 对象。要找到那些评论数两倍于 pingbacks 的博客条目,我们这样修改查询条件:

  1. >>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks') * 2)

要找出所有评分低于 pingback 和评论总数之和的条目,修改查询条件:

  1. >>> Entry.objects.filter(rating__lt=F('number_of_comments') + F('number_of_pingbacks'))

你也能用双下划线在 F() 对象中通过关联关系查询。带有双下划线的 F() 对象将引入访问关联对象所需的任何连接。例如,要检索出所有作者名与博客名相同的博客,这样修改查询条件:

  1. >>> Entry.objects.filter(authors__name=F('blog__name'))

对于 date 和 date/time 字段,你可以加上或减去一个 timedelta 对象。以下会返回所有发布 3 天后被修改的条目:

  1. >>> from datetime import timedelta
  2. >>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

F() 对象通过 .bitand().bitor().bitrightshift().bitleftshift() 支持位操作。例子:

  1. >>> F('somefield').bitand(16)

主键 (pk) 查询快捷方式

出于方便的目的,Django 提供了一种 pk 查询快捷方式, pk 表示主键 “primary key”。

示例 Blog 模型中,主键是 id 字段,所以这 3 个语句是等效的:

  1. >>> Blog.objects.get(id__exact=14) # Explicit form
  2. >>> Blog.objects.get(id=14) # __exact is implied
  3. >>> Blog.objects.get(pk=14) # pk implies id__exact

pk 的使用并不仅限于 __exact 查询——任何的查询项都能接在 pk 后面,执行对模型主键的查询:

  1. # Get blogs entries with id 1, 4 and 7
  2. >>> Blog.objects.filter(pk__in=[1,4,7])
  3. # Get all blog entries with id > 14
  4. >>> Blog.objects.filter(pk__gt=14)

pk 查找也支持跨连接。例如,以下 3 个语句是等效的:

  1. >>> Entry.objects.filter(blog__id__exact=3) # Explicit form
  2. >>> Entry.objects.filter(blog__id=3) # __exact is implied
  3. >>> Entry.objects.filter(blog__pk=3) # __pk implies __id__exact

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

等效于 LIKE SQL 语句的字段查询子句 (iexactcontainsicontainsstartswithistartswithendswithiendswith) 会将 LIKE 语句中有特殊用途的两个符号,即百分号和下划线自动转义。(在 LIKE 语句中,百分号匹配多个任意字符,而下划线匹配一个任意字符。)

这意味着事情应该直观地工作,这样抽象就不会泄露。例如,要检索所有包含百分号的条目,就像对待其它字符一样使用百分号:

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

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

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

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

缓存和 QuerySet

每个 QuerySet 都带有缓存,尽量减少数据库访问。理解它是如何工作的能让你编写更高效的代码。

新创建的 QuerySet 缓存是空的。一旦要计算 QuerySet 的值,就会执行数据查询,随后,Django 就会将查询结果保存在 QuerySet 的缓存中,并返回这些显式请求的缓存(例如,下一个元素,若 QuerySet 正在被迭代)。后续针对 QuerySet 的计算会复用缓存结果。

牢记这种缓存行为,在你错误使用 QuerySet 时可能会被它咬一下。例如,以下会创建两个 QuerySet,计算它们,丢掉它们:

  1. >>> print([e.headline for e in Entry.objects.all()])
  2. >>> print([e.pub_date for e in Entry.objects.all()])

这意味着同样的数据库查询会被执行两次,实际加倍了数据库负载。同时,有可能这两个列表不包含同样的记录,因为在两次请求间,可能有 Entry 被添加或删除了。

要避免此问题,保存 QuerySet 并复用它:

  1. >>> queryset = Entry.objects.all()
  2. >>> print([p.headline for p in queryset]) # Evaluate the query set.
  3. >>> print([p.pub_date for p in queryset]) # Re-use the cache from the evaluation.

QuerySet 未被缓存时

查询结果集并不总是缓存结果。当仅计算查询结果集的 部分 时,会校验缓存,若没有填充缓存,则后续查询返回的项目不会被缓存。特别地说,这意味着使用数组切片或索引的 限制查询结果集 不会填充缓存。

例如,重复的从某个查询结果集对象中取指定索引的对象会每次都查询数据库:

  1. >>> queryset = Entry.objects.all()
  2. >>> print(queryset[5]) # Queries the database
  3. >>> print(queryset[5]) # Queries the database again

不过,若全部查询结果集已被检出,就会去检查缓存:

  1. >>> queryset = Entry.objects.all()
  2. >>> [entry for entry in queryset] # Queries the database
  3. >>> print(queryset[5]) # Uses cache
  4. >>> print(queryset[5]) # Uses cache

以下展示一些例子,这些动作会触发计算全部的查询结果集,并填充缓存的过程:

  1. >>> [entry for entry in queryset]
  2. >>> bool(queryset)
  3. >>> entry in queryset
  4. >>> list(queryset)

注解

只是打印查询结果集不会填充缓存。因为调用 __repr__() 仅返回了完整结果集的一个切片。

通过 Q 对象完成复杂查询

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

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

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

  1. from django.db.models import Q
  2. Q(question__startswith='What')

Q 对象能通过 &| 操作符连接起来。当操作符被用于两个 Q 对象之间时会生成一个新的 Q 对象。

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

  1. Q(question__startswith='Who') | Q(question__startswith='What')

这等价于以下 SQL WHERE 字句:

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

你能通过 &| 操作符和括号分组,组合任意复杂度的语句。当然, Q 对象也可通过 ~ 操作符反转,允许在组合查询中组合普通查询或反向 (NOT) 查询:

  1. Q(question__startswith='Who') | ~Q(pub_date__year=2005)

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

  1. Poll.objects.get(
  2. Q(question__startswith='Who'),
  3. Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
  4. )

…粗略地转为 SQL:

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

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

  1. Poll.objects.get(
  2. Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
  3. question__startswith='Who',
  4. )

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

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

……却是无效的。

参见

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

比较对象

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

使用前文的 Entry,以下的两个语句是等效的:

  1. >>> some_entry == other_entry
  2. >>> some_entry.id == other_entry.id

若模型主键名不是 id,没问题。比较时总会使用主键,不管它叫啥。例如,若模型的主键字段名为 name,以下两个语句是等效的:

  1. >>> some_obj == other_obj
  2. >>> some_obj.name == other_obj.name

删除对象

通常,删除方法被命名为 delete()。该方法立刻删除对象,并返回被删除的对象数量和一个包含了每个被删除对象类型的数量的字典。例子:

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

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

例如,这会删除 2005 发布的所有 Entry 对象:

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

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

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

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

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

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

  1. Entry.objects.all().delete()

复制模型实例

虽然没有用于拷贝模型实例的内置方法,但仍能很简单的拷贝所有字段值创建新实例。最简单的例子,你可以将 pk 设为 None。使用博客示例:

  1. blog = Blog(name='My blog', tagline='Blogging is easy')
  2. blog.save() # blog.pk == 1
  3. blog.pk = None
  4. blog.save() # blog.pk == 2

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

  1. class ThemeBlog(Blog):
  2. theme = models.CharField(max_length=200)
  3. django_blog = ThemeBlog(name='Django', tagline='Django is easy', theme='python')
  4. django_blog.save() # django_blog.pk == 3

根据继承的原理,你必须将 pkid 都设置为 None:

  1. django_blog.pk = None
  2. django_blog.id = None
  3. django_blog.save() # django_blog.pk == 4

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

  1. entry = Entry.objects.all()[0] # some previous entry
  2. old_authors = entry.authors.all()
  3. entry.pk = None
  4. entry.save()
  5. entry.authors.set(old_authors)

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

  1. detail = EntryDetail.objects.all()[0]
  2. detail.pk = None
  3. detail.entry = entry
  4. detail.save()

一次修改多个对象

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

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

你仅能用此方法设置非关联字段和 ForeignKey 字段。要修改非关联字段,需要用常量提供新值。要修改 ForeignKey 字段,将新值置为目标模型的新实例。例子:

  1. >>> b = Blog.objects.get(pk=1)
  2. # Change every Entry so that it belongs to this Blog.
  3. >>> Entry.objects.all().update(blog=b)

方法 update() 立刻被运行,并返回匹配查询调节的行数(若某些行早已是新值,则可能不等于实际匹配的行数)。更新 QuerySet 的唯一限制即它只能操作一个数据表:该模型的主表。你可以基于关联字段进行筛选,但你只能更新模型主表中的列。例子:

  1. >>> b = Blog.objects.get(pk=1)
  2. # Update all the headlines belonging to this Blog.
  3. >>> Entry.objects.select_related().filter(blog=b).update(headline='Everything is the same')

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

  1. for item in my_queryset:
  2. item.save()

调用更新方法时也能使用 F 表达式 基于同一模型另一个字段的值更新某个字段。这在基于计数器的当前值增加其值时特别有用。例如,要增加针对博客中每项条目的 pingback 技术:

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

然而,与过滤器中的 F() 对象和排除字句不同,你不能在更新方法中使用 F() 对象的同时使用 join——你只能引用被更新模型的内部字段。若你试着在使用 join 字句时使用 F() 对象,会抛出一个 FieldError():

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

关联对象

当你在模型中定义了关联关系(如 ForeignKeyOneToOneField, 或 ManyToManyField),该模型的实例将会自动获取一套 API,能快捷地访问关联对象。

拿本文开始的模型做例子,一个 Entry 对象 e 通过 blog 属性获取其关联的 Blog 对象: e.blog

(在幕后,这个函数是由 Python descriptors 实现的。这玩意一般不会麻烦你,但是我们为你指出了注意点。)

Django 也提供了从关联关系 另一边 访问的 API —— 从被关联模型到定义关联关系的模型的连接。例如,一个 Blog 对象 b 能通过 entry_set 属性 b.entry_set.all() 访问包含所有关联 Entry 对象的列表。

本章节中的所有例子都是用了本页开头定义的 BlogAuthorEntry 模型。

一对多关联

正向访问

若模型有个 ForeignKey,该模型的实例能通过其属性访问关联(外部的)对象。

举例:

  1. >>> e = Entry.objects.get(id=2)
  2. >>> e.blog # Returns the related Blog object.

你可以通过 foreign-key 属性获取和设置值。如你所想,对外键的修改直到你调用 save() 后才会被存入数据库。例子:

  1. >>> e = Entry.objects.get(id=2)
  2. >>> e.blog = some_blog
  3. >>> e.save()

ForeignKey 字段配置了 null=True (即其允许 NULL 值),你可以指定值为 None 移除关联。例子:

  1. >>> e = Entry.objects.get(id=2)
  2. >>> e.blog = None
  3. >>> e.save() # "UPDATE blog_entry SET blog_id = NULL ...;"

首次通过正向一对多关联访问关联对象时会缓存关联关系。后续在同一对象上通过外键的访问也会被缓存。例子:

  1. >>> e = Entry.objects.get(id=2)
  2. >>> print(e.blog) # Hits the database to retrieve the associated Blog.
  3. >>> print(e.blog) # Doesn't hit the database; uses cached version.

注意 select_related() QuerySet 方法会预先用所有一对多关联对象填充缓存。例子:

  1. >>> e = Entry.objects.select_related().get(id=2)
  2. >>> print(e.blog) # Doesn't hit the database; uses cached version.
  3. >>> print(e.blog) # Doesn't hit the database; uses cached version.

“反向” 关联

若模型有 ForeignKey,外键关联的模型实例将能访问 Manager,后者会返回第一个模型的所有实例。默认情况下,该 Manager 名为 FOO_setFOO 即源模型名的小写形式。 Manager 返回 QuerySets,后者能以 “检索对象” 章节介绍的方式进行筛选和操作。

举例:

  1. >>> b = Blog.objects.get(id=1)
  2. >>> b.entry_set.all() # Returns all Entry objects related to Blog.
  3. # b.entry_set is a Manager that returns QuerySets.
  4. >>> b.entry_set.filter(headline__contains='Lennon')
  5. >>> b.entry_set.count()

你可以在定义 ForeignKey 时设置 related_name 参数重写这个 FOO_set 名。例如,若修改 Entry 模型为 blog = ForeignKey(Blog, on_delete=models.CASCADE, related_name='entries'),前文示例代码会看起来像这样:

  1. >>> b = Blog.objects.get(id=1)
  2. >>> b.entries.all() # Returns all Entry objects related to Blog.
  3. # b.entries is a Manager that returns QuerySets.
  4. >>> b.entries.filter(headline__contains='Lennon')
  5. >>> b.entries.count()

使用自定义反向管理器

RelatedManager 反向关联的默认实现是该模型 默认管理器 一个实例。若你想为某个查询指定一个不同的管理器,可以使用如下语法:

  1. from django.db import models
  2. class Entry(models.Model):
  3. #...
  4. objects = models.Manager() # Default Manager
  5. entries = EntryManager() # Custom Manager
  6. b = Blog.objects.get(id=1)
  7. b.entry_set(manager='entries').all()

EntryManager 在其 get_queryset() 方法执行了默认过滤行为,改行为会应用到 all() 调用中。

当然,指定一个自定义反向管理也允许你调用模型自定义方法:

  1. b.entry_set(manager='entries').is_published()

管理关联对象的额外方法

ForeignKey Manager 还有方法能处理关联对象集合。除了上面的 “检索对象” 中定义的 QuerySet 方法以外,以下是每项的简要介绍,而完整的细节能在 关联对象参考 中找到。

add(obj1, obj2, ...)

将特定的模型对象加入关联对象集合。

create(**kwargs)

创建一个新对象,保存,并将其放入关联对象集合中。返回新创建的对象。

remove(obj1, obj2, ...)

从关联对象集合删除指定模型对象。

clear()

从关联对象集合删除所有对象。

set(objs)

替换关联对象集合

要指定关联集合的成员,调用 set() 方法,并传入可迭代的对象实例集合。例如,若 e1e2 都是 Entry 实例:

  1. b = Blog.objects.get(id=1)
  2. b.entry_set.set([e1, e2])

若能使用 clear() 方法, entry_set 中所有旧对象会在将可迭代集合(本例中是个列表)中的对象加入其中之前被删除。若 不能 使用 clear() 方法,添加新对象时不会删除旧对象。

本节介绍的所有 “反向” 操作对数据库都是立刻生效的。每次的增加,创建和删除都是及时自动地保存至数据库。

多对多关联

多对多关联的两端均自动获取访问另一端的 API。该 API 的工作方式类似上面的 “反向” 一对多关联。

不同点在为属性命名上:定义了 ManyToManyField 的模型使用字段名作为属性名,而 “反向” 模型使用源模型名的小写形式,加上 '_set' (就像反向一对多关联一样)。

一个更易理解的例子:

  1. e = Entry.objects.get(id=3)
  2. e.authors.all() # Returns all Author objects for this Entry.
  3. e.authors.count()
  4. e.authors.filter(name__contains='John')
  5. a = Author.objects.get(id=5)
  6. 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() 方法能接收主键值。例如,若 ee2Entry 的实例,以下两种 set() 调用结果一致:

  1. a = Author.objects.get(id=5)
  2. a.entry_set.set([e1, e2])
  3. a.entry_set.set([e1.pk, e2.pk])

一对一关联

一对一关联与多对一关联非常类似。若在模型中定义了 OneToOneField,该模型的实例只需通过其属性就能访问关联对象。

例如:

  1. class EntryDetail(models.Model):
  2. entry = models.OneToOneField(Entry, on_delete=models.CASCADE)
  3. details = models.TextField()
  4. ed = EntryDetail.objects.get(id=2)
  5. ed.entry # Returns the related Entry object.

不同点在于 “反向” 查询。一对一关联所关联的对象也能访问 Manager 对象,但这个 Manager 仅代表一个对象,而不是对象的集合:

  1. e = Entry.objects.get(id=2)
  2. e.entrydetail # returns the related EntryDetail object

若未为关联关系指定对象,Django 会抛出 DoesNotExist 异常。

实例能通过为正向关联指定关联对象一样的方式指定给反向关联:

  1. e.entrydetail = ed

反向关联是如何实现的?

其它对象关联映射实现要求你在两边都定义关联关系。而 Django 开发者坚信这违反了 DRY 原则(不要自我重复),故 Django 仅要求你在一端定义关联关系。

但这是如何实现的呢,给你一个模型类,模型类并不知道是否有其它模型类关联它,直到其它模型类被加载?

答案位于 应用注册。 Django 启动时,它会导入 INSTALLED_APPS 列出的每个应用,和每个应用中的 model 模块。无论何时创建了一个新模型类,Django 为每个关联模型添加反向关联。若被关联的模型未被导入,Django 会持续追踪这些关联,并在关联模型被导入时添加关联关系。

出于这个原因,包含你所使用的所有模型的应用必须列在 INSTALLED_APPS 中。否则,反向关联可能不会正常工作。

查询关联对象

涉及关联对象的查询与涉及普通字段的查询遵守同样的规则。未查询条件指定值时,你可以使用对象实例,或该实例的主键。

例如,若有个博客对象 b,其 id=5,以下三种查询是一样的:

  1. Entry.objects.filter(blog=b) # Query using object instance
  2. Entry.objects.filter(blog=b.id) # Query using id from instance
  3. Entry.objects.filter(blog=5) # Query using id directly

回归原生 SQL

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

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