Optimizing queries in SerializerMethodField in Django REST framework - django

I am accessing the related field's data in my SerializerMethodField and there is a query for every object that is being rendered. My models look like (will keep short for brevity):
class Listing(models.Model):
variant = models.ForeignKey(to='Variant', related_name='variant_listings')
seller = models.ForeignKey(to='user.Seller', related_name='seller_listings')
locality = models.ForeignKey(to='user.Locality', blank=True, null=True)
price = models.IntegerField(blank=True, null=True)
Variant, Seller and Locality are all related models.
My Viewset:
class ListingViewSet(viewsets.ModelViewSet):
"""Viewset class for Listing model"""
queryset = Listing.objects.all()
serializer_class = ListingSerializer
pagination_class = TbPagination
filter_backends = (filters.DjangoFilterBackend,)
filter_class = ListingFilter
def get_queryset(self):
listing_qs = Listing.objects.filter(status='active')
listing_qs = ListingSerializer.setup_eager_loading(listing_qs)
return listing_qs
And my serializer:
class ListingSerializer(serializers.ModelSerializer):
"""Serializer class for Listing model"""
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related('variant', 'seller', 'locality')
return queryset
#staticmethod
def get_car_link(obj):
variant_name_slug = obj.variant.name.replace(' ', '-').replace('+', '')
return '/buy-' + obj.seller.city.name.lower() + '/' + variant_name_slug
car_link = serializers.SerializerMethodField(read_only=True)
#staticmethod
def get_car_info(obj):
return {
'id': obj.id,
'variant_name': obj.variant.name,
'localities': obj.locality.name,
}
car_info = serializers.SerializerMethodField(read_only=True)
#staticmethod
def get_image_urls(obj):
caption_ids = [1, 2, 3, 5, 7, 8, 18]
attachments_qs = Attachment.objects.filter(listing_id=obj.id, caption_id__in=caption_ids)
image_urls = []
for attachment in attachments_qs:
url = str(obj.id) + '-' + str(attachment.file_number) + '-360.jpg'
image_urls.append(url)
return image_urls
image_urls = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Listing
fields = ('car_link', 'car_info', 'sort_by', 'image_urls')
For each listing returned by the listing viewset, there is a query for every related field accessed in the SerializerMethodField.
I found some related questions like this. But that didn't help. Also, I tried doing prefetch_related on my get_queryset method of the viewset and also implemented eager loading with the help of this article. But nothing helped.
Is there any way to avoid these queries?
Edit
The get_car_info function written above, contains a few more fields (along with the ones already present) which are required separately in a nested JSON by the name of car_info in the final serialized data that is being rendered at the front end.

I have used this article:
http://ses4j.github.io/2015/11/23/optimizing-slow-django-rest-framework-performance/
I created a set up eager loading method in my serializer, like so:
class EagerGetProjectSerializer(serializers.ModelSerializer):
lead_researcher = UserSerializer()
participating_researcher = UserSerializer(many=True)
client = ProjectClientSerializer()
test_items = TestItemSerializer(many=True)
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('lead_researcher', 'client')
queryset = queryset.prefetch_related('participating_researcher',
'test_items')
return queryset
class Meta:
model = Project
fields = '__all__'
Notice that when referencing the objects you want to pull in the serializer you have to use the foreign key related name attribute.
and called in my view, before accessing the serializer:
class SingleProject(APIView):
def get(self, request):
ans = Project.objects.filter(id=project_id)
qs = EagerGetProjectSerializer.setup_eager_loading(ans)
serializer = EagerGetProjectSerializer(qs, many=True)

Related

How to get serializer data before response in DRF ListAPIView to order it by all fields?

