Optimizing djangos count() method - python

So I have this queryset:
from django.contrib.gis.db.models.query import GeoQuerySet
from django.db import models as base_models
class RestaurantsQuerySet(GeoQuerySet):
def get_list(self, lng, lat):
reference_point = Point(lng, lat, srid=SRID)
return self.annotate(rating=models.Avg('comments__rating'))\
.annotate(distance=Distance('location', reference_point))
def count(self):
return self.values('id').aggregate(count=base_models.Count('id'))['count']
I thought that the query would look something like:
SELECT COUNT("__col1")
FROM (
SELECT "restaurants_restaurant"."id" AS "__col1"
FROM "restaurants_restaurant"
GROUP BY "restaurants_restaurant"."id") subquery
And instead django orm creates this little monstrosity:
SELECT COUNT("__col1")
FROM (
SELECT "restaurants_restaurant"."id" AS Col1, "restaurants_restaurant"."id" AS "__col1"
FROM "restaurants_restaurant"
LEFT OUTER JOIN "comments_comment" ON ("restaurants_restaurant"."id" = "comments_comment"."restaurant_id")
GROUP BY "restaurants_restaurant"."id", ST_Distance_Sphere("restaurants_restaurant"."location",
ST_GeomFromEWKB('\x0101000020e61000003eb555a41d2d4b405a338d81d0a73240'::bytea
))) subquery
First method to be called is get_list. It looks as if django would "remember" that call and that the qs was annotated with rating and distance and places it also into the count query. So I guess the question is - how do I "reset" this queryset to the state before annotating it?
EDIT:
Okay, seems my question was not complete. I also have a RestaurantsList view defined as follows:
class RestaurantList(generics.ListAPIView):
def get_queryset(self):
return Restaurant.objects.get_list(self._lng, self._lat)
I took a look into the entrails of django-rest-framework and I can see this:
class ListModelMixin(object):
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
So it looks like it always uses the queryset that was returned from the get_queryset method and since it's annotated with distance and rating it ends up being included in the count query. Still no solution to this...

After browsing through django-rest-framework code I came up with this idea:
Override default Paginator class for a given view:
class RestaurantList(generics.ListAPIView):
pagination_class = custom.LimitOffsetPagination
Create custom pagination class:
class LimitOffsetPagination(pagination.LimitOffsetPagination):
def __init__(self):
self._countable_queryset = None
self._was_counted = False
def was_initialized(self):
return self._countable_queryset is not None
def set_raw_queryset_for_count(self, queryset: QuerySet):
self._countable_queryset = queryset
def paginate_queryset(self, queryset, request, view=None):
self.limit = self.get_limit(request)
if self.limit is None:
return None
self.offset = self.get_offset(request)
self.count = self._countable_queryset.count()
self.request = request
if self.count > self.limit and self.template is not None:
self.display_page_controls = True
return list(queryset[self.offset:self.offset + self.limit])
Override views paginator property:
#property
def paginator(self):
paginator = super().paginator
if not paginator.was_initialized():
paginator.set_raw_queryset_for_count(Restaurant.objects.all())
return paginator
Now the count query will look a bit more friendly
SELECT COUNT(*) AS "__count" FROM "restaurants_restaurant"

Related

Django filter by two parameters

I want to filter by two parameters. I have this:
def get_queryset(self):
queryset = Product.objects.filter(
Q(category__in=self.request.GET.getlist("category")) &
Q(brand__in=self.request.GET.getlist("brand")))
return queryset
But this works only if two filters are chosen(otherwise it returns nothing). I want my program to filter by one filter if only one is chosen and filter by two parameters if two are chosen. How can I do that?
def get_queryset(self):
queryset = Product.objects.all()
if "category" in self.request.GET:
queryset = queryset.filter(category__in=self.request.GET.getlist("category"))
if "brand" in self.request.GET:
queryset = queryset.filter(brand__in=self.request.GET.getlist("brand"))
return queryset
Most methods applied to a queryset will return a new queryset. This allows you to add more parameters as required. As the queryset is lazy, it doesn't actually query the database until you start consuming data so there's no performance overhead to the above either.
You can do this.
def get_queryset(self):
kwargs = {}
if self.request.GET.getlist("category"):
kwargs["category__in"] = self.request.GET.getlist("category")
if self.request.GET.getlist("brand"):
kwargs["brand__in"] = self.request.GET.getlist("brand")
return Product.objects.filter(**kwargs)
You can actually write model manager for custom query_sets. For more information you can read the documentation: https://docs.djangoproject.com/en/3.0/topics/db/managers/#calling-custom-queryset-methods-from-the-manager
class ProductQuerySet(models.query.QuerySet):
def active(self):
return self.filter(active=True)
def search(self, query):
lookups = (Q(title__icontains=query) |
Q(description__icontains=query) |
Q(price__icontains=query) |
Q(tag__title__icontains=query)) # (model_name)__(model_field)__(filter)
return self.filter(lookups).distinct()
class ProductManager(models.Manager):
def get_queryset(self):
return ProductQuerySet(self.model, using=self._db)
def active(self):
return self.get_queryset().active()
def search(self, query):
return self.get_queryset().active().search(query)
In Views.py:
def get_queryset(self, *args, **kwargs):
request = self.request
query = request.GET.get('q', None)
if query is not None:
query = query.strip()
return Product.objects.search(query)
return Product.objects.featured()
I hope that will help you. Good Luck

