Функция 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:

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 с 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 раза.

Метки

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