常用 Django QuerySet Api

本文将介绍常用的 QuerySet Api, 同上一篇Django QuerySet 方法梳理类似, 是对 Django 文档 QuerySet API reference 中常用或比较重要 API 的翻译。

Model 对象

本文使用的 model 对象, 与Django QuerySet 方法梳理 类似, 只是额外为 Author, Blog 添加了关联 Model Category, City, Tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from django.db import models

class Category(models.Model):
name = models.TextField(max_length=100)

class Tag(models.Model):
name = models.TextField(max_length=20)

class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
cate = models.Foreignkey(Category)
tag = models.ManyToManyField(Tag)

class City(models.Model):
name = models.CharField(max_length=20)

class Author(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField()
hometown = models.Foreignkey(City, on_delete=models.SET_NULL)
visition = models.ManyToManyField(City)

class Entry(models.Model):
blog = models.ForeignKey(Blog)
headline = models.CharField(max_length=255) //外键
body_text = models.TextField()
pub_date = models.DateField()
mod_date = models.DateField()
authors = models.ManyToManyField(Author) // 多对多
n_comments = models.IntegerField()

select_related 在执行数据库查询时, 会将外建字段关联的表一起查出来, 在 SQL 中通过 INNER JOIN 来实现。通过这种方式查询后,在使用外键字段时,就不会再执行一次数据库操作。例如:
1、不使用 select_related
下例中: 在使用 Entry 对象 e 的外键属性 blog 时, 会执行一次数据库操作,查询表 blog。 加上 entry 表的查找, 完成如下操作要进行两次的数据库检索。

1
2
3
4
e = Entry.objects.filter(id=5)
if e.exists(): # 第一次检索: 查询表 entry
e = e[0]
b = e.blog # 第二次检索: 查询表 blog

2、使用 select_related
使用 select_related 方法, 可以通过一次数据检索,完成上例中的操作。select_realted 可以将需要用到关联表通过 INNER JOIN 的方式将需要的数据一次性加载。

1
2
3
4
e = Entry.objects.filter(id=5).select_related('blog')
if e.exists(): # 第一次检索: 将 entry 和 blog 表中相关的数据都取出
e = e[0]
b = e.blog # 不再检索数据库

filter() 方法和 select_related() 两个方法串联时无所谓先后顺序, 即: e = Entry.objects.select_related(‘blog’).filter(id=5) 效果同 Entry.objects.filter(id=5).select_related(‘blog’) 是一样的。

3、select_related 同时查询多张表
当一个 model 中同时包含多个外键字段时, 通过 select_related 一次性将他们都检索出来。假设 model Person 包含外键字段 city, school。
在查询 person 时 需要将其 city, school 信息一次性加载出来, 可以在 select_related 同时传入字段名, ‘city’, ‘school’, 以逗号隔开, 或者采用链式结构书写, 两种方法效果一致

1
2
3
p = Person.objects.filter(id=5).select_related('city', 'school')
# 或者
p = Person.objects.filter(id=5).select_related('city').select_related('school')

4、select_related 外键级联
使用下例说明:

1
2
3
4
5
6
7
8
9
10
11
e1 = Entry.objects.filter(id=5).select_related('blog__cate')
if e1.exists(): # 检索数据库
e1 = e1[0]
b1 = e1.blog # 不检索数据库
c1 = b1.cate # 不检索数据

e2 = Entry.objects.filter(id=5)
if e2.exists(): # 检索数据库
e2 = e2[0]
b2 = e2.blog # 检索数据库
c2 = b2.cate # 检索数据库

select_related 可以进行多张表的级联查询, 从上例中看出, select_related(‘blog_cate’) 涉及了表 entry, 表 blog, 表 category 的查询, 通过一次检索将所有外键相关联的数据取出。 blog 同双下划线 __ 与其外键字段(cate)关联。

5、select_related 不带参数
当需要查询许多个关联字段, 或不清楚待查询 Model 的关联关系时, 可以执行不带参数的 select_related() 方法。 其会将所有非空的 Foreignkey 字段对应表的相关数据一次性都检索出来。这样做会造成不必要的性能浪费。

6、清除 select_related 关联的外键数据
如果需要清除 select_related 查询关联的其他表的数据, 可以给 select_related 传入参数 None 来实现

1
2
3
4
5
e1 = Entry.objects.filter(id=5).select_related('blog')
e1 = e1.select_related(None)
if e1.exists(): # 检索数据库
e1 = e1[0]
b1 = e1.blog # 检索数据库

注意: 为了避免返回结果集过大, select_related 被限制于 ForeignKey 和 OneToOneField 这两种关联关系使用。
深入理解
请参考博客 实例详解Django的 select_related 和 prefetch_related 函数对 QuerySet 查询的优化(一)

prefetch_related 方法返回一个 QuerySet 对象, 其作用与 select_related 方法类似,用来减少 ORM 层与数据库的交互的。与 select_related 不同 prefetch_related 不使用 JOIN 方式来查询数据库, 而是分别查找每个表, 最后在 python 实现 JOIN 操作。prefetch_related 方法适用于 many-to-many 和 one-to-many 的对应关系。 ForeignKey 是 many-to-one 的对应关系, 但是反过来, 被 ForeignKey 关联的字段就是 one-to-many 的关系了(子表回溯父表)。

1、使用 prefetch_related

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Many-To-Many 模式
e = Entry.objects.prefetch_related('authors').filter(id=5)
# 在 e 求值的时候,会执行两次 SQL 查询:
# 第一次: 先查询 entry 表, 获得 Entry 对象 e,
# 第二次: 本次主要是 prefetch_related 执行的查询, 会在第一次查询结束后立刻执行。其根据第一次查询的结果, 生成相应的 SQL 查询 author 表, 获取 authors 属性值
if e.exists():
e = e[0]
for author in e.authors.all():
print('author %s' % author.name) # 不查询数据库, 直接从 caches 取出数据。

# One-To-Many 模式
# entry 通过 blog 外键字段与 blog 表相关联,
# 此时 blog 与 entry 表的对应关系是 One-To-Many
b = Blog.objects.prefetch_related('entry_set').filter(id=5)
# 在 b 求值的时候, 同样会执行两次 SQL 查询
# 第一次: 先查询 blog 表, 获得 Blog 对象 b
# 第二次: 根据第一次查询的结果, 生成相应的 SQL 去查询 entry 表, 获取 entry_set 属性值
if b.exists():
b = b[0]
for entry in b.entry_set.all():
print('entry %s' % entry.headline) # 不查询数据库, 从 caches 取出数据

2、prefetch_related 级联
跟 select_related 一样, prefetch_related 也可以通过双下划线(__), 执行级联操作:
a、全是 many-to-many 的级联关系, 例如: entry 表中的 authors, 与 author 表中 visition

1
2
3
4
5
6
7
8
9
10
11
e = Entry.objects.prefetch_related('authors_visition').filter(id=5)
# 在 e 求值的时候,会执行三次 SQL 查询:
# 第一次: 先查询 entry 表, 获得 Entry 对象 e,
# 第二次: 其根据第一次查询的结果, 生成相应的 SQL 查询 author 表, 获取 authors 属性值
# 第三次: 根据第二次查询的结果, 生成相应的 SQL 语句 查询 city 表, 获取 visition 属性值
if e.exists():
e = e[0]
for author in e.authors.all():
for city in author.visition.all(): # 不查询数据库
print('city %s' % city.name) # 不查询数据库
# 2、

b、ForeignKey 和 many-to-many 的级联关系, 例如: entry 表中的 blog, 与 blog 表中的 tag

1
2
3
4
5
# e 求值时会顺序的生成三个 SQL 语句, 分别查询三张表, entry, blog, tag 
e = Entry.objects.prefetch_related('blog__tag').filter(id=5)
# 为了减少查询次数, 由于 entry 与 blog 是 ForeignKey 对关系,
# 可以使用 select_related 将查询次数减少为两次, entry, blog 两张表合在一起查询
e2 = Entry.objects.filter(id=5).select_related('blog').prefetch_related('blog__tag')

3、prefetch_related 注意
在遍历 prefetch_related 获得结果时, 改变 QuerySet 的查询条件, 所有的 prefetch_related 的结果将全部失效, 这个在开发中需要注意。

1
2
3
4
5
entries = Entry.objects.prefetch_related('authors').all()
if entries.exists():
# 由于改变了 authors 的查询条件, 因此,每一次循环都会查询一次数据库
# 导致 prefetch_related 查询结果完全没有发挥作用
[e.authors.filter(name__contains='john') for e in entries]

4、清空 prefetch_related
同 select_related 一样, 只需要传入 None 参数即可

1
2
e = Entry.objects.prefetch_related('authors').all()
e_clear = e.prefetch_related(None)

5、使用 Prefetch 对象控制 prefetch_related 查询
使用 Prefetch 对象可以进一步控制 prefetch_related 查询返回的结果, 而不仅仅是 all()
a、基本使用

1
2
from django.db.models import Prefetch
e = Entry.objects.prefetch_related(Prefetch('authors'))

b、添加查询参数 queryset

1
2
3
4
from django.db.models import Prefetch
e = Entry.objects.prefetch_related(
Prefetch('authors', queryset=Author.objects.filter(name__contains('john').order_by('name'))
)

c、用to_attr 添加属性

1
2
3
4
5
e = Entry.objects.prefetch_related(
Prefetch('authors', queryset=Author.objects.filter(name__contains('john').order_by('name'))
to_attr='john_author')
# 此时 queryset 对象 e 中包含的 每个 model 对象都包含有 john_author 属性
john_author = e[0].john_auhor

深入理解
请参考博客 实例详解Django的 select_related 和 prefetch_related 函数对 QuerySet 查询的优化(二)

next to add