执行查询(一)
一旦创建 数据模型 后,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
一个 “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。
同样,这只介绍了皮毛。完整的参考能在 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 操作。