Функция prefetch_related в Django | Блог python программиста
Изображение гика

Блог питониста

Функция prefetch_related в Django

6 июня 2020 г.

В этом посте хотел рассказать немного о том, как уменьшить количество запросов в базу на простом примере. А именно, хотел бы рассмотреть N+1 проблему в Django ORM.

Начну с установки последней версии Django:

pip install -U Django

Затем, начну новый проект и создам app foobar, где и будут определены две модели.

django-admin startproject afoobar_django
python manage.py startapp foobar

Конечно, надо добавить наше app в INSTALLED_APPS:

1 INSTALLED_APPS = [
2     'foobar.apps.FoobarConfig',
3     ...
4 ]

Mодели будут называться Foo и Bar, они будут иметь one-to-many связь, то есть у одной Foo может быть много Bar:

 1 # foobar/models.py
 2 from django.db import models
 3 
 4 
 5 class Foo(models.Model):
 6     """У одного Foo может быть много Bar."""
 7     name = models.CharField(max_length=10)
 8 
 9 
10 class 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 
 4 from django.db import migrations, models
 5 import django.db.models.deletion
 6 
 7 from foobar.models import Foo, Bar
 8 
 9 
10 def 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 
18 class 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 с Bar у каждого. Теперь можно делать миграции и открывать shell:

python manage.py migrate
python manage.py shell

И сначала выполним в shell такой код и посмотрим количество запросов в базу, кстати проверять количество запросов можно так - print(len(connection.queries)), а сама функция импортируется так: from django.db import connection.

 1 from foobar.models import Foo, Bar
 2 from django.db import connection
 3 print('test1;', len(connection.queries))
 4 
 5 foo_qs = Foo.objects.all()
 6 for foo in foo_qs:
 7     print(foo.name)
 8     print([bar.name for bar in foo.bar_set.all()])
 9  
10 print('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):

 1 from foobar.models import Foo, Bar
 2 from django.db import connection
 3 from django.db.models import Prefetch
 4 print('test1;', len(connection.queries))
 5 
 6 foo_qs = Foo.objects.all().prefetch_related(
 7 	Prefetch('bar_set', to_attr='prefetched_bars'))
 8 
 9 print('test2;', len(connection.queries))
10 
11 for foo in foo_qs:
12     print(foo.name)
13     print([bar.name for bar in foo.prefetched_bars])
14 
15 print('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 раза.

Метки

Django python
Если вам понравился пост, можете поделиться им в соцсетях: