👼 Django ORM:天使与魔鬼 II
date
Sep 9, 2022
slug
django-orm-best-practice-II.html
status
Published
tags
python
django
orm
best-practice
tech
summary
CRUD boy 回来了
type
Post
最近重操 CRUD 旧业,又有一些新的发现,故增加一篇 Django ORM:天使与魔鬼 Part II。
利用 batch_size 控制数据库单次提交的大小
bulk_create
和 bulk_update
是我们常用的批量创建、更新的方法,但批量提速一时爽,提交过长会直接导致任务失败。from itertools import islice
batch_size = 100
objs = (Entry(headline='Test %s' % i) for i in range(1000))
while True:
batch = list(islice(objs, batch_size))
if not batch:
break
Entry.objects.bulk_create(batch, batch_size)
通过 Prefetch 控制预取的查询
N + 1 问题是非常常见的查询效率杀手。在 Django 中我们通常会使用
selected_related
或prefetch_related
来预取关联对象,来减少和 DB 之间的交互,但是在使用上也需要有一些注意的地方。首先,预取需要精确控制到字段。
Django 默认的查询方式都是粗放的,例如普通查询不使用
values
或者 only
时都是 select *
,而预取也不例外,看看下面这个例子。class Foo(models.Model):
...
class Bar(models.Model):
foo = models.ForeignKey(Foo)
...
class Baz(models.Model):
"""A very large table"""
foo = models.ForeignKey(Foo)
我们在查询
Foo
时,会尝试预取关联字段以加速后续数据读取,但如果我们在调用时不加任何参数:Foo.objects.all().prefetch_related()
,默认地 Django 会将所有关联字段都取出来,加入 Baz
表无比巨大,本来用作性能优化的 prefetch_related
就会摇身变成耗时怪兽。此外,我们还会遇到级联预取的场景。
class Foo(models.Model):
...
class Bar(models.Model):
foo = models.ForeignKey(Foo, related_name="bars")
...
class Baz(models.Model):
bar = models.ForeignKey(Bar, related_name="bazs")
large_config = models.JSONField()
...
此时在后续的循环处理中,我们需要通过
Foo
对象查询到 Baz
的数据,为了避免 N + 1 我们也会多级预取:Foo.objects.filter().select_related("bars").prefetch_related("bars__bazs")
此时二级预取也是默认获取全部字段,倘若
Baz
表中有一个需要额外耗时序列化的字段,同样会使优化适得其反。这时可以考虑引入 Prefetch
对象,做更细致的查询控制。Foo.objects.filter().select_related("bars")
.prefetch_related(
Prefetch("bars__bazs", queryset=Baz.objects.defer("large_config"))
)
是不是觉得 ORM 查询本身也挺繁杂的?用 SQL 有时会更直接清晰地多。所以也会有一些完全不使用 ORM 的观点。在我看来,ORM 能让 90% 的查询都变得结构化更清晰、更易维护、甚至更安全,但剩下的 10% 也许会耗费更多的精力,所以何时使用 ORM 是根据具体项目场景来定的,不能因噎废食。
小广告
是不是觉得 Part II 内容有点少?没关系,更多的内容我都放在了这里。
我和团队小伙伴整理了很多 Python\Django\DRF 的最佳实践经验,项目会持续更新,欢迎一起探讨维护,希望每一个 CRUD 男孩/女孩都能少踩坑。