Django 避免N+;1使用引用反向外键关系的属性序列化模型时的查询

Django 避免N+;1使用引用反向外键关系的属性序列化模型时的查询,django,django-rest-framework,Django,Django Rest Framework,我目前遇到一个问题,Django的prefetch_related()和prefetch_related_objects()都不足以优化Django Rest框架序列化,我很好奇这里是否有人有一些想法 为了形象化我所处的情况,假设我有三个模型,它们相互关联: class VotableObjectExampleParent(models.Model): #several other fields redacted here# class VotableObjectExample(mod

我目前遇到一个问题,Django的
prefetch_related()
prefetch_related_objects()
都不足以优化Django Rest框架序列化,我很好奇这里是否有人有一些想法

为了形象化我所处的情况,假设我有三个模型,它们相互关联:

class VotableObjectExampleParent(models.Model):
    #several other fields redacted here#

class VotableObjectExample(models.Model):
    #several other fields redacted here#
    votableobjectexampleparent = models.ForeignKey('VotableObjectExampleParent', on_delete=models.CASCADE, related_name = 'votableobjectexamples')
    @property
    def total_votes(self):
        return self.votableobjectexample_votes.aggregate(sum = Sum("vote"))["sum"] or 0

class VotableObjectExample_Vote(models.Model):
    #several other fields redacted here#
    votablebjectexample = models.ForeignKey('VotableObjectExample', on_delete=models.CASCADE, related_name = 'votablebjectexample_votes')
    vote = models.IntegerField(choices = [(-1, "down"), (1, "up")])

然后,我有了适当的(功能性的)DRF序列化程序,如图所示:

class VotableObjectExampleParent_Serializer(serializers.ModelSerializer):
   related_votableobjectexamples = Idea_Proposal_Serializer(source = "votableobjectexamples", many=True, read_only=True)
    class Meta:
        model = VotableObjectExampleParent
        fields = ['related_votableobjectexamples'] #other fields would normally be in this array
        read_only_fields = fields

class VotableObjectExample_Serializer(serializers.ModelSerializer):
    class Meta:
        model = VotableObjectExample
        fields = ['total_votes'] #other fields would normally be in this array
        read_only_fields = fields
首先,我只需在视图中调用父序列化程序(
VotableObjectExampleParent\u序列化程序
):

#assume a variable I already have called this_pk is correct and available for use
serializer = VotableObjectExampleParent_Serializer(VotableObjectExampleParent.objects.get(pk = this_pk), many = True)
JSONRenderer().render(serializer.data)
这产生了100%符合我需要的正确JSON,但我很快就遇到了N+1查询的问题,因为有大量的
VotableObjectExamples
VotableObjectExample_投票
,在现实生活中,还有大量其他嵌套的反向外键关系需要遍历。通过使用
prefetch\u related\u objects()
替换上面的代码块,我能够将这需要的30个查询(很容易扩展到100个)减少到20个:

this_votableobjectparent = [VotableObjectExampleParent.objects.get(pk = this_pk)]
prefetch_related_objects(this_votableobjectparent, 'votableobjectexamples', 'votableobjectexamples__votablebjectexample_votes')
JSONRenderer().render(VotableObjectExampleParent_Serializer(this_votableobjectparent[0]).data)
使用fantastic
Django调试工具栏
我可以验证是否正确地进行了查询,以便在上面所有指定的模型中预取正确的ID。尽管如此,仍有N+1个查询仅用于计算
总投票数
字段

我从不同的StackOverflow线程中尝试了一百万种不同的建议,比如从使用
aggregate
切换到
annotate

@property
def total_votes(self):
    query = self.votablebjectexample_votes.annotate(sum = Sum("vote")).values_list('sum',flat=True)
        if query:
            return query[0]
        else:
            return 0
我还尝试为每个模型添加一个自定义的
Manager
,该模型带有
total\u voces
字段,将total\u voces计算移动到get\u queryset方法。不幸的是,在这种情况下,DRF抱怨它根本找不到
total_voces
字段,我怀疑这是因为该字段不再在
ModelSerializer
指向的模型中显式声明

有人知道从这里到哪里去吗?也许有一个简单的解决方案,但对于某些人来说,这似乎不是一个特别不寻常的问题,所以我有点惊讶,需要这么多的修补才能产生高效和可扩展的东西


提前感谢您的帮助

可能是
N+1
的根本原因是
VotableObjectExample.total\u Voces
属性,因为
self.VotableObjectExample\u Voces
将创建另一个查询

  • 使用
    预回迁\u related
    时,它会存储在
    实例中。_prefetched\u objects\u cache
    (请注意,如果未执行预回迁,它将导致
    AttributeError
    )。如果
    instance.\u prefetched\u objects\u cache
    存在,则它是
    {”“:,…}
    dict

  • 另一点是,即使使用了
    实例。_prefetched_objects_cache
    ,在缓存的queryset中添加更多查询也会创建另一个查询。在这种情况下,运行以下行将导致另一个查询,因为聚合是另一个查询:

    self._prefetched_objects_cache["votableobjectexample_votes"].aggregate(sum = Sum("vote"))["sum"]
    

总之,你可以试试这个:

class VotableObjectExample(models.Model):
    #several other fields redacted here#

    @property
    def total_votes(self):
        # Check if prefetch cache exists
        if hasattr(self, "_prefetched_objects_cache") and self._prefetched_objects_cache.get("votableobjectexample_votes"):
            total_votes = 0

            for votableobjectexample_vote in votableobjectexample_votes:
                total_votes += votableobjectexample_vote.vote

            return total_votes

        # Still need this if `prefetch_related` wasn't called
        return self.votableobjectexample_votes.aggregate(sum = Sum("vote"))["sum"] or 0

可伸缩:将作业放入队列中,以计算和存储每个投票事件的总投票数,而不是在查询时计算。或者,实现一个查询缓存,并在每次投票事件中插入一个作业来更新它。@bryan60,这很有意义。我仍然很好奇如何通过查找来优化属性(谁知道什么时候该问题会合法出现!),但我必须承认,在解决我的特定问题时,您是正确的。我需要考虑如何在这里编写SQL。感觉可以对此进行优化,但可能是使用了
@property
装饰器弄乱了ORM。我会先设法解决这个问题,不让事情复杂化,然后看看你能不能在事后再把那个部分放进去。DRF很棒,但序列化程序会将一些愚蠢的查询留给自己的设备。应该有一种方法让它在一个查询中计算并注释所有投票总数,而不是在每个votable对象的查询中。