web-dev-qa-db-ja.com

Django Rest Frameworkシリアライザーの集計フィールド(およびその他の注釈付きフィールド)

集計(計算)フィールドなどの注釈付きフィールドを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()

serializers.py

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

目的のJSON出力:

[

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

いくつかの解決策がありますが、それぞれにいくつかの問題があります。

オプション1:モデルにゲッターを追加し、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']

serializers.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クエリを実行するという事実を変更することはありませんper 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に含める必要があるのは一部のみである場合、構築するにはややいハックになりますシリアライザーのないエンドポイント。

40
elnygren

可能な解決策:

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')
        )

serializers.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モデルに存在しないというエラーをスローしないように、フィールドはシリアライザーのクラス属性として宣言する必要があります。

49
elnygren

定義時にクエリセットに注釈を付けることで、 elnygreen's answer を少し単純化しました。その後、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モデルに存在しないというエラーを避けるために、フィールドはシリアライザーのクラス属性として宣言する必要があります。

2
Don Kirkby

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(何もオーバーライドしません)、単にコンストラクターから肥大化を防ぐことができます。

1
Andrey Berenda