Using selected_related() in nested serializers - django

I've been using select_related() to speed up a large DRF call with great success, but I've hit a wall.
My main serializer references two other serializers, and one of those references yet another serializer. I'm unsure as how to implement prefetching in the second level serializer.
serializer.py
class DocumentsThinSerializer(serializers.ModelSerializer):
class Meta:
model = Documents
fields = ('confirmed', )
class PersonThinSerializer(serializers.ModelSerializer):
documents = DocumentsThinSerializer()
class Meta:
model = Person
fields = ('name', 'age', 'gender')
class EventThinSerializer(serializers.ModelSerializer):
day = DayThinSerializer()
person = PersonThinSerializer()
#staticmethod
def setup_eager_loading(queryset):
return queryset.select_related('day', 'person')
class Meta:
model = Event
views.py
class EventList(generics.ListAPIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
queryset = Event.objects.all()
serializer_class = EventThinSerializer
def get_queryset(self):
return self.get_serializer_class().setup_eager_loading(queryset)
As you can see, I'm using the static method setup_eager_loading() to get things going, but I can't find a queryset hook for my PersonThinSerializer() to get the speedup when accessing the DocumentsThinSerializer() in the same way.

Assuming Documents has a foreign key to Person, you should be able to add "person__documents" to your queryset.select_related in EventThinSerializer.setup_eager_loading:
class EventThinSerializer(serializers.ModelSerializer):
day = DayThinSerializer()
person = PersonThinSerializer()
#staticmethod
def setup_eager_loading(queryset):
return queryset.select_related('day', 'person', 'person__documents')

Related

Add a field to DRF generic list view

I need to create a DRF list view that shows each course along with a boolean field signifying whether the user requesting the view is subscribed to the course.
Course subscriptions are stored in the following model:
class Subscription(models.Model):
user = models.ForeignKey(
CustomUser, related_name='subscriptions', null=False,
on_delete=CASCADE)
course = models.ForeignKey(
Course, related_name='subscriptions', null=False,
on_delete=CASCADE)
class Meta:
ordering = ['course', 'user']
unique_together = [['user', 'course']]
This is the view I am writing:
class CourseListView(generics.ListAPIView):
permission_classes = [IsAuthenticated, ]
queryset = Course.objects.all()
serializer_class = CourseSerializer
def isSubscribed(self, request, course):
sub = Subscription.objects.filter(
user=request.user, course=course).first()
return True if sub else False
def list(self, request, format=None):
queryset = Course.objects.all()
serializer = CourseSerializer(queryset, many=True)
return Response(serializer.data)
I am looking for a way to modify the list method, so as to add to the response the information about whether request.user is subscribed to each of the courses.
The best solution I have for now is to construct serializer manually, which would look (at the level of pseudo-code) something like this:
serializer = []
for course in querySet:
course['sub'] = self.isSubscribed(request, course)
serializer.append(CourseSerializer(course))
I suspect there should be a better (standard, idiomatic, less convoluted) way for adding a custom field in a list view, but could not find it. In addition, I am wondering whether it is possible to avoid a database hit for every course.
You can do that easily with Exists:
just change your queryset in your view:
from django.db.models import Exists, OuterRef
class CourseListView(generics.ListAPIView):
permission_classes = [IsAuthenticated, ]
serializer_class = CourseSerializer
def get_queryset(self):
subquery = Subscription.objects.filter(user=request.user, course=OuterRef('id'))
return Course.objects.annotate(sub=Exists(subquery))
and add a field for it in your serializer:
class CourseSerializer(serializers.ModelSerializer):
sub = serializers.BooleanField(read_only=True)
class Meta:
model = Course
fields = '__all__'

No data is being returned when I use an inherited Django Rest Framework serializer? It just returns an empty dictionary

I have a serializer that is meant to act as a template for other ModelSerializers.
class CountryBasedModelSerializer(ModelSerializer):
def __init__(self, data, context):
assert 'country' in self.Meta.fields
class Meta:
model = Country
fields = ()
I want to use it with this, which is the actual serializer which will be called.
class CountryBasedProjectSerializer(CountryBasedModelSerializer):
class Meta:
model = Project
fields = ('id', 'country', 'name')
I want to use it with this inherited viewset:
class CountryBasedViewset(viewsets.ModelViewSet):
queryset = None
serializer_class = CountryBasedModelSerializer
def get_queryset(self):
return self.queryset.filter(country_pk=self.request.data["country"])
And this is the actual viewset that will be called:
class CountryProjectBasedViewset(CountryBasedViewset):
queryset = Project.objects.all()
Is there anything that I am clearly doing incorrectly?
Just define the serializer_class for CounteryProjectBasedViewSet as below
class CountryProjectBasedViewset(CountryBasedViewset):
queryset = Project.objects.all()
serializer_class = CountryBasedProjectSerializer

Django DRF serializer method field on many to many running 2n queries

I'm using Django 2.2 and Django REST Framework.
I have the following model structure
class MyModel(models.Model):
name = models.ChartField(max_length=200)
class Tag(models.Model):
name = models.ChartField(max_length=50, unique=True)
class MyModelRelation(models.Model):
obj = models.ForeignKey(MyModel, related_name='relation')
user = models.ForeignKey(User)
tags = models.ManyToManyField(Tag)
def tag_list(self):
return self.tags.all().values_list('name', flat=True).distinct()
I want to get the tags list with the MyModel instance and for that, the serializer is
class MyModelSerializer(serializers.ModelSerializer):
tags_list = serializers.SerializerMethodField(read_only=True)
def get_tags_list(self, obj):
return obj.relation.tag_list()
class Meta:
fields = [
'name',
'tags_list'
]
and the view is
class ObjListView(ListAPIView):
serializer_class = MyModelSerializer
def get_queryset(self):
return super().get_queryset().select_related('relation').prefetch_related('relation__tags')
But to get 58 records, it is running almost 109 queries.
The my_app_mymodel`, `my_app_mymodelrelation_tags is repeated multiple times
This is how I suggest you solve the problem. Instead of extracting the name in the DB level, you can do it in the serializer level. It will make things way easier and faster. First, remove the tag_list method from the model class. First add the annotation to your views:
from django.db.models import F
def get_queryset(self):
return super().get_queryset().annotate(tags_list=F('relation__tags')).select_related('relation')
Then in your serializers
class MyModelSerializer(serializers.ModelSerializer):
tags_list = serializers.SlugRelatedField(many=True, slug_field='name', read_only=True)
...

Django DRF More Detail After Fetching Object

New to DRF (and REST in general).
Models:
class Event(models.Model):
name = models.CharField()
class Result(models.Model):
event = models.ForeignKey(Event, related_name='results')
person = models.CharField()
score = models.IntegerField()
Serializers:
class ResultSerializer(serializers.ModelSerializer):
class Meta:
model = Result
class EventSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Event
Viewsets:
class ResultViewSet(viewsets.ModelViewSet):
queryset = Result.objects.all()
serializer_class = ResultSerializer
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
Router:
router = routers.DefaultRouter()
router.register(r'events', EventViewSet)
URL:
urlpatterns = [
url(r'^api/', include(router.urls)),
]
This works fine, I can go to http://mysite/api and see "events" with a link to http://mysite/api/events/. From there each event has a link to http://mysite/api/events/id. So far so good.
If I change the event serializer to this, it will also include all the results (for that event) from Result:
class EventSerializer(serializers.HyperlinkedModelSerializer):
results = ResultSerializer(many=True, read_only=True)
class Meta:
model = Event
This also works fine. But I don't want the (often very long) results to be included for each event at http://mysite/api/events. There are way too many. I'd only like to see results included when I go to http://mysite/api/events/id.
Any tips on how I can get from where I am to where I want to be? It would be even better if each item on http://mysite/api/events included a count of the results, then http://mysite/api/events/id actually had the results.
Hope this made sense.
Thanks.
We can create 2 serializers EventSerializer and EventDetailSerializer which will return different serialized representations based on the type of request.
class EventSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Event
class EventDetailSerializer(serializers.HyperlinkedModelSerializer):
results = ResultSerializer(many=True, read_only=True) # contain the serialized results
class Meta:
model = Event
Then we will override the get_serializer_class() of EventViewSet which return EventDetailSerializer in case of retrieve requests and EventSerializer otherwise.
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
def get_serializer_class(self):
if self.action == 'retrieve': # check if a 'retrieve' request
return EventDetailSerializer
return EventSerializer # otherwise return this serializer

Aggregate (and other annotated) fields in Django Rest Framework serializers

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.
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.
You can hack the ModelSerializer constructor to modify the queryset it's passed by a view or viewset.
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=...,
)
There is no significance in the name _build_queryset (it's not overriding anything), it just allows us to keep the bloat out of the constructor.