【问题标题】:Aggregate (and other annotated) fields in Django Rest Framework serializers在 Django Rest Framework 序列化程序中聚合(和其他带注释的)字段
【发布时间】:2015-11-02 10:39:18
【问题描述】:

我正在尝试找出添加注释字段的最佳方法,例如将任何聚合(计算)字段添加到 DRF(模型)序列化器。我的用例只是端点返回的字段未存储在数据库中而是从数据库中计算出来的情况。

我们看下面的例子:

models.py

class IceCreamCompany(models.Model):
    name = models.CharField(primary_key = True, max_length = 255)

class IceCreamTruck(models.Model):
    company = models.ForeignKey('IceCreamCompany', related_name='trucks')
    capacity = models.IntegerField()

序列化器.py

class IceCreamCompanySerializer(serializers.ModelSerializer):
    class Meta:
        model = IceCreamCompany

所需的 JSON 输出:

[

    {
        "name": "Pete's Ice Cream",
        "total_trucks": 20,
        "total_capacity": 4000
    },
    ...
]

我有几个可行的解决方案,但每个都有一些问题。

选项 1:为模型添加 getter 并使用 SerializerMethodFields

models.py

class IceCreamCompany(models.Model):
    name = models.CharField(primary_key=True, max_length=255)

    def get_total_trucks(self):
        return self.trucks.count()

    def get_total_capacity(self):
        return self.trucks.aggregate(Sum('capacity'))['capacity__sum']

序列化器.py

class IceCreamCompanySerializer(serializers.ModelSerializer):

    def get_total_trucks(self, obj):
        return obj.get_total_trucks

    def get_total_capacity(self, obj):
        return obj.get_total_capacity

    total_trucks = SerializerMethodField()
    total_capacity = SerializerMethodField()

    class Meta:
        model = IceCreamCompany
        fields = ('name', 'total_trucks', 'total_capacity')

上面的代码也许可以重构一下,但它不会改变这个选项将执行 2 个额外的 SQL 查询的事实每个 IceCreamCompany 这不是很有效。

选项 2:在 ViewSet.get_queryset 中进行注释

models.py 与最初描述的一样。

views.py

class IceCreamCompanyViewSet(viewsets.ModelViewSet):
    queryset = IceCreamCompany.objects.all()
    serializer_class = IceCreamCompanySerializer

    def get_queryset(self):
        return IceCreamCompany.objects.annotate(
            total_trucks = Count('trucks'),
            total_capacity = Sum('trucks__capacity')
        )

这将在单个 SQL 查询中获取聚合字段,但我不确定如何将它们添加到序列化程序,因为 DRF 并不神奇地知道我已经在 QuerySet 中注释了这些字段。如果我将 total_trucks 和 total_capacity 添加到序列化程序,它会抛出关于模型上不存在这些字段的错误。

选项 2 可以通过使用 View 在没有序列化程序的情况下工作,但如果模型包含很多字段,并且只有一些字段需要在 JSON 中,那么构建端点将是一个有点丑陋的黑客攻击没有序列化器。

