I am trying to figure out the best way to add annotated fields, such as any aggregated (calculated) fields to DRF (Model)Serializers. My use case is simply a situation where an endpoint returns fields that are NOT stored in a database but calculated from a database.
Let's look at the following example:
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
desired JSON output:
[ { "name": "Pete's Ice Cream", "total_trucks": 20, "total_capacity": 4000 }, ... ]
I have a couple solutions that work, but each have some issues.
Option 1: add getters to model and use 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')
The above code can perhaps be refactored a bit, but it won't change the fact that this option will perform 2 extra SQL queries per IceCreamCompany which is not very efficient.
Option 2: annotate in ViewSet.get_queryset
models.py as originally described.
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') )
This will get the aggregated fields in a single SQL query but I'm not sure how I would add them to the Serializer as DRF doesn't magically know that I've annotated these fields in the QuerySet. If I add total_trucks and total_capacity to the serializer, it will throw an error about these fields not being present on the Model.
Option 2 can be made work without a serializer by using a View but if the model contains a lot of fields, and only some are required to be in the JSON, it would be a somewhat ugly hack to build the endpoint without a serializer.
In the Django framework, both annotate and aggregate are responsible for identifying a given value set summary. Among these, annotate identifies the summary from each of the items in the queryset. Whereas in the case of aggregate, the summary is calculated for the entire queryset.
Serializers in Django REST Framework are responsible for converting objects into data types understandable by javascript and front-end frameworks. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON , XML or other content types.
When specifying the field to be aggregated in an aggregate function, Django will allow you to use the same double underscore notation that is used when referring to related fields in filters. Django will then handle any table joins that are required to retrieve and aggregate the related value.
Possible solution:
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')
By using Serializer fields I got a small example to work. The fields must be declared as the serializer's class attributes so DRF won't throw an error about them not existing in the IceCreamCompany model.
I made a slight simplification of elnygreen's answer by annotating the queryset when I defined it. Then I don't need to override 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')
As elnygreen said, the fields must be declared as the serializer's class attributes to avoid an error about them not existing in the IceCreamCompany model.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With