共计 3649 个字符,预计需要花费 10 分钟才能阅读完成。
本篇笔记目录如下:
- select_related
- prefetch_related
在介绍 select_related 和 prefetch_related 这两个函数前,咱们先来看一个例子。
对于,Entry 和 Blog 这两个 model,后面介绍过,Blog 是 Entry 的外键,如下:
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
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()
authors = models.ManyToManyField(Author)
number_of_comments = models.IntegerField()
number_of_pingbacks = models.IntegerField()
rating = models.IntegerField()
比方咱们须要获取 Entry 的前十条数据,而后打印出关联的 Blog 的 name 字段信息。
咱们个别会如此操作:
for entry in Entry.objects.all()[:10]
if entry.blog:
print(entry.blog.name)
else:
print("没有关联 blog 数据")
然而这样会有一个问题,那就是,这个 for 循环的操作会查问数据十一次,一次查问 Entry 数据,十次是查问每个 entry_obj 关联的 blog 数据。
这个设计对于零碎来说是不合理的,想一想如果咱们查问的数据是一千条,一万条,无论是零碎接口的等待时间,还是数据库的拜访压力,都是不可承受的。
因而咱们能够引入 外键 和 ManyToManyTo 的一种可能缩小数据库的拜访次数的形式:select_related,prefetch_related。
select_related
当咱们在应用的时候,如果有须要获取的外键数据,比方 Entry 关联的 Blog 数据,则能够将其字段名作为参数传入,这样在获取数据的时候就能够一次性将所有关联的 Blog 数据也取出来,而不必独自再去查问一遍数据库。
如下,批量操作:
for entry in Entry.objects.select_related("blog").all():
print(e.blog) # 这个操作不会额定再去查询数据库
当然也实用于 单条数据:
e = Entry.objects.get(id=5).select_related("blog")
为了验证 select_related() 的确会只查问一遍数据库,有两种办法:
一种是在数据库层面打印进去所有查问的 SQL 语句,
另一种能够从侧面示意,那就是在零碎层面打印出咱们的查问条件转化的 SQL 语句。
比方:
Entry.objects.select_related("blog").all().query.__str__()
能够看到会输入一个 关联了 Blog 表的 inner join 的 SQL 语句。
SELECT `blog_entry`.`id`, `blog_entry`.`blog_id`, `blog_entry`.`headline`, `blog_entry`.`body_text`, `blog_entry`.`pub_date`, `blog_entry`.`mod_date`, `blog_entry`.`number_of_comments`, `blog_entry`.`number_of_pingbacks`, `blog_entry`.`rating`, `blog_blog`.`id`, `blog_blog`.`name`, `blog_blog`.`tagline` FROM `blog_entry` INNER JOIN `blog_blog` ON (`blog_entry`.`blog_id` = `blog_blog`.`id`)
链式获取外键数据
比方上面的 model:
class City(models.Model):
pass
class Person(models.Model):
hometown = models.ForeignKey(City, on_delete=models.SET_NULL, blank=True, null=True)
class Book(models.Model):
author = models.ForeignKey(Person, on_delete=models.CASCADE)
咱们能够通过以下语句来将 Book 关联的 Person,以及该条 Person 数据关联的 City 数据一起查问进去:
book = Book.objects.select_related("author__hometown").get(id=4)
person = book.author
city = person.hometown
因为咱们在第一步查问的时候,通过双下划线将两个外键字段连贯在一起取了进去,所以在第二步和第三步取 Person 数据和 City 数据的时候,就不须要再次查询数据库了。
同时获取多个外键关联字段
如果一个 model 有两个外键字段 foo 和 bar,那么上面的两种写法都将这两个外键字段关联取出:
select_related("foo", "bar")
select_related("foo").select_related("bar")
须要留神的是,这个链式的操作和 order_by() 的后果是不一样的哦,后面提到的 order_by() 的链式操作会导致前面的笼罩后面的,然而取外键数据的时候会同时取出。
留神: select_related() 仅作用于 ForeignKey 和 OneToOne,如果是 ManyToMany 字段,则须要用到上面的 prefetch_related() 函数。
prefetch_related()
prefetch_related() 和 select_related() 作用相似,都是通过缩小查问的次数,来实现查问优化。
但 prefetch_related() 是针对 ManyToMany 的操作。
举个例子:
from django.db import models
class Topping(models.Model):
name = models.CharField(max_length=30)
class Pizza(models.Model):
name = models.CharField(max_length=50)
toppings = models.ManyToManyField(Topping)
def __str__(self):
return "%s (%s)" % (
self.name,
",".join(topping.name for topping in self.toppings.all()),
)
当咱们执行:
Pizza.objects.all()
的时候,因为每一条 Pizza 数据实例化的时候,都会调用 __str__() 函数,而这个函数会再次去申请一遍数据库,所以多条 Pizza 数据会导致查问屡次数据库。
因为咱们能够应用 prefetch_related() 函数来达到缩小查问的目标:
Pizza.objects.prefetch_related('toppings').all()
这样的话,对数据库的查问会缩小到两次,一次是查问出所有的 Pizza 数据,一次是依据所有的 pizza_id 找到所有关联的 topping 数据。
如果有趣味,能够比对上面两条语句在 shell 中执行的时候,MySQL 服务器接管到的 SQL 查问语句:
Pizza.objects.all()
Pizza.objects.prefetch_related('toppings').all()
上面一种状况须要留神哦:
pizzas = Pizza.objects.prefetch_related('toppings')
[list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]
因为第二步操作里,会对 toppings 数据进行一次新的 filter 过滤操作,所以会导致每次该语句从新去查询数据库,也就是说,咱们的 prefetch_related() 操作是生效的。
以上就是本篇笔记全部内容,接下来会介绍查问里的 defer 和 only 函数。
本文首发于自己微信公众号:Django 笔记。
原文链接:Django 笔记十一之外键查问优化 select_related 和 prefetch_related
如果想获取更多相干文章,可扫码关注浏览: