Django prefetch_related
Многие начинающие разработчики на Django при работе с Django ORM сталкиваются с проблемой известной как "N+1 запрос". Это неэффективное обращение к базе данных , когда для получения связанных данных для каждого объекта генерируется новый запрос. В предыдущем посте, мы рассмотрели как это проблема решается с помощью использования select_related. Но там мы рассматривали связь многое-к-одному. Советую ознакомиться с этой статьей
А что если у нас связь много-к-многим?Как быть в такой ситуации?
Допустим, у нас есть модель с пиццами. И каждая пицца состоит из начинок. Одна пицца может состоять из нескольких начинок. И каждая начинка может относиться к разным пиццам. Это связь много-к-многим(Many-to-Many)
from django.db import models
class Topping(models.Model):
"""
Модель для начинок
"""
name = models.CharField(max_length=30)
def __str__(self):
return self.name
class Pizza(models.Model):
"""
Модель для пицц
"""
name = models.CharField(max_length=50)
toppings = models.ManyToManyField(Topping)
def __str__(self):
return f"{self.name} ({','.join(topping.name for topping in self.toppings.all())})"
Напишем вьюху для вывода пицц
from django.views.generic import ListView
from .models import Pizza
class PizzaListView(ListView):
model = Pizza
queryset = Pizza.objects.all()
template_name = 'core/pizza_list.html'
И шаблон для вывода пицц
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Пиццы</title>
</head>
<body>
<h1>Пиццы</h1>
{% for pizza in object_list %}
<h3>{{ pizza }}</h3>
{% endfor %}
</body>
</html>
Смотрим на вывод и обнаружим там 4 запроса.
Если рассмотрим эти запросы внимательно , то поймем , что один запрос сделан для получения всех пицц. А потом для каждой пиццы делается новый запрос. Это проиходит , когда мы выводим название пиццы , то мы в скобках выводим название начинок тоже. И каждый раз когда выводим название пиццы , то идет запрос на получение начинок
def __str__(self):
return f"{self.name} ({','.join(topping.name for topping in self.toppings.all())})"
Здесь у нас три дублирующихся запросов. Если у нас на странице будет 15 пицц, то соответственно будут 15 дублирующихся запросов. Это и есть проблема n+1 запросов.
Чтобы решить эту проблему , мы добавляем prefetch_related.
class PizzaListView(ListView):
model = Pizza
queryset = Pizza.objects.prefetch_related('toppings')
template_name = 'core/pizza_list.html'
Теперь у нас вместо 4 запросов будут 2 запроса. Первым запросом мы получаем пиццы , а вторым запросом мы получаем все начинки для пицц.
В чем же отличие select_related и prefetch_related ?
select_related работает путем создания соединения SQL и включения полей связанного объекта в оператор SELECT. По этой причине select_related получает связанные объекты в том же запросе к базе данных. Однако, чтобы избежать гораздо большего набора результатов, который может возникнуть в результате объединения через отношение «многие», select_related ограничивается однозначными отношениями - внешним ключом и взаимно-однозначным.
prefetch_related выполняет отдельный поиск для каждой связи и выполняет «объединение» в Python. Это позволяет предварительно выбрать объекты «многие ко многим» и «многие к одному», что невозможно сделать с помощью select_related