Функция prefetch_related в Django
В этом посте хотел рассказать немного о том, как уменьшить количество запросов в базу на простом примере. А именно, хотел бы рассмотреть N+1 проблему в Django ORM.
Начну с установки последней версии Django:
pip install -U Django
Затем, начну новый проект и создам app foobar
, где и будут определены две модели.
django-admin startproject afoobar_django
python manage.py startapp foobar
Конечно, надо добавить наше app
в INSTALLED_APPS
:
1INSTALLED_APPS = [ 2 'foobar.apps.FoobarConfig', 3 ... 4]
Mодели будут называться Foo
и Bar
, они будут иметь one-to-many
связь, то есть у одной Foo
может быть много Bar
:
1# foobar/models.py 2from django.db import models 3 4 5class Foo(models.Model): 6 """У одного Foo может быть много Bar.""" 7 name = models.CharField(max_length=10) 8 9 10class Bar(models.Model): 11 """много Bar могут быть связаны с одним Foo.""" 12 name = models.CharField(max_length=10) 13 foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
Затем сделаю миграцию и немного добавлю логики для заполнения базы сразу после миграции:
python manage.py makemigrations
Для удобства буду заполнять базу сразу после миграции, файл foobar/migrations/0001_initial.py
:
1# Generated by Django 3.0.7 on 2020-06-03 14:34 2# foobar/migrations/0001_initial.py 3 4from django.db import migrations, models 5import django.db.models.deletion 6 7from foobar.models import Foo, Bar 8 9 10def populate_db(apps, schema_editor): 11 """Заполняем базу.""" 12 for i in range(3): 13 foo = Foo.objects.create(name='foo_{0}'.format(i)) 14 for j in range(5): 15 Bar.objects.create(name='bar_{0}'.format(j), foo_id=foo.id) 16 17 18class Migration(migrations.Migration): 19 20 initial = True 21 22 dependencies = [ 23 ] 24 25 operations = [ 26 migrations.CreateModel( 27 name='Foo', 28 fields=[ 29 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 ('name', models.CharField(max_length=10)), 31 ], 32 ), 33 migrations.CreateModel( 34 name='Bar', 35 fields=[ 36 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 ('name', models.CharField(max_length=10)), 38 ('foo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='foobar.Foo')), 39 ], 40 ), 41 migrations.RunPython(populate_db), 42 ]
То есть в функции populate_db
я создаю 3
объекта Foo
с 5ю
Bar
у каждого. Теперь можно делать миграции и открывать shell:
python manage.py migrate
python manage.py shell
И сначала выполним в shell
такой код и посмотрим количество запросов в базу, кстати проверять количество запросов можно так - print(len(connection.queries))
, а сама функция импортируется так: from django.db import connection
.
1from foobar.models import Foo, Bar 2from django.db import connection 3print('test1;', len(connection.queries)) 4 5foo_qs = Foo.objects.all() 6for foo in foo_qs: 7 print(foo.name) 8 print([bar.name for bar in foo.bar_set.all()]) 9 10print('test2;', len(connection.queries))
Что выведет:
test1; 0
foo_0
['bar_0', 'bar_1', 'bar_2', 'bar_3', 'bar_4']
foo_1
['bar_0', 'bar_1', 'bar_2', 'bar_3', 'bar_4']
foo_2
['bar_0', 'bar_1', 'bar_2', 'bar_3', 'bar_4']
test2; 4
То есть мы потратили 4 запроса в базу - 1 для того, чтобы взять все Foo
, и еще 3, чтобы взять Bar
для каждого Foo
. Но Django ORM позволяет сделать это и по-другому (запустим новый shell
):
1from foobar.models import Foo, Bar 2from django.db import connection 3from django.db.models import Prefetch 4print('test1;', len(connection.queries)) 5 6foo_qs = Foo.objects.all().prefetch_related( 7 Prefetch('bar_set', to_attr='prefetched_bars')) 8 9print('test2;', len(connection.queries)) 10 11for foo in foo_qs: 12 print(foo.name) 13 print([bar.name for bar in foo.prefetched_bars]) 14 15print('test3;', len(connection.queries))
Что выведет:
test1; 0
test2; 0
foo_0
['bar_0', 'bar_1', 'bar_2', 'bar_3', 'bar_4']
foo_1
['bar_0', 'bar_1', 'bar_2', 'bar_3', 'bar_4']
foo_2
['bar_0', 'bar_1', 'bar_2', 'bar_3', 'bar_4']
test3; 2
Как можно заметить, количество запросов уменьшилось в 2 раза.