Django- Дублирование запросов во вложенных моделях при запросе с помощью ManyToManyField

Как избавиться от дублирующихся запросов, как на скриншоте?

enter image description here


У меня есть две следующие модели,

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)

Итак, в основном модель, заполненная при сериализации, выглядит следующим образом enter image description here

Результат соответствует моим ожиданиям, но есть 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')

Вот список запросов на панели отладки Django

Вернуться на верх