Django ORM эффективно запрашивает вложенные таблицы "многие ко многим"?
Допустим, я проектирую базу данных для создания моделей сессий следующим образом
from django.db import models
class Recipe(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Step(models.Model):
name = models.CharField(max_length=255)
recipes = models.ManyToManyField(Recipe, related_name='steps')
def __str__(self):
return self.name
class CookingSession(models.Model):
name = models.CharField(max_length=255)
steps = models.ManyToManyField(Step, related_name='cooking_sessions')
def __str__(self):
return self.name
Как с помощью минимального количества запросов (желательно одного) получить все шаги для определенной кулинарной сессии, где каждый шаг должен иметь соответствующие рецепты, если таковые имеются.
cooking_sessions = (
CookingSession.objects.annotate(
step_list=ArrayAgg(
models.F(
"steps__name",
),
distinct=True,
),
recipe_list=ArrayAgg(models.F("steps__recipes__name")),
)
)
Вот как выглядят данные
[
{
'id': 1,
'name': 'Italian Night',
'step_list': ['Preparation', 'Cooking', 'Serving'],
'recipe_list': ['Tomato Sauce', 'Pasta Dough', 'Spaghetti', 'Tomato Sauce', 'Garlic Bread']
},
...
]
Я бы хотел, чтобы данные имели вид
{
'id': 1,
'name': 'Italian Night',
'steps': [
{
'step_name': 'Preparation',
'recipes': ['Tomato Sauce', 'Pasta Dough']
},
{
'step_name': 'Cooking',
'recipes': ['Spaghetti', 'Tomato Sauce']
},
{
'step_name': 'Serving',
'recipes': ['Garlic Bread']
}
]
}
Вы можете преобразовать результат с помощью ArrayAgg
s [Django-doc] в:
from itertools import groupby
from operator import itemgetter
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import F
cooking_sessions = CookingSession.objects.annotate(
step_list=ArrayAgg('steps__name'),
recipe_list=ArrayAgg('steps__recipes__name'),
)
for cooking_session in cooking_sessions:
cooking_session.steps = [
{'step_name': name, 'recipes': [r for __, r in items]}
for name, items in groupby(
zip(cooking_session.step_list, cooking_session.recipe_list),
itemgetter(0),
)
]
Но это довольно сложно и чревато ошибками. Например, здесь мы предполагаем, что PostgreSQL вернет steps__name
и steps__recipes__name
в том же порядке, который со временем может измениться.
Я бы посоветовал просто префетчить элементы с помощью .prefetch_related(…)
[Django-doc], что позволит сделать это за два дополнительных запроса, но не за CookingSession
. Таким образом, независимо от количества CookingSession
s, Step
s и Recipe
s, мы получаем данные в трех запросах с:
CookingSession.objects.prefetch_related('steps__recipes')