I have two models.
class Team(models.Model):
user = models.ManyToManyField(User, related_name='%(class)s_user')
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='%(class)s_company')
is_active = models.BooleanField(default=True)
has_user = models.BooleanField(default=False)
class Meta:
app_label = 'accounts'
class TeamTranslation(models.Model):
team_name = models.CharField(max_length=100)
team_description = models.TextField()
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name='%(class)s_team')
languages = models.ForeignKey(Languages, on_delete=models.CASCADE, related_name='%(class)s_languages')
is_active = models.BooleanField(default=True)
This is list view...
class teamListView(generics.ListAPIView):
search_fields = ['teamtranslation_team__team_name']
filter_backends = (filters.SearchFilter,)
serializer_class = teamSerializer
pagination_class = StandardResultsSetPagination
def get_queryset(self):
accessor_id = self.request.user.id
queryset = Team.objects.filter(is_active=True).order_by("id")
return queryset
def post(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def get_serializer_context(self):
lang_id = UserProfile.objects.get(user_id=self.request.user.id).languages_id
return {"lang_id": lang_id}
And this is serializer...
class teamSerializer(serializers.ModelSerializer):
team_id = serializers.IntegerField(source='id')
members_count = serializers.SerializerMethodField()
team_description = serializers.SerializerMethodField()
team_name = serializers.SerializerMethodField()
company_code = serializers.CharField(source='company.company_code')
def get_team_description(self, obj):
lang_id = self.context.get('lang_id')
if TeamTranslation.objects.filter(team_id=obj.id, languages_id=lang_id, is_active=True):
return f'{TeamTranslation.objects.get(team_id=obj.id, languages_id=lang_id, is_active=True).team_description}'
return f'{""}'
def get_team_name(self, obj):
lang_id = self.context.get('lang_id')
if TeamTranslation.objects.filter(team_id=obj.id, languages_id=lang_id, is_active=True):
return f'{TeamTranslation.objects.get(team_id=obj.id, languages_id=lang_id, is_active=True).team_name}'
return f'{""}'
def get_members_count(self, obj):
return len(obj.user.values_list('id', flat=True))
class Meta:
model = Team
fields = ("team_id", "team_name", "team_description", 'members_count', 'company_id', 'company_code', 'has_user')
I try to order my data according to query parameter comes from request. So there is no problem when use order_by clause for Team model fields. The problem starts when try to order TeamTranslation fields.
I want to order my serializer data before ListAPIView response it. Is there any way to do it?
I try using order_by clause to do that for example.
queryset = queryset.order_by("teamtranslation_team__team_name")
or for reverse I use - sign. It works but I want to query parameter comes with field name that I describe in serializer fields. Because of that I think the best way can be order data in serializer data but I can't reach it. Order data by members count is also another problem.
Thanks for your help.
You can look into OrderingFilter which allows you to order the queryset. Like this:
class teamListView(generics.ListAPIView):
search_fields = ['teamtranslation_team__team_name']
ordering_fields = ['id', 'teamtranslation_team__team_name']
ordering = ['id']
filter_backends = (filters.SearchFilter,filters.OrderingFilter)
serializer_class = teamSerializer
pagination_class = StandardResultsSetPagination
queryset = Team.objects.filter(is_active=True)
# no need to override get_queryset method
update
Another efficient of doing this:
from django.db.models import OuterRef, Subquery, Count
class teamListView(generics.ListAPIView):
search_fields = ['team_name']
ordering_fields = ['id', 'team_name']
filter_backends = (filters.SearchFilter,filters.OrderingFilter)
serializer_class = teamSerializer
pagination_class = StandardResultsSetPagination
def get_queryset(self):
queryset = Team.objects.filter(is_active=True)
subquery = TeamTranslation.objects.filter(team=OuterRef('pk'), is_active=True)
queryset = queryset.annotate(team_name=Subquery(subquery.values('team_name')[:1]), team_description=Subquery(subquery.values('team_description')[:1]), member_count=Count('user'))
return queryset
Then you can clean up your serializers like this:
class teamSerializer(serializers.ModelSerializer):
team_id = serializers.IntegerField(source='id')
members_count = serializers.IntegerField(source='member_count')
team_description = serializers.CharField(source="team_description")
team_name = serializers.CharField(source="team_name")
company_code = serializers.CharField(source='company.company_code')
Here I am annotating subquery of TeamTranslation with the Team. I am also annotating user count through Count expression. With this implementation, I am reducing DB calls, hence making it efficient.
FYI: Please use PEP-8 guidelines for naming conventions in Python.

Django Rest Framework Limiting get_queryset result to not include all fields

view.py
class charity_totals(generics.ListAPIView):
serializer_class= CharityTotalSerializer
queryset=Transaction.objects.all()
def get_queryset(self):
queryset = super().get_queryset()
user_id = self.request.GET.get('userID')
if user_id is None:
return queryset
queryset = queryset.filter(userID=user_id)
return queryset.values('charityID').annotate(total_donation=Sum('transactionAmount'))
serializer.py
class CharityTotalSerializer(ModelSerializer):
charity_name= serializer.ReadOnlyField(source='charityID.charityName')
total_donation= serializer.DecimalField(max_digits=64,decimal_places=2)
class Meta:
model = Transaction
fields = ['charity_name','total_donation']
model
class Transaction(models.Model):
transactionAmount = models.DecimalField(max_digits=6, decimal_places=2)
userID = models.ForeignKey(User,on_delete=models.CASCADE)
charityID = models.ForeignKey(Charity,on_delete=models.CASCADE, related_name='charity_set')
processed = models.BooleanField(auto_created=True, default=False)
transactionDate = models.DateField(auto_now_add=True)
Off of a request such as this http://localhost:8000/requests/charitytotal/?userID=1 my json response is limited to just the [{"total_donation":"3.00"},{"total_donation":"17.00"}] and is not including the charity names that are specified in the serializer. From what I understand the .values should return a dict of both the charityID and the total_donation that was specified which should be able to interact with my serializer. Any Insight would be appreciated
That is because you are returning Values queryset from get_queryset method when you are getting the value of userID in request.GET. Also I assume it is important for you return that way so that you can group by and sum values of total donation. So, I think you can approach something like this:
First, change get_queryset method to return the name of the charity annotated:
from django.db.models import F, Sum
...
def get_queryset(self):
queryset = super().get_queryset()
user_id = self.request.GET.get('userID')
if user_id is not None:
queryset = queryset.filter(userID=user_id)
return queryset.values('charityID').annotate(total_donation=Sum('transactionAmount')).annotate(charity_name=F('charityID__charityName'))
Then update the serializer like this:
class CharityTotalSerializer(ModelSerializer):
charity_name= serializer.ReadOnlyField() # no need to define source
total_donation= serializer.DecimalField(max_digits=64,decimal_places=2)
class Meta:
model = Transaction
fields = ['charity_name','total_donation']
Also, better to have charityName unique(using unique=True in models), so that it does not produce confusing results.
You have to put it into Serializer and it will work fine.
charity_name = serializer.SerializerMethodField()
def get_charity_name(self, instance):
return instance.charityID.name

Annotated django queryset is getting ignored during serialization

I have a ModelViewSet where I want to annotate the list() response. I've extended the queryset with an annotation and added the field to the serializer, but the serializer just ignores the new data and doesn't add the field at all in the final data.
I am using a customized get_queryset() too (show abbreviated here) which is definitely getting called and producing the right annotations. It just doesn't show up in the REST API response.
If I set default=None on the serializer field definition, it appears in the response.
class SequenceSerializer(serializers.ModelSerializer):
unread=serializers.IntegerField(read_only=True)
.....
class SequenceViewSet(viewsets.ModelViewSet,ScopedProtectedResourceView):
authentication_classes = [OAuth2Authentication]
queryset = Sequence.objects.all()
serializer_class = SequenceSerializer
.....
def get_queryset(self):
queryset = Sequence.objects.all().filter(<..... some filter>)
queryset = queryset.annotate(unread=FilteredRelation('unreadseq',
condition=Q(unreadseq__userid=self.request.user)))
print("Seq with unread",queryset.values('id','unread')) ## <<<<this shows the correct data
return queryset
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data) ##<<< this is missing the annotation
I have been banging my head against this all day and I can't for the life of me see what's going wrong.
Any ideas please?
--
more info:
class UnreadSeq(models.Model):
userid = models.ForeignKey('auth.User', on_delete=models.CASCADE)
seqid = models.ForeignKey(Sequence, on_delete=models.CASCADE)
class Meta:
unique_together=('seqid','userid')
verbose_name = "UnreadSeq"
verbose_name_plural = "UnreadSeqs"
class Sequence(models.Model):
userid = models.ForeignKey('auth.User', on_delete=models.SET_NULL,null=True)
topic = models.ForeignKey('Topic',on_delete=models.CASCADE,null=False,blank=False)
.....
class Meta:
verbose_name = "Sequence"
verbose_name_plural = "Sequences"
I think that this annotation don't return Integer. Try to annotate (you want to COUNT unreadseq) like this:
def get_queryset(self):
mytopics=getMyTopics(self.request,False)
queryset = Sequence.objects.all().filter(<..... some filter>)
count_unreadseq = Count('unreadseq', filter=Q(unreadseq__userid=self.request.user))
queryset=queryset.annotate(unread=count_unreadseq)
...
EDITED after comments to get unreadseq ids
def get_queryset(self):
mytopics=getMyTopics(self.request,False)
queryset = Sequence.objects.all().filter(<..... some filter>)
unreadseq_ids = UnreadSeq.objects.filter(seqid=OuterRef('pk'), userid=self.request.user).values('pk')
queryset=queryset.annotate(unread=Subquery(unreadseq_ids))
...
Also you need to edit serializer:
class SequenceSerializer(serializers.ModelSerializer):
unread=serializers.IntegerField(read_only=True)
.....

