I'm trying to return the sum of fields from another model inside a Subquery.
My main queryset returns all users of type company. I have to return the total of credits used by taking the data from CreditOrder and Sum the credit_used field.
I'm using ClusterableModel and ParentalKey from django-modelcluster
My CreditOrder model
class CreditOrder(ClusterableModel):
credit = ParentalKey(
Credit, on_delete=models.CASCADE, related_name="credit_order"
)
order = ParentalKey(Order, on_delete=models.CASCADE, related_name="credit_order")
credit_used = models.DecimalField(
max_digits=12, decimal_places=2, null=True, blank=True
)
My User model
class User(AbstractUser, ClusterableModel):
username = models.CharField(max_length=40, null=True, blank=True)
user_type = models.CharField(max_length=20, choices=TIPO_UTENTE, default="dipendente")
My queryset using the class model User
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.filter(user_type='company')
credits_used_subquery = Subquery(CreditOrder.objects.filter(credit__font__company__id=OuterRef('id')).order_by()
.values('credit_used').annotate(credit_used_sum=Sum('credit_used'))
.values('credit_used_sum'), output_field=DecimalField())
qs = qs.annotate(
_credits_used_sum=credits_used_subquery
)
return qs
But this error is returning me:
django.db.utils.ProgrammingError: more than one row returned by a subquery used as an expression
If you just need to sum all the credits used by the company, you can do:
qs.annotate(_credits_used_sum=Sum('font__credit__credit_used'))
Without seeing all the models between CreditOrder and User, it's hard to tell exactly what you've got wrong. It looks like the Credit model is linked to Font and Font might have an attribute called Company which is a foreign key to the User model?
In any case, your first values call has the wrong argument, you need to group by the the essentially the same thing you are linked to in the outer ref. So I'd suggest
.values('credit__font__company__id')
In the first call to values. And keep the annotate and second call to values the same.
Another answer suggests doing the Sum with a join instead of a Subquery, if you like the simplicity of that api, but you still want to use a Subquery, you can use the django-sql-utils package. After you pip install django-sql-utils
from sql_util.utils import SubquerySum
qs.annotate(_credits_used_sum=SubquerySum('font_credit_credit_used')
I solved my problem with this class:
class SubquerySum(Subquery):
template = "(SELECT COALESCE(SUM(%(field)s), %(zero_value)s) FROM (%(subquery)s) _sum)"
def as_sql(self, compiler, connection, template=None, **extra_context):
if 'field' not in extra_context and 'field' not in self.extra:
if len(self.queryset._fields) > 1:
raise FieldError('You must provide the field name, or have a single column')
extra_context['field'] = self.queryset._fields[0]
if 'zero_value' not in extra_context and 'zero_value' not in self.extra:
extra_context['zero_value'] = 0
return super().as_sql(compiler, connection, template=template, **extra_context)
and
def get_queryset(self, request):
credit_query=CreditOrder.objects.filter(credit__font__company__id=OuterRef('id')).order_by()
.values('credit_used')
qs = super().get_queryset(request)
qs = qs.filter(user_type='company')
qs = qs.annotate(
_credits_used_sum=SubquerySum(credit_query, zero_value=0, field='credit_used')
)
return qs
Related
I have a model like this with relationship Booking -- Payment (one to may)
(one Booking can have many Payments)
My problem is that I calling too many expensive queries because of SerializerFieldModel().
class Booking(AbstractItem):
accommodation = models.CharField(max_length=100)
room = models.CharField(max_length=100)
booking_method = models.CharField(max_length=100)
class Payment(AbstractItem):
booking = models.ForeignKey(Booking, on_delete=models.CASCADE)
# paid, refund
status = models.PositiveIntegerField(choices=core_choices.PAYMENT_STATUS, default=core_choices.PAID, null=True)
# cash, credit_card, transfer
payment_method = models.PositiveIntegerField(choices=core_choices.PAYMENT_METHOD, default=core_choices.CASH)
price = models.DecimalField(max_digits=10, decimal_places=0, null=True)
This is my serializer
class BookingXLSXSerializer(ModelSerializer):
paid_cash = SerializerMethodField()
paid_card = SerializerMethodField()
refund_cash = SerializerMethodField()
refund_card = SerializerMethodField()
class Meta:
model = Booking
fields = ('id', 'accommodation ', 'room', 'booking_method', 'paid_cash', 'paid_card', 'refund_cash', 'refund_card')
def get_paid_cash(self, obj):
payments = Payment.objects.filter(booking=obj.id, status=core_choices.CASH)
cash = 0
for payment in payments:
cash += payment.price
return cash
#I noticed that many of the def could use the .all() query then just filter it out
...
this is my view:
class MyExampleViewSet(ListAPIView):
queryset = Booking.objects.all()
serializer_class = BookingXLSXSerializer
pagination_class = SuperLargePagination
I noticed that many SerializerMethodField() could share query and use different filter. Is there a smarter way to reduce calling queries for SerializerMethodField(). Or maybe a way to share the query?
prefetch_related()
You can try adding a prefetch_related clause:
queryset = Booking.objects.prefetch_related('payment_set').all()
To benefit from this, you need to change the code in your serializer to actually use the related field, for example:
cash = obj.payment_set.filter(status=core_choices.CASH).aggregate(Sum('price'))['price_sum']
This will still result in two queries, though. annotate is more precise.
See also Django: Does prefetch_related() follow reverse relationship lookup?
annotate()
Another, more complex possibility which also gives you more influence what the DB is actually returning to you, are annotations:
This will reduce it to one query letting the DB do all of the work. I'm writing this down without testing, you will have to check the details out for yourself.
It is definitly not easy to create complex annotations but it is a great tool for improving performance, and you can find a lot of good code for it already on this site. (And it can actually be fun...)
from django.db.models import OuterRef, Subquery, IntegerField
class MyExampleViewSet(ListAPIView):
cash_sub_qs = Payment.objects.filter(
booking=OuterRef('pk'),
status=core_choices.CASH
).annotate(paid_cash=Sum('price')).values('paid_cash')[:1]
queryset = Booking.objects.annotate(
paid_cash=Subquery(cash_sub_qs, output_field=DecimalField()),
refund_cash=..., # missing from your question, prolly also a sum?
).all()
serializer_class = BookingXLSXSerializer
pagination_class = SuperLargePagination
Your serializer should only use the queryset call as a fallback:
class BookingXLSXSerializer(ModelSerializer):
paid_cash = SerializerMethodField()
paid_card = SerializerMethodField()
refund_cash = SerializerMethodField()
refund_card = SerializerMethodField()
class Meta:
model = Booking
fields = ('id', 'accommodation ', 'room', 'booking_method', 'paid_cash', 'paid_card', 'refund_cash', 'refund_card')
def get_paid_cash(self, obj):
cash = getattr(obj, 'paid_cash', None) # this is the annotated field
if cash is None:
cash = obj.payment_set.filter(status=core_choices.CASH).aggregate(Sum('price'))['price_sum']
return cash
I have a django project with a django rest framework. I am trying to use the Retreive API View to return the members details. I want to grab all the member records based on a parameter that is passed in teh url with the groups name. I tried using the .get() and .filter(). The .get() returned an error of returning more than 2 items. The .filter() is not part of the dict.
I tried .list and .retrieve
How can I retreive the data objects based with more than 1 item. Here is my View that I am calling.
class MemberDetailView(RetrieveAPIView):
queryset = Member.objects.all()
serializer_class = MemberSerializer
def get_object(self):
return self.queryset.filter(group__name=self.kwargs.filter('name'))
model
class Member(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
host = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.group.name + ' - ' + self.user.username
urls
path('members/', MemberListView.as_view()),
path('members/<name>', MemberDetailView.as_view()),
_________________________________________-
UPDATE:
so i am getting the error when i override the list:
TypeError at /api/groups/members/homiez
list() got an unexpected keyword argument 'name'
when i dont override the list I get an empty results object.
Here is the code I have right now...
class MemberGroupListView(ListAPIView):
serializer_class = MemberSerializer
def get_queryset(self):
return Member.objects.filter(group__name=self.request.query_params.get('name'))
def list(self, request):
# Note the use of `get_queryset()` instead of `self.queryset`
queryset = self.get_queryset()
serializer = MemberSerializer(queryset, many=True)
return Response(serializer)
models
class Group(models.Model):
name = models.CharField(max_length=42)
description = models.CharField(max_length=220)
user_count = models.IntegerField()
status = models.CharField(max_length=12)
image = models.ImageField(upload_to='group_images/')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name + ' - ' + self.created_by.username
class Member(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
host = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.group.name + ' - ' + self.user.username
urls:
path('members/<name>', MemberGroupListView.as_view()),
RetrieveAPIView is designed for retrieving single instance. You should use ListAPIView instead. And use get_queryset method instead of get_object
ListAPIView calls serializer with many=True param, which returns a list of objects instead of one.
Django ORM get is used to retrieve only one object. filter maybe used to query for multiple objects in a queryset. Hence for retrieving multiple stuff you have to use filter.
In your View I think you need to override the list method.
class MemberDetailView(RetrieveAPIView):
serializer_class = MemberSerializer
def get_queryset(self):
"""
This view should return a list of all the purchases for
the user as determined by the username portion of the URL.
"""
username = self.kwargs['username']
return Purchase.objects.filter(group__name=self.request.query_params.get('name'))
def list(self, request):
# Note the use of `get_queryset()` instead of `self.queryset`
queryset = self.get_queryset()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
Note: self.request.query_params.get() is how you retrieve data from request object. It is about a dictionary operations rather than ORM operations. So don't do .filter() on self.kwargs.
In addition to #Alex's answer, you need to implement the filter method or typicallu add the filter backend to be able to filter querysets based on url filter parameters.
I would go with a filter backend using django-filter as it is quite effective, flexiblle and has become quite a standard for Django applications.
You need something like this:
from django_filters import rest_framework as filters
class MemberDetailView(RetrieveAPIView):
queryset = Member.objects.all()
serializer_class = MemberSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('user', 'host', 'group')
You can read the Django-filter docs for more details on how to use it.
Hi I have two models with m2m link through third model:
class Group(UsefulAbstractModel):
hotels = models.ManyToManyField(
Hotel,
through='HotelDetails',
related_name='groups', )
class Hotel(UsefulAbstractModel):
name = models.CharField(
max_length=255,)
class HotelDetails(models.Model):
hotel = models.ForeignKey(
Hotel,
related_name='hotel_details', )
group = models.ForeignKey(
Group,
related_name='hotel_details', )
num = models.IntegerField(
validators=[
MaxValueValidator(2),
MinValueValidator(1), ], )
All groups has two hotel links with numbers 1 and 2. I need to display it in admin group interface.
I create two custom columns for each hotel:
class GroupAdmin(admin.ModelAdmin):
def first_hotel(self, instance):
return instance.hotel_details.filter(num=1).first().hotel
def second_hotel(self, instance):
return instance.hotel_details.filter(num=1).first().hotel
But for every instance I have 2 additional query now.
I tried to override queryset method, but in not helped:
def queryset(self, request):
return super(GroupAdmin,self).queryset(request).prefetch_related('hotels')
The problem is that you are filtering the prefetched results with filter(num=1). This causes Django to do a new query.
You can use a Prefetch object to fetch the correct queryset. Note you should override the model admin's get_queryset method, not queryset.
def get_queryset(self, request):
return super(GroupAdmin,self).get_queryset(request).prefetch_related(
Prefetch('hotel_details', queryset=HotelDetails.objects.filter(num=1).select_related('hotel'), to_attr='hotel_details_num1'),
)
Then change your model admin methods to use the new queryset, for example:
def first_hotel(self, instance):
return instance.hotel_details_num1.first().hotel
See the docs for more into about prefetch_related and Prefetch objects.
I would like to order by entry_count but it seems (from the django error) I can only order by name and status. How can I order by entry_count in this example? Thanks
Category.objects.filter(status='Published').order_by('-entry_count')
Model.py
class Category(models.Model):
"""
Entry Categories.
"""
name = models.CharField(max_length=60, help_text="Title of Category")
status = models.CharField(max_length=16, choices=STATUS_CHOICES,
default="Draft", )
#property
def entry_count(self):
return Entry.objects.filter(category=self.id).count()
You can do this by using aggregation:
from django.db.models import Count
Category.objects.annotate(
entry_count=Count('entry_set')
).order_by('-entry_count')
Like this, you'll get all the counts in one query and all the category objects will have entry_count, so you can remove the #property from the model.
I have the following self referencing model that gives who this profile supports.
supports = models.ManyToManyField('self', blank=True, symmetrical=False,
related_name='supporters')
And I want the count not supports, supporters that support this user.
Any ideas ?
def get_queryset(self):
qs = super(ProfileListView, self).get_queryset()
if self.request.GET.get('s'):
sorting = self.request.GET.get('s')
if sorting == 'pop':
qs = ......
return qs
I will assume I have an instance of your model with the name myUser:
supporters = myUser.supports_set.all()
Annotate number of supporters like below, solved my problem
qs = qs.annotate(number_of_supporters=Count('supporters'))
qs = qs.order_by('-number_of_supporters')