Django-使用 ManyToManyField 查询嵌套模型中的重复查询

Posted

技术标签:

【中文标题】Django-使用 ManyToManyField 查询嵌套模型中的重复查询【英文标题】:Django- Duplicated queries in nested models querying with ManyToManyField 【发布时间】:2022-01-01 20:44:38 【问题描述】:

如何消除屏幕截图中的重复查询?


我有以下两种型号,

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。 此外,在粘贴中省略的 urls.py 中添加 path('games/', GamesByGenreAPI.as_view()),

更新

尝试记录查询以检查它是否与调试工具栏有关,但不是,查询重复.. 这是屏幕截图。

【问题讨论】:

我认为是调试工具栏上的问题。我有类似的问题,重复查询和非常慢的性能,cpu使用率很高......最后,我删除了调试工具栏并直接在mysql日志中检查了查询。这很好。无重复查询,快速响应... @Tonio 在我的情况下它也在 postgres 中重复.. 我认为您的查询很好,它们非常相似但不一样。您获得最佳性能的时间成本可能会比这些查询消耗的资源(cpu、内存)高得多。 prefetch_related('children__children'),为什么不只是 children 在 prefetch_related 中? prefetch_related, on the other hand, does a separate lookup for each relationship, and does the ‘joining’ in Python 【参考方案1】:

这是我如何克服多个查询的方法。

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(),但我试图将其保留在一种方法中以便更好地理解。

【讨论】:

【参考方案2】:

prefetch_related 仅适用于根级别树,因为它仅在该查询中指定。通过使用RecursiveField 生成的新查询获得的后续子对象没有预取这些相关对象。也许您可以覆盖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)

【讨论】:

一种冗长的解决方案。接受的答案在不牺牲“Django 的魔力”的情况下完成了这项工作,感谢您的努力。 没问题。谢谢@Rivadiz。【参考方案3】:

从调试工具栏输出中,我假设您在 Genre 模型中有两层嵌套(根,第 1 层)。不知道Level 1有没有孩子,即有Level 2的流派,因为我无法查看查询结果(但这与当前问题无关)。

根级别 Genres 是 (1, 4, 7),级别 1 是 (2, 3, 5, 6, 8, 9)。 预取适用于这些查找prefetch_related("children__children"),因为查询被分组在两个单独的查询中,这是应该的。

与根级别类型 (prefetch_related("games")) 相关的游戏的下一个查询也被预取。这是调试工具栏输出中的第四个查询。

如您所见,接下来的查询是在单独的查询中获取每个级别 1 类型的游戏,我认为这是从序列化器字段触发的,因为视图中没有指定可以预取这些记录的查找.添加针对这些记录的另一个预取查找应该可以解决问题。

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)

【讨论】:

【参考方案4】:

您错过了prefetch_related() 中的children__games 关系。如果你更换它会工作

prefetch_related('children__children', 'games')

prefetch_related('children__children', 'children__games', 'games')

Here is request list at Django debug panel

【讨论】:

以上是关于Django-使用 ManyToManyField 查询嵌套模型中的重复查询的主要内容,如果未能解决你的问题,请参考以下文章

Django:ManyToManyField,如果对象不存在则添加它

关于 django ManyToManyField

Django:带有附加列的 ManyToManyField

如何在 Django 中使用 through 过滤 ManytoManyField?

Django学习17 -- ManytoManyField

Django-使用 ManyToManyField 查询嵌套模型中的重复查询