Django prefetch_related

26 Ноя 2020 , 556

Многие начинающие разработчики на 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

comments powered by Disqus

Подписка

Подпишитесь на наш список рассылки, чтобы получать обновления из блога

Рубрики

Теги