Combine ListModelMixin with APIView to show pagination

I want to show the pagination feature in my API and I am using APIView with multiple serializers.
I know it is very easy to show pagination with ListView.
I have seen somewhere that combining ListModelMixin and APIView works but if my code is as follows:
class ListModelMixin(object):
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serilaizer.data)
class ItemsAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get(self, request, format=None):
"""
Return a list of all devices of this user.
"""
reply = {}
try:
products = BaseItem.objects.owned_items().filter(owner=request.user)
reply['data'] = OwnedItemSerializer(products, many=True).data
items = BaseItem.objects.dev_items().filter(owner=request.user)
reply['data'].extend(ItemSerializer(items, many=True).data)
except:
reply['data'] = []
return Response(reply, status.HTTP_200_OK)
How can I combine them so I can get paginated results?
Thanks in advance!
First things first, what you are currently doing is too complex without reason.
In order to achieve a "paginatable" queryset, it is preferred to change your owned_items() and dev_items() in simple filter combinations, rather than model methods. To clarify by example:
products = BaseItem.objects.filter(owner=request.user, owned=True)
instead of
products = BaseItem.objects.owned_items().filter(owner=request.user)
That way, you can produce a single queryset which will be easier to paginate:
user_items = BaseItem.objects.filter(
Q(owner=request.user, owned=True) |
Q(owner=request.user, dev=True)
)
Note 1: You can simplify things further if you like, but that gets out of scope of your question. As food for thought, check this out:
user_items = BaseItem.objects.filter(owner=request.user).distinct()
Note 2: You should use a single serializer for a single model because what you are doing adds complexity without benefit (high risk-low reward situation)
With the above mentioned and assumed:
There are some ways to achieve what you want here:
By utilizing GeneriAPIView and ListModelMixin you can refactor your class in such a way to have a .list() method with auto-pagination:
from rest_framework import mixins, generics
class ItemsAPIView(mixins.ListModelMixin, generics.GenericAPIView,):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
serializer_class = OwnedItemSerializer
# You can define .get in here if you really need it.
# You can also override .list to add specific functionality
If you don't want to use the above, and you want to keep your APIView, then you can keep your get method and provide pagination for it as mentioned in this Q&A example:
class ItemsAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
serializer_class = MyNewUnifiedSerializerClass
def get(self, request):
user_items = BaseItem.objects.filter(
owner=request.user
).distinct()
page = self.paginate_queryset(user_items)
if page is not None:
serializer = self.serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(user_items, many=True)
return Response(serializer.data)
# Now add the pagination handlers taken from
# django-rest-framework/rest_framework/generics.py
#property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator
def paginate_queryset(self, queryset):
"""
Return a single page of results,
or `None` if pagination is disabled.
"""
if self.paginator is None:
return None
return self.paginator.paginate_queryset(
queryset,
self.request,
view=self
)
def get_paginated_response(self, data):
"""
Return a paginated style `Response` object
for the given output data.
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)

use GenericAPIView for different seriailizers based on different condition

Due to the use of different serializers based on certain condition, i preferred to use APIView and override get function. I was content with APIView but now that i need pagination feature, I am having trouble to make it happen. That is why i want to switch to GenericAPIView but due to the use of multiple serializer I have no idea how can i do it.
class ItemsAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get(self, request, format=None):
"""
Return a list of all devices of this user.
"""
reply = {}
try:
products = BaseItem.objects.owned_items().filter(owner=request.user)
reply['data'] = OwnedItemSerializer(products, many=True).data
items = BaseItem.objects.dev_items().filter(owner=request.user)
reply['data'].extend(ItemSerializer(items, many=True).data)
except:
reply['data'] = []
return Response(reply, status.HTTP_200_OK)
UPDATE
Another way i tried is
class ItemsAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get(self, request, format=None):
"""
Return a list of all items with product of this user.
"""
reply = {}
print ('request', request.META.get('REMOTE_ADDR'))
try:
products = BaseItem.objects.owned_items().filter(owner=request.user)
reply['data'] = OwnedItemSerializer(products, many=True).data
items = BaseItem.objects.dev_items().filter(owner=request.user)
page = self.paginate_queryset(items)
print ('page', page) # i always get None even when pass url as api/items?page=1
if page is not None:
reply['data'].extend(ItemSerializer(page, many=True).data)
reply['data'].extend(ItemSerializer(items, many=True).data)
except:
reply['data'] = []
return Response(reply, status.HTTP_200_OK)
#property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
print (hasattr(self, '_paginator'))
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator
def paginate_queryset(self, queryset):
"""
Return a single page of results, or `None` if pagination is disabled.
"""
print ('queryset', queryset)
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)
def get_paginated_response(self, data):
"""
Return a paginated style `Response` object for the given output data.
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
No any way is working. Where have i done mistake?
Do you really need two serializers ?
I think it may be a better choice to use a single Serializer with a custom to_representation:
class ItemSerializer(ModelSerializer):
# Your fields
def to_representation(self, instance):
data = super(ItemSerializer, self).to_representation(instance)
request = self.context.get('request')
if request and instance.is_owned_by(request.user):
return self.owner_to_representation(data, instance) # TO IMPLEMENT
return data
Then, you can use a generic view. Your code is cleaner, simpler and you do not have to worry about the pagination:
class ItemList(generics.ListAPIView):
serializer_class = ItemSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self):
return BaseItem.objects.owned_items()| BaseItem.objects.dev_items()
This is as simple as importing your paginator, and calling it manually in the APIView.
class PollView(views.APIView):
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication,)
paginator = CustomPagination()
def get(self, request):
queryset = Poll.objects.all()
context = self.paginator.paginate_queryset(queryset, request)
serializer = PollSerializer(context, many=True)
return self.paginator.get_paginated_response(serializer.data)
NOTE: Custom class is not necessary, you can simply import from rest_framework.pagination at the top of your script. I created a CustomPagination class, inheriting from PageNumberPagination, so that I could set the page_size query, as per docs - http://www.django-rest-framework.org/api-guide/pagination/

