Serializers
Speed up serializers queries
Let’s say we have model Travel
with many related fields:
class Travel(models.Model):
tags = models.ManyToManyField(
Tag,
related_name='travels', )
route_places = models.ManyToManyField(
RoutePlace,
related_name='travels', )
coordinate = models.ForeignKey(
Coordinate,
related_name='travels', )
date_start = models.DateField()
And we want to build CRUD in /travels
via view ViewSet.
Here is the simple viewset:
class TravelViewset(viewsets.ModelViewSet):
queryset = Travel.objects.all()
serializer_class = TravelSerializer
Problem with this ViewSet is we have many related fields in our Travel
model, so Django will hit db for every Travel
instance. We can call select_related and prefetch_related directly in queryset
attribute, but what if we want to separate serializers for list
, retrieve
, create
.. actions of ViewSet.
So we can put this logic in one mixin and inherit from it:
class QuerySerializerMixin(object):
PREFETCH_FIELDS = [] # Here is for M2M fields
RELATED_FIELDS = [] # Here is for ForeignKeys
@classmethod
def get_related_queries(cls, queryset):
# This method we will use in our ViewSet
# for modify queryset, based on RELATED_FIELDS and PREFETCH_FIELDS
if cls.RELATED_FIELDS:
queryset = queryset.select_related(*cls.RELATED_FIELDS)
if cls.PREFETCH_FIELDS:
queryset = queryset.prefetch_related(*cls.PREFETCH_FIELDS)
return queryset
class TravelListSerializer(QuerySerializerMixin, serializers.ModelSerializer):
PREFETCH_FIELDS = ['tags'']
RELATED_FIELDS = ['coordinate']
# I omit fields and Meta declare for this example
class TravelRetrieveSerializer(QuerySerializerMixin, serializers.ModelSerializer):
PREFETCH_FIELDS = ['tags', 'route_places']
Now rewrite our ViewSet
with new serializers
class TravelViewset(viewsets.ModelViewSet):
queryset = Travel.objects.all()
def get_serializer_class():
if self.action == 'retrieve':
return TravelRetrieveSerializer
elif self.action == 'list':
return TravelListSerializer
else:
return SomeDefaultSerializer
def get_queryset(self):
# This method return serializer class
# which we pass in class method of serializer class
# which is also return by get_serializer()
q = super(TravelViewset, self).get_queryset()
serializer = self.get_serializer()
return serializer.get_related_queries(q)
Updatable nested serializers
Nested serializers by default don’t support create and update. To support this without duplicating DRF create/update logic, it is important to remove the nested data from validated_data
before delegating to super
:
# an ordinary serializer
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ('phone', 'company')
class UserSerializer(serializers.ModelSerializer):
# nest the profile inside the user serializer
profile = UserProfileSerializer()
class Meta:
model = UserModel
fields = ('pk', 'username', 'email', 'first_name', 'last_name')
read_only_fields = ('email', )
def update(self, instance, validated_data):
nested_serializer = self.fields['profile']
nested_instance = instance.profile
# note the data is `pop`ed
nested_data = validated_data.pop('profile')
nested_serializer.update(nested_instance, nested_data)
# this will not throw an exception,
# as `profile` is not part of `validated_data`
return super(UserDetailsSerializer, self).update(instance, validated_data)
In the case of many=True
, Django will complain that ListSerializer
does not support update
. In that case, you have to handle the list semantics yourself, but can still delegate to nested_serializer.child
.
Order of Serializer Validation
In DRF, serializer validation is run in a specific, undocumented order
- Field deserialization called (
serializer.to_internal_value
andfield.run_validators
) serializer.validate_[field]
is called for each field.- Serializer-level validators are called (
serializer.run_validation
followed byserializer.run_validators
) - Finally,
serializer.validate
is called to complete validation.
Getting list of all related children objects in parent’s serializer
Assume that, we implement a simple API and we have the following models.
class Parent(models.Model):
name = models.CharField(max_length=50)
class Child(models.Model):
parent = models.ForeignKey(Parent)
child_name = models.CharField(max_length=80)
And we want to return a response when a particular parent
is retrieved via API.
{
'url': 'https://dummyapidomain.com/parents/1/',
'id': '1',
'name': 'Dummy Parent Name',
'children': [{
'id': 1,
'child_name': 'Dummy Children I'
},
{
'id': 2,
'child_name': 'Dummy Children II'
},
{
'id': 3,
'child_name': 'Dummy Children III'
},
...
],
}
For this purpose, we implement the corresponding serializers like this:
class ChildSerializer(serializers.HyperlinkedModelSerializer):
parent_id = serializers.PrimaryKeyRelatedField(queryset=Parent.objects.all(),source='parent.id')
class Meta:
model = Child
fields = ('url','id','child_name','parent_id')
def create(self, validated_data):
subject = Child.objects.create(parent=validated_data['parent']['id'], child_name=validated_data['child_name'])
return child
class ParentSerializer(serializers.HyperlinkedModelSerializer):
children = ChildSerializer(many=True, read_only=True)
class Meta:
model = Course
fields = ('url','id','name','children')
To make this implementation work properly we need to update our Child
model and add a related_name to parent
field. Updated version of our Child
model implementation should be like this:
class Child(models.Model):
parent = models.ForeignKey(Parent, related_name='children') # <--- Add related_name here
child_name = models.CharField(max_length=80)
By doing this, we’ll be able to get the list of all related children objects in parent’s serializer.