Django- Дублирование запросов во вложенных моделях при запросе с помощью ManyToManyField
Как избавиться от дублирующихся запросов, как на скриншоте?
У меня есть две следующие модели,
class Genre(MPTTModel):
name = models.CharField(max_length=50, unique=True)
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True,
blank=True, related_name='children')
def __str__(self):
return self.name
class Game(models.Model):
name = models.CharField(max_length=50)
genre = models.ManyToManyField(Genre, blank=True, related_name='games')
def __str__(self):
return self.name
и иметь сериализатор и представления,
class GameSerializer(serializers.ModelSerializer):
class Meta:
model = Game
exclude = ['genre', ]
class GenreGameSerializer(serializers.ModelSerializer):
children = RecursiveField(many=True)
games = GameSerializer(many=True,)
class Meta:
model = Genre
fields = ['id', 'name', 'children', 'games']
class GamesByGenreAPI(APIView):
queryset = Genre.objects.root_nodes()
serializer_class = GenreGameSerializer
def get(self, request, *args, **kwargs):
ser = GenreGameSerializer(data=Genre.objects.root_nodes()
.prefetch_related('children__children', 'games'), many=True)
if ser.is_valid():
pass
return Response(ser.data)
Итак, в основном модель, заполненная при сериализации, выглядит следующим образом
Результат соответствует моим ожиданиям, но есть n дублированных запросов для каждого из жанров. Как я могу это исправить? Спасибо.
вот паста https://pastebin.com/xfRdBaF4 со всем кодом, если вы хотите воспроизвести проблему.
Вот мой подход к тому, как преодолеть множественные запросы.
from collections import defaultdict
from rest_framework.serializers import SerializerMethodField
class GameSerializer(serializers.ModelSerializer):
class Meta:
model = Game
exclude = ['genre', ]
class GenreGameSerializer(serializers.ModelSerializer):
children = SerializerMethodField(source='get_children')
games = GameSerializer(many=True)
class Meta:
model = Genre
fields = ['id', 'name', 'games']
def get_children(self, obj):
# get genre childrens from context and pass it to same serializer
# no extra queries are done, since we alredy have the instances
children = self.context['children'].get(obj.id, [])
serializer = GenreGameSerializer(children, many=True, context=self.context)
return serializer.data
class GamesByGenreAPI(APIView):
queryset = Genre.objects.root_nodes()
serializer_class = GenreGameSerializer
def get(self, request, *args, **kwargs):
# gather genres from queryset class attribute and prefetch games
genres = self.get_queryset().prefetch_related('games')
# gather all descendants of root nodes and prefetch games
genre_descendants = genres.get_descendants().prefetch_related('games')
# create a dictionary with key parent and value list of children
# this will not require extra queries
children_dict = defaultdict(list)
for descendant in descendants:
children_dict[descendant.parent_id].append(descendant)
# add the dictionary as context for serializer
context = self.get_serializer_context()
context['children'] = children_dict
# send the context to serializer
ser = GenreGameSerializer(data=genres, context=context, many=True)
return Response(ser.data)
Класс GamesByGenreAPI
можно написать более красиво, переопределив self.get_queryset()
и self.get_serializer_context()
, но я попытался сохранить его в одном методе для лучшего понимания.
prefetch_related
работает только на дереве корневого уровня, потому что он определен только в этом запросе. Дочерние дочерние элементы, полученные новым запросом, сформированным с помощью RecursiveField
, не имеют prefetch_related
. Возможно, вы можете перезаписать запросы в RecursiveField
, но я думаю, что он выполняет отдельный новый запрос в каждом найденном атрибуте childrens
.
Ну, если вы хотите сократить количество запросов до 3, вам нужно будет получить все данные перед моделями и вручную построить выходной массив рекурсивным способом.
Это очень грязный код, вы теряете "магию Django", и, как сказано в комментарии, я думаю, что это пустая трата времени в большинстве сценариев.
class GamesByGenreAPI(APIView):
def get(self, request, *args, **kwargs):
games = {}
for g in Game.objects.all():
games[g.pk] = {
'id': g.id,
'name': g.name
}
TreeGame = Game.genre.through
tree_game = {}
for tg in TreeGame.objects.all():
if tg.genre_id not in tree_game:
tree_game[tg.genre_id] = []
tree_game[tg.genre_id].append(tg.game_id)
childrens = {}
roots = []
for g in Genre.objects.all():
if g.level == 0:
roots.append(g)
else:
if g.parent_id not in childrens:
childrens[g.parent_id] = []
childrens[g.parent_id].append(g)
def _get_data_from_tree_branch(game):
branch_data = {
'id': game.pk,
'name': game.name
}
if game.pk in childrens:
# Move up if you need a children array in every response
branch_data['children'] = []
for c in childrens[game.pk]:
branch_data['children'].append(
_get_data_from_tree_branch(c)
)
if game.pk in tree_game:
# Move up if you need a games array in every response
branch_data['games'] = []
for rel in tree_game[game.pk]:
branch_data['games'].append(games[rel])
return branch_data
data = []
for g in roots:
data.append(_get_data_from_tree_branch(g))
return Response(data)
Из вывода панели инструментов отладки я предположу, что у вас есть два уровня вложенности в модели жанров (корень, уровень 1). Я не знаю, есть ли у Уровня 1 дочерние элементы, т.е. жанры Уровня 2, поскольку не могу просмотреть результаты запроса (но это не относится к текущей проблеме).
Жанры корневого уровня - (1, 4, 7), первого уровня - (2, 3, 5, 6, 8, 9).
Предварительная выборка сработала для этих поисков prefetch_related("children__children")
, поскольку запросы сгруппированы в два отдельных запроса, как и должно быть.
Следующий запрос на игры, относящиеся к жанрам корневого уровня (prefetch_related("games")
), также префетчирован. Это четвертый запрос в выводе панели инструментов отладки.
Следующие запросы, как вы можете видеть, получают игры для каждого из жанров первого уровня в отдельном запросе, которые, как я предполагаю, вызываются из полей сериализатора, так как в представлении нет указанного поиска, который мог бы предварительно получить эти записи. Добавление другого предварительного поиска, нацеленного на эти записи, должно решить проблему.
ser = GenreGameSerializer(data=Genre.objects.root_nodes()
.prefetch_related(
'children__children',
'games'
# prefetching games for Level 1 genres
'children__games'),
many=True)
Обратите внимание, что если вложенных жанров больше, то такая же логика должна применяться для каждого уровня вложенности. Например, если есть жанры уровня 2, то вы должны предварительно выбрать связанные игры для этих жанров с помощью:
ser = GenreGameSerializer(data=Genre.objects.root_nodes()
.prefetch_related(
'children__children',
'games'
'children__games',
'children__children__games'),
many=True)
Вы пропустили отношение children__games
в prefetch_related()
. Это будет работать, если вы замените
prefetch_related('children__children', 'games')
с
prefetch_related('children__children', 'children__games', 'games')