Get max value from django rest

I am using django rest framework and instead of getting the complete list of an object, I only want to get a specific value, like max(date) for example. Here is the code I am using:
My Serializer
class MoodSerializer(serializers.ModelSerializer):
class Meta:
model = Mood
fields = ('date', 'rating')
def create(self, validated_data):
return Mood.objects.create(**validated_data)
My View
class MoodList(generics.ListCreateAPIView):
queryset = Mood.objects.all()
serializer_class = MoodSerializer
class MoodDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Mood.objects.all()
serializer_class = MoodSerializer
My URLS
url(r'^mood/$', views.MoodList.as_view()),
url(r'^mood/(?P<pk>[0-9]+)/$', views.MoodDetail.as_view()),
So if fire a GET on "max_mood" I want the latest Mood entry from the db.
Instead of Mood.objects.all(), why not sort Mood objects by some_criteria and get the first one (which will be the max one). Like this:
mood = Mood.objects.order_by('-some_criteria').first()
There is another way but it requires additional queries:
from django.db.models import Max
maximum = Mood.objects.aggregate(m=Max('some_criteria')).get('m')
max_mood = Mood.objects.get(some_criteria=maximum)
view
class MaxMoodView(generics.RetrieveAPIView):
queryset = Mood.objects.all()
serializer_class = MoodSerializer
def retrieve(self, request, *args, **kwargs):
mood=self.get_queryset().order_by('-date').first()
return Response(self.get_serializer(instance=mood))
urls
url(r'^mood/max_mood/$', views.MaxMoodView.as_view()),
Alright I got it working with the help of the comments:
view:
class MaxMoodView(generics.ListCreateAPIView):
queryset = Mood.objects.all()
serializer_class = MoodSerializer
def get(self, request, format=None):
mood=self.get_queryset().order_by('-date').first()
serializer = MoodSerializer(mood)
return Response(serializer.data)
urls:
url(r'^mood/max_mood/$', views.MaxMoodView.as_view()),

Bulk set properties to django queryset

I have model like this:
class MyModel(models.Model):
#fields
def __init__(self, *args, **kwargs):
self._issue_status = None
super(...).__init__(...)
#property
def issue_status(self):
if self._issue_status is None:
self.get_issue_status()
return self._issue_status
where get_issue_status goes to api and take status. But api can bulk return statuses and i want to use it when get queryset of MyModel.
So i write own manager:
class MyModelManager(models.Manager):
def get_queryset(self):
queryset = super(...).get_queryset()
issues = self.bulk_get_issues()
set_issue_statuses(queryset)
return queryset
It works nice for getting all objects. But when i try to filter queryset - status property for every object is None. How can i fix that? Thank.
I think you want to override your get_queryset function,
something like:
def get_queryset(self, request):
qs = super(...).get_queryset(request)
the_filter = ["None", "", "SKIPPED", ]
return qs.exclude(status__in=the_filter)

Categories

Resources