1. ホーム
  2. django

Django Rest Framework のシリアライザで集約(と他のアノテーション)されたフィールドを使う

2023-10-10 11:47:59

質問

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: ゲッターをモデルに追加し、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を最初に説明したようにします。

ビュー.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')
        )

これは1つのSQLクエリで集約されたフィールドを取得しますが、DRFはこれらのフィールドがQuerySetにアノテーションされていることを魔法のように知らないので、シリアライザにどのように追加すればよいのかわかりません。シリアライザーに total_trucks と total_capacity を追加すると、これらのフィールドがモデル上に存在しないというエラーがスローされます。

オプション 2 は、シリアライザーを使用せずに動作させるために ビュー を使用することでシリアライザーなしで動作させることができますが、モデルが多くのフィールドを含み、JSONにあることが必要なのは一部だけである場合、シリアライザーなしでエンドポイントを構築するのはやや醜いハックとなるでしょう。

どのように解決するのでしょうか?

考えられる解決方法です。

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

を使うことで シリアライザーのフィールド を使用することで、小さなサンプルを動作させることができました。フィールドはシリアライザのクラス属性として宣言する必要があり、DRFがIceCreamCompanyモデルに存在しないというエラーを投げないようにするためです。