集計(計算)フィールドなどの注釈付きフィールドを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に含める必要があるのは一部のみである場合、構築するにはややいハックになりますシリアライザーのないエンドポイント。
可能な解決策:
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モデルに存在しないというエラーをスローしないように、フィールドはシリアライザーのクラス属性として宣言する必要があります。
定義時にクエリセットに注釈を付けることで、 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モデルに存在しないというエラーを避けるために、フィールドはシリアライザーのクラス属性として宣言する必要があります。
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
(何もオーバーライドしません)、単にコンストラクターから肥大化を防ぐことができます。