Django REST Serializer执行N + 1个数据库调用,实现多个嵌套关系,3个级别

时间:2016-01-26 18:46:33

标签: django django-models polymorphism django-rest-framework

我的情况是我的模型有外键关系:

# models.py
class Child(models.Model):
    parent = models.ForeignKey(Parent,)

class Parent(models.Model):
    pass

和我的序列化器:

class ParentSerializer(serializer.ModelSerializer):
    child = serializers.SerializerMethodField('get_children_ordered')

    def get_children_ordered(self, parent):
        queryset = Child.objects.filter(parent=parent).select_related('parent')
        serialized_data = ChildSerializer(queryset, many=True, read_only=True, context=self.context)
        return serialized_data.data

    class Meta:
        model = Parent

当我在N个父项的视图中调用Parent时,Django在抓取子节点时在序列化程序中进行了N次数据库调用。有没有办法让所有的孩子为所有家长最小化数据库呼叫的数量?

我已经尝试过了,但它似乎无法解决我的问题:

class ParentList(generics.ListAPIView):

    def get_queryset(self):
        queryset = Parent.objects.prefetch_related('child')
        return queryset

    serializer_class = ParentSerializer
    permission_classes = (permissions.IsAuthenticated,)

修改

我已经更新了下面的代码,以反映Alex的反馈....它解决了N + 1的一个嵌套关系。

# serializer.py
class ParentSerializer(serializer.ModelSerializer):
    child = serializers.SerializerMethodField('get_children_ordered')

    def get_children_ordered(self, parent):
        # The all() call should hit the cache
        serialized_data = ChildSerializer(parent.child.all(), many=True, read_only=True, context=self.context)
        return serialized_data.data

    class Meta:
            model = Parent

# views.py
class ParentList(generics.ListAPIView):

    def get_queryset(self):
        children = Prefetch('child', queryset=Child.objects.select_related('parent'))
        queryset = Parent.objects.prefetch_related(children)
        return queryset

    serializer_class = ParentSerializer
    permission_classes = (permissions.IsAuthenticated,)

现在让我们再说一个模型,这是一个孙子:

# models.py
class GrandChild(models.Model):
    parent = models.ForeignKey(Child,)

class Child(models.Model):
    parent = models.ForeignKey(Parent,)

class Parent(models.Model):
    pass

如果我将以下内容放在我的views.py父亲queryset中:

queryset = Parent.objects.prefetch_related(children, 'children__grandchildren')

看起来那些孙子孙女正被带入ChildSerializer,因此,我再次运行另一个N + 1问题。对此有何看法?

编辑2

也许这会提供清晰度......也许我仍在进行N + 1数据库调用的原因是因为我的子孙类都是多态的......即

# models.py
class GrandChild(PolymorphicModel):
    child = models.ForeignKey(Child,)

class GrandSon(GrandChild):
    pass

class GrandDaughter(GrandChild):
    pass

class Child(PolymorphicModel):
    parent = models.ForeignKey(Parent,)

class Son(Child):
    pass

class Daughter(Child):
    pass

class Parent(models.Model):
    pass

我的序列化程序看起来更像是这样:

# serializer.py
class ChildSerializer(serializer.ModelSerializer):
    grandchild = serializers.SerializerMethodField('get_children_ordered')

    def to_representation(self, value):
        if isinstance(value, Son):
            return SonSerializer(value, context=self.context).to_representation(value)
        if isinstance(value, Daughter):
            return DaughterSerializer(value, context=self.context).to_representation(value)

    class Meta:
        model = Child

class ParentSerializer(serializer.ModelSerializer):
    child = serializers.SerializerMethodField('get_children_ordered')

    def get_children_ordered(self, parent):
        queryset = Child.objects.filter(parent=parent).select_related('parent')
        serialized_data = ChildSerializer(queryset, many=True, read_only=True, context=self.context)
        return serialized_data.data

    class Meta:
        model = Parent

对于Grandaughter,Grandson来说,同样,我会以代码方式向您提供详细信息,但我认为您可以了解相关信息。

当我为ParentList运行我的视图,并且我监视数据库查询时,我只获得了几千个查询的内容,只有少数父母。

如果我在django shell中运行相同的代码,我可以在不超过25个查询的情况下完成相同的查询。我怀疑它可能与我使用django-polymorphic库的事实有关?原因在于,除了每个Son / Daughter,Grandson / Granddaughter表之外,还有一个Child和GrandChild数据库表,总共有6个表。跨越这些对象。所以我的直觉告诉我,我错过了那些多态表。

或许对我的daata模型来说,这是一个更优雅的解决方案吗?

2 个答案:

答案 0 :(得分:5)

据我记忆,嵌套序列化程序可以访问预取关系,只是确保您不修改查询集(即使用all()):

class ParentSerializer(serializer.ModelSerializer):
    child = serializers.SerializerMethodField('get_children_ordered')

    def get_children_ordered(self, parent):
        # The all() call should hit the cache
        serialized_data = ChildSerializer(parent.child.all(), many=True, read_only=True, context=self.context)
        return serialized_data.data

    class Meta:
            model = Parent


class ParentList(generics.ListAPIView):

    def get_queryset(self):
        children = Prefetch('child', queryset=Child.objects.select_related('parent'))
        queryset = Parent.objects.prefetch_related(children)
        return queryset

    serializer_class = ParentSerializer
    permission_classes = (permissions.IsAuthenticated,)             

答案 1 :(得分:0)

这个问题有点旧,但我遇到了一个非常类似的问题,并设法大幅减少db调用。在我看来,Django-mptt会让事情变得更容易。

一种方法是使用ForeignKey定义单个模型。这样,您可以通过树中的级别找出层次结构。例如:

class Person(MPTTModel):
    parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)  

你可以通过检查Person.level是否为0来找出对象是否是父对象。如果它等于1,它是一个孩子,两个孙子等...

然后,您可以将代码修改为以下内容:

# serializers.py
class ChildSerializer(serializers.ModelSerializer):
    children = serializers.SerializerMethodField()

    def get_children(self, parent):
        queryset = parent.get_children()
        serialized_data = ChildSerializer(queryset, many=True, read_only=True, context=self.context)
        return serialized_data.data

# views.py
class ParentList(generics.ListAPIView):

    def get_queryset(self):
        queryset = cache_tree_children(Person.objects.all())

有了这个,你就可以消除你的N + 1问题。例如,如果要将新的ForeignKey添加到流派模型,则只需将最后一行修改为:

queryset = cache_tree_children(Person.objects.filter(channel__slug__iexact=channel_slug).select_related('genre'))