Django rest filter custom fields

I am converting a UNIX date to a string date and passing it as a custom read-only field. What would be the best way to use django-filter to be able to filter this custom field? The error I get is Cannot resolve keyword 'convert_time' into the field. Choices are:
Models class
class AccountT(models.Model):
created_date_t = models.BigIntegerField(blank=True, null=True)
def convert_time(self):
result = time.strftime("%D", time.localtime(self.created_date_t))
return result
Serializer Class
class AccountTSerializer(serializers.ModelSerializer):
created_date = serializers.ReadOnlyField(source='convert_time')
class Meta:
model = AccountT
fields = ('othermodelfield','othermodelfield', 'created_date',)
ListAPIView
class AccountTListView(generics.ListAPIView):
serializer_class = AccountTSerializer
queryset = AccountT.objects.all()
filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter,)
filter_fields = ('othermodelfield','created_date_t')
The filterset_fields option is a shortcut that inspects model fields (not serializer fields) in order to generate filters. Since created_date isn't a model field, you'll need to manually declare a filter on a filterset_class. Declared filters can take advantage of the method argument, which will allow you to transform the incoming date into your timestamp. Something like...
# filters.py
from django_filters import rest_framework as filters
class AccountTFilter(filters.FilterSet):
# could alternatively use IsoDateTimeFilter instead of assuming local time.
created_date = filters.DateTimeFilter(name='created_date_t', method='filter_timestamp')
class Meta:
model = models.AccountT
# 'filterset_fields' simply proxies the 'Meta.fields' option
# Also, it isn't necessary to include declared fields here
fields = ['othermodelfield']
def filter_timestamp(self, queryset, name, value):
# transform datetime into timestamp
value = ...
return queryset.filter(**{name: value})
# views.py
class AccountTListView(generics.ListAPIView):
filterset_class = filters.AccountTFilter
...
Note: The old filter_* options have since been renamed to filtserset_*.
class AccountT(models.Model):
created_date_t = models.BigIntegerField(blank=True, null=True)
created_date = models.DateField(null=True)
def save(self, *args, **kwargs):
self.created_date = self.convert_time()
return super(IncomeExpense, self).save(*args, **kwargs)
def convert_time(self):
result = time.strftime("%D", time.localtime(self.created_date_t))
return result
class AccountTListView(generics.ListAPIView):
serializer_class = AccountTSerializer
queryset = AccountT.objects.all()
filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter,)
filter_fields = ('othermodelfield','created_date')
class AccountTFilter(FilterSet):
class Meta:
model = AccountT
fields = {
'created_date': ['gte', 'lte'],
}
class AccountTListView(generics.ListAPIView):
serializer_class = AccountTSerializer
queryset = AccountT.objects.all()
filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter,)
filter_class = AccountTFilter
To achieve this I would create a custom field on the serializer (ConvertTimeField):
import time
from .models import AccountT
from rest_framework import serializers
# Custom serializer field
# https://www.django-rest-framework.org/api-guide/fields/#custom-fields
class ConvertTimeField(serializers.ReadOnlyField):
"""
Convert time Field.
"""
def to_representation(self, value):
return time.strftime("%D", time.localtime(value))
def to_internal_value(self, data):
"""
Here is where you could convert the incoming value
(It's only needed if you want to perform modification before writing to the database)
"""
# Serializer
class AccountTSerializer(serializers.ModelSerializer):
created_date = ConvertTimeField(source='created_date_t')
class Meta:
model = AccountT
fields = ('othermodelfield', 'othermodelfield', 'created_date')
NOTE: You still need to pass your filter argument in the same format i.e UNIX epoch when you filter. If you need to change that you can convert your filter query param as suggested here: https://www.django-rest-framework.org/api-guide/filtering/#filtering-against-query-parameters. (Although, there are also other ways you could try accomplish that)

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.