【问题讨论】:

    标签: django python-3.x django-rest-framework


    【解决方案1】:

    我对@9​​87654321@ 做了一点简化,在定义查询集时对其进行了注释。那么我就不需要覆盖get_queryset()

    # views.py
    class IceCreamCompanyViewSet(viewsets.ModelViewSet):
        queryset = IceCreamCompany.objects.annotate(
                total_trucks=Count('trucks'),
                total_capacity=Sum('trucks__capacity'))
        serializer_class = IceCreamCompanySerializer
    
    # serializers.py
    class IceCreamCompanySerializer(serializers.ModelSerializer):
        total_trucks = serializers.IntegerField()
        total_capacity = serializers.IntegerField()
    
        class Meta:
            model = IceCreamCompany
            fields = ('name', 'total_trucks', 'total_capacity')
    

    正如 elnygreen 所说,必须将字段声明为序列化程序的类属性,以避免出现有关它们在 IceCreamCompany 模型中不存在的错误。

    【讨论】:

    • 如何实例化这样的序列化程序?我正在编写单元测试,IceCreamCompanySerializer(instance=ice_cream_company) 显然不起作用,
    【解决方案2】:

    您可以破解 ModelSerializer 构造函数来修改它由视图或视图集传递的查询集。

    class IceCreamCompanySerializer(serializers.ModelSerializer):
        total_trucks = serializers.IntegerField(readonly=True)
        total_capacity = serializers.IntegerField(readonly=True)
    
        class Meta:
            model = IceCreamCompany
            fields = ('name', 'total_trucks', 'total_capacity')
    
        def __new__(cls, *args, **kwargs):
            if args and isinstance(args[0], QuerySet):
                  queryset = cls._build_queryset(args[0])
                  args = (queryset, ) + args[1:]
            return super().__new__(cls, *args, **kwargs)
    
        @classmethod
        def _build_queryset(cls, queryset):
             # modify the queryset here
             return queryset.annotate(
                 total_trucks=...,
                 total_capacity=...,
             )
    

    _build_queryset 这个名字没有任何意义(它没有覆盖任何东西),它只是让我们避免构造函数膨胀。

    【讨论】:

    • 请解释你的答案。它如何提高被接受的程度?
    • @RubioRic 这允许您使用序列化程序(而不是视图)保留注释。它应该的方式。请参阅其他答案的 cmets。
    • @Oli 我没有将此问题标记为低质量问题,但在审核队列中找到了它。我仍然认为可以改进这个答案,添加更多像你上面指出的细节。
    • 您可能会查看 Meta 中的字段以获取注释并添加它们。
    【解决方案3】:

    可能的解决方案:

    views.py

    class IceCreamCompanyViewSet(viewsets.ModelViewSet):
        queryset = IceCreamCompany.objects.all()
        serializer_class = IceCreamCompanySerializer
    
        def get_queryset(self):
            return IceCreamCompany.objects.annotate(
                total_trucks=Count('trucks'),
                total_capacity=Sum('trucks__capacity')
            )
    

    序列化器.py

    class IceCreamCompanySerializer(serializers.ModelSerializer):
        total_trucks = serializers.IntegerField()
        total_capacity = serializers.IntegerField()
    
        class Meta:
            model = IceCreamCompany
            fields = ('name', 'total_trucks', 'total_capacity')
    

    通过使用Serializer fields,我得到了一个小例子。这些字段必须声明为序列化程序的类属性,这样 DRF 就不会抛出有关它们在 IceCreamCompany 模型中不存在的错误。

    【讨论】:

    • 这是推荐的解决方案。 DRF 无法自省这些字段,因此您必须手动指定它们。
    • 我希望有一个解决方案,我可以在 SQL 实际触发之前修改序列化程序中的查询集。在序列化程序生命周期中我可以插入注释钩子吗?如果能够序列化任何基于IceCreamCompany 模型构建的查询集而无需手动注释,那就太好了……
    • 我试图达到相同的结果,但我不知道如何使用 ViewSet。您能否详细说明 IceCreamCompanyViewSet 是如何在您的(或理论上的)代码中使用的?
    • @RyanPergent 在 DRF 中,通过向路由器注册它们来使用 ViewSet,以便它们可以处理请求:django-rest-framework.org/api-guide/routers
    • 嘿@Co​​derer,你有没有机会找到一个解决方案来为传入的查询集添加一个钩子到序列化程序中?我现在正在为此苦苦挣扎,很惊讶似乎没有一个明确的地方可以做到这一点......
    猜你喜欢
    • 2016-01-03
    • 2013-11-15
    • 1970-01-01
    • 1970-01-01
    • 2016-04-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-07
    相关资源
    最近更新 更多