I am having issues with the performance of a couple of queries in my Django app... all others are very fast.
I have an Orders model with OrderItems, the query seems to be running much slower than other queries (1-2 seconds, vs. 0.2 seconds). I'm using MySQL backend. In the serializer I do a count to return whether an order has food or drink items, I suspect this is causing the performance hit. Is there a better way to do it?
Here is my models setup for Order and OrderItems
class Order(models.Model):
STATUS = (
('1', 'Placed'),
('2', 'Complete')
)
PAYMENT_STATUS = (
('1', 'Pending'),
('2', 'Paid'),
('3', 'Declined'),
('4', 'Manual')
)
shop= models.ForeignKey(Shop,on_delete=models.DO_NOTHING)
customer = models.ForeignKey(Customer,on_delete=models.DO_NOTHING)
total_price = models.DecimalField(max_digits=6, decimal_places=2,default=0)
created_at = models.DateTimeField(auto_now_add=True, null=True)
time_completed = models.DateTimeField(auto_now_add=True, null=True,blank=True)
time_cancelled = models.DateTimeField(auto_now_add=True, null=True,blank=True)
status = models.CharField(max_length=2, choices=STATUS, default='1',)
payment_method = models.CharField(max_length=2, choices=PAYMENT_METHOD, default='3',)
payment_status = models.CharField(max_length=2, choices=PAYMENT_STATUS, default='1',)
type = models.CharField(max_length=2, choices=TYPE, default='1',)
def __str__(self):
return str(self.id)
class OrderItem(models.Model):
order = models.ForeignKey(Order,on_delete=models.CASCADE)
type = models.CharField(max_length=200,default='DRINK')
drink = models.ForeignKey(
Drink,
blank=True,null=True,on_delete=models.DO_NOTHING
)
food = models.ForeignKey(
Food,
blank=True,
null=True,
on_delete=models.DO_NOTHING
)
quantity = models.IntegerField(blank=True,null=True)
price = models.DecimalField(max_digits=6, decimal_places=2,default=0)
created_at = models.DateTimeField(auto_now_add=True, null=True)
delivered = models.BooleanField(default=False)
def __str__(self):
return str(self.id)
In my rest order serializer, here is the query for get,
queryset = Order.objects.filter(shop=shop,status__in=['1','2'],payment_status__in=['2','4'])
The serializer is below, but this query is quite slow. I assume because I am doing a count() on OrderItems - is there a more efficient way to do this?
class OrderOverviewSerializer(serializers.ModelSerializer):
tabledetails = serializers.SerializerMethodField()
has_food = serializers.SerializerMethodField()
has_drink = serializers.SerializerMethodField()
class Meta:
model = Order
fields = ['id','total_price', 'created_at','has_food','has_drink','type','status','shop','table','customer','shopdetails']
def get_shopdetails(self, instance):
qs = Shop.objects.get(id=instance.shop.id)
serializer = ShopSerializer(instance=qs, many=False)
return serializer.data
def get_has_food(self, obj):
foodCount = OrderItem.objects.filter(order=obj.id,type='FOOD').count()
return foodCount
def get_has_drink(self, obj):
drinkCount = OrderItem.objects.filter(order=obj.id,type='DRINK').count()
return drinkCount
There reason for that is famous N+1 problem that Django ORM is so inept to handle. The solution for it is to use select_related answerd in this question. More on that here.
You should consider db_index=True on the fields you're querying over (Order.status, Order.payment_status, OrderItem.type).
get_shopdetails() isn't used for anything in the serializer? (On a similar note, the getter for tabledetails is missing... are you maybe presenting some code that's not exactly what you're running?)
get_shopdetails() is redundant anyway; you can simply declare shop = ShopSerializer() and DRF will know what to do.
If the get_has_food/get_has_drink fields did prove to be the bottleneck (which they apparently didn't), you could use a Django aggregate to count the rows during the query for orders.
Speaking of, your serializer is accessing several foreign keys, which will all cause N+1 queries; you can add .select_related('shop', 'customer', 'table') (or .prefetch_related() the same) at the very least to have those get loaded in one fell swoop.
Beyond this -- profile your code! The easiest way to do that is to copy the skeleton from manage.py and add some code to simulate your query, e.g. (this is dry-coded):
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my_settings")
import django
django.setup()
from django.test import TestClient
c = TestClient()
for x in range(15):
c.get("/api/order/") # TODO: use correct URL
and run your script with
python -m cProfile my_test_script.py
You'll see which functions end up taking the most time.
Related
# Here is my models
This is my CustmerBuySell model DB designed.
class CustomerBuySell(models.Model):
customer = models.ForeignKey(CustomerAdd, on_delete=models.CASCADE)
customer_buy_sell_debit = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
customer_buy_sell_credit = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
description = models.CharField(max_length=250, blank=True)
date = models.DateField()
sms = models.BooleanField(default=False)
picture = models.ImageField(upload_to='customer_buy_sell_pics', default='images.png')
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True, blank=True, null=True)
def __str__(self):
return self.customer.customer_name
class Meta:
verbose_name = "Customer BuySell"
verbose_name_plural = "Customer BuySell"
# Here, is my View.
This is the class-based APIView, which I have used. And try to use the aggregate query in this view.
class DailyCustomerBuySellAPIView(APIView):
def get(self, request):
customer_buy_sell = CustomerBuySell.objects.extra(select={'day': 'date( date )'}).values('day').order_by(
'date__date').annotate(available=Count('date__date'))
serializer = CustomerBuySellSerializer(customer_buy_sell, many=True)
return Response({"customer_buy_sell": serializer.data})
# And, finally here are my Serializers
I have no idea what's the problem! Please help me.
class CustomerBuySellSerializer(serializers.ModelSerializer):
# customer = CustomerAddSerializer()
class Meta:
model = CustomerBuySell
fields = '__all__'
def to_representation(self, instance):
representation = super(CustomerBuySellSerializer, self).to_representation(instance)
if instance.customer is not None:
customer_name = instance.customer.customer_name
previous_due = instance.customer.previous_due
representation['custo`enter code here`mer_name'] = customer_name
representation['previous_due'] = previous_due
return representation
There are many problems with your approach. Let me mention each of them one by one:
First of all remove date__date from your APIVIew
Before:
customer_buy_sell = CustomerBuySell.objects.extra(select={'day': 'date( date )'}).values('day').order_by(
'date__date').annotate(available=Count('date__date'))
Instead, write it as:
from django.db.models.functions import Extract
customer_buy_sell = CustomerBuySell.objects.annotate(day=Extract('date','day')).values('day').order_by('day')
if you need a count of the days then you can try
customer_buy_sell_count = customer_buy_sell.count()
Another thing that you are doing wrong is you pass a dict to serializer as you are already using values that return a dictionary of days and not object of CustomerBuySell so you do not need to pass it to serializer otherwise you have to do it according to your need.
In CustomerBuySellSerializer you are using a model serializer with __all__ while you are passing an extra fields day that is not part of it.
So in short there are so many syntax issues with your Django and Django Rest Framework.Great way to fix such issues is to set with an experience programmer so that he can improve the flow of the code. Later on you can focus on logic.
I suppose it is just a typo: Change date__date to date
I am trying to generate a uniq OrderItem_ID during the order create api. But, it generates the above error as django.db.utils.IntegrityError:
The first api is always successful from the postman, but in the second call I tried changing different products for creating the order, but I am getting this unique id order.
I have to remove the order_items from db , to create a new order_item object otherwise I get this unique error.
I am sending data like this.
My model:
import random
import string
# Create your models here.
def id_generator(size=10, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size))
class Order(models.Model):
ORDER_STATUS = (
('To_Ship', 'To Ship',),
('Shipped', 'Shipped',),
('Delivered', 'Delivered',),
('Cancelled', 'Cancelled',),
)
user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True)
order_status = models.CharField(max_length=50,choices=ORDER_STATUS,default='To_Ship')
ordered_date = models.DateTimeField(auto_now_add=True)
ordered = models.BooleanField(default=False)
total_price = models.CharField(max_length=50,blank=True,null=True)
def __str__(self):
return self.user.email
class Meta:
verbose_name_plural = "Orders"
ordering = ('-id',)
class OrderItem(models.Model):
orderItem_ID = models.CharField(max_length=12,unique=True, editable=False, default=id_generator())
order = models.ForeignKey(Order,on_delete=models.CASCADE, blank=True,null=True,related_name='order_items')
item = models.ForeignKey(Product, on_delete=models.CASCADE,blank=True, null=True)
order_variants = models.ForeignKey(Variants,on_delete=models.CASCADE,blank=True,null=True)
quantity = models.IntegerField(default=1)
total_item_price = models.PositiveIntegerField(blank=True,null=True,)
My serializers:
class OrderSerializer(serializers.ModelSerializer):
billing_details = BillingDetailsSerializer()
order_items = OrderItemSerializer(many=True)
user = serializers.PrimaryKeyRelatedField(read_only=True, default=serializers.CurrentUserDefault())
class Meta:
model = Order
fields = ['id','user','ordered_date','order_status', 'ordered', 'order_items', 'total_price','billing_details']
# depth = 1
def create(self, validated_data):
user = self.context['request'].user
if not user.is_seller:
order_items = validated_data.pop('order_items')
billing_details = validated_data.pop('billing_details')
order = Order.objects.create(user=user,**validated_data)
BillingDetails.objects.create(user=user,order=order,**billing_details)
for order_items in order_items:
OrderItem.objects.create(order=order,**order_items)
return order
else:
raise serializers.ValidationError("This is not a customer account.Please login as customer.")
In python shell, i tired this and it works fine
the problem is with
orderItem_ID = models.CharField(max_length=12,unique=True, editable=False, default=id_generator())
Here in the default, you're have assigned function call. Thus, it will only be evaluated once at the time of creation, .i.e., at first time you run makemigrations.
We need to have function references in the default values, this way it will be called each time a new instance is created.
Try replacing the line with
orderItem_ID = models.CharField(max_length=12,unique=True, editable=False, default=id_generator)
Note default=id_generator and not default=id_generator().
Hope this answers your question.
PS: you would be required to rerun the makemigrations and migrations commands to set this change into effect.
The validation error comes from here:
orderItem_ID = models.CharField(max_length=12,unique=True, editable=False, default=id_generator())
Add orderItem_ID in OrderItemSerializer and try sending orderItem_ID in "order_items" with a unique value on each post.
As for this: default=id_generator()
Check what this function is generating on each hit, probably its saving the same value each time which is causing the error.
Im trying to get values of an order which a particular user made, I mean I have an e commerce app where a user can made purchases, I successfully got the order item to display when a user wants to make an order, but i want to get all the orders which are already purchased by that user to display in a different page (Order History), Im trying to use queryset for the Serializers but its just not work despite severally tweaks, and i have ready the docs but cant seem to get it right. Please help,
Model:
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
ref_code = models.CharField(max_length=20, blank=True, null=True)
items = models.ManyToManyField(eOrderItem)
start_date = models.DateTimeField(auto_now_add=True)
ordered_date = models.DateTimeField(null=True)
ordered = models.BooleanField(default=False)
payment = models.ForeignKey(
'Payment', on_delete=models.SET_NULL, blank=True, null=True)
coupon = models.ForeignKey(
'Coupon', on_delete=models.SET_NULL, blank=True, null=True)
being_delivered = models.BooleanField(default=False)
received = models.BooleanField(default=False)
refund_requested = models.BooleanField(default=False)
refund_granted = models.BooleanField(default=False)
transaction_id = models.CharField(max_length=200, null=True)
qr_code = models.ImageField(upload_to='qrcode', blank=True)
This is the serializer for the (Order History)
class TicketSerializer(serializers.ModelSerializer):
order_items = serializers.SerializerMethodField()
class Meta:
model = Order
fields = '__all__'
def get_order_items(self, obj):
return OrderItemSerializer().data
View:
class TicketDetailView(RetrieveAPIView):
serializer_class = TicketSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
try:
# order = Order.objects.get(user=self.request.user).filter(ordered=True)
# order = Order.objects.filter(order=True)
# order = Order.objects.get(user=self.request.user, ordered=True)
order = Order.objects.filter(ordered=False, user=self.request.user)
return order
except ObjectDoesNotExist:
return Response({"message": "You do not have any ticket"}, status=HTTP_400_BAD_REQUEST)
from the view, you can see i try tried may options with queryset, but its not work, It works when i use get(user=self.request.user), but when i pass Order=True(for order history) it says get() returned more than one Order -- it returned 3! and i understand because i use get() (other options dont work) when i pass Order=False (as it works for the order item to be purchased) it works because its just one at a time.
What do i do please, i just want to be about to get all the items that are order by a particular user.
You expect multiples results that's why you should override get_queryset() and not get_object() (should be used for detail views):
def get_queryset(self):
return Order.objects.filter(ordered=False, user=self.request.user)
I have Pick model which when I select a record within Django Admin takes an age (about 20 seconds) to retrieve the record (there are about 70k). However, it is quick when I try and create/save a record. I have added the indexes to try and speed up retrieval but don't really know what I should be doing
class Pick (models.Model):
team = models.ForeignKey(Team, on_delete=models.CASCADE, null=True, blank=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, db_index=True)
contest_entry=models.ForeignKey(ContestEntry, on_delete=models.CASCADE, db_index=True)
game_round=models.ForeignKey(GameRound, on_delete=models.CASCADE, db_index=True)
objects = DataFrameManager()
def __str__(self):
return f'%s %s' % (self.contest_entry, self.game_round)
class Meta:
unique_together = ['user', 'contest_entry', 'game_round']
ordering = ['contest_entry','game_round','user','team']
index_together = [["user", "contest_entry", 'game_round'],]
indexes = [
models.Index(fields=['user', 'contest_entry', 'game_round']),
models.Index(fields=['game_round'], name='game_round_idx'),
models.Index(fields=['contest_entry'], name='contest_entry_idx'),
]
class PickAdmin(admin.ModelAdmin):
model = Pick
list_filter= (
('user', RelatedDropdownFilter),
('contest_entry__contest', RelatedDropdownFilter),
('game_round', RelatedDropdownFilter)
)
def get_queryset(self, request):
return super().get_queryset(request).select_related('user','contest_entry','game_round')
admin.site.register(Pick, PickAdmin)
How can I improve performance here?
The key is specifying the raw_id_fields on the admin class. The default <select> widget for foreign key relationships can cause a lot of overhead retrieving and rendering all the options.
class PickAdmin(admin.ModelAdmin):
model = Pick
list_filter= (
('user', RelatedDropdownFilter),
('contest_entry__contest', RelatedDropdownFilter),
('game_round', RelatedDropdownFilter)
)
raw_id_fields = ('user', 'contest_entry', 'game_round',)
def get_queryset(self, request):
return super().get_queryset(request).select_related('user','contest_entry','game_round')
My guess is that what takes so long is rendering the dropdowns for the ForeignKey fields of your model.
I'd suggest looking at the SQL queries that are performed, either by installing Django toolbar or enabling logging in your database server.
You can improve the queries by using select_related for foreignkeys https://docs.djangoproject.com/en/2.2/ref/models/querysets/#select-related
I have a Django project that consists of a scraper of our inventory, run on the server as a cronjob every few hours, and the Django Admin page - which we use to view / access all items.
We have about 30 items that are indexed.
So each 'Scraping Operation' consists of about 30 individual 'Search Operations' each of which get around 500 results per run.
Now, this description is a bit confusing, so I've included the models below.
class ScrapingOperation(models.Model):
date_started = models.DateTimeField(default=timezone.now, editable=True)
date_completed = models.DateTimeField(blank=True, null=True)
completed = models.BooleanField(default=False)
round = models.IntegerField(default=-1)
trusted = models.BooleanField(default=True)
class Search(models.Model):
item = models.ForeignKey(Item, on_delete=models.CASCADE)
date_started = models.DateTimeField(default=timezone.now, editable=True)
date_completed = models.DateTimeField(blank=True, null=True)
completed = models.BooleanField(default=False)
round = models.IntegerField(default=1)
scraping_operation = models.ForeignKey(ScrapingOperation, on_delete=models.CASCADE, related_name='searches')
trusted = models.BooleanField(default=True)
def total_ads(self):
return self.ads.count()
class Ad(models.Model):
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='ads')
title = models.CharField(max_length=500)
price = models.DecimalField(max_digits=8, decimal_places=2, null=True)
first_seen = models.DateTimeField(default=timezone.now, editable=True)
last_seen = models.DateTimeField(default=timezone.now, editable=True)
def __str__(self):
return self.title
Now here is the problem we've run into.
On the admin pages for both the Search model and the SeachOperation model we would like to see the amount of ads scraped for that particular object (represented as a number) This works fine four our seachers, but our implementation for the SearchOperation has run into problems
This is the code that we use:
class ScrapingOperationAdmin(admin.ModelAdmin):
list_display = ['id', 'completed', 'trusted', 'date_started', 'date_completed', 'number_of_ads']
list_filter = ('completed', 'trusted')
view_on_site = False
inlines = [
SearchInlineAdmin,
]
def number_of_ads(self, instance):
total_ads = 0
for search in instance.searches.all():
total_ads += search.ads.count()
return total_ads
The problem that we have run into is this: The code works and provides the correct number, however, after +/- 10 ScrapingOperation we noticed that the site started to slow done when loading the page. We are now up to 60 ScrapingOperations and when we click the ScrapingOperations page in the Django admin it takes almost a minute to load.
Is there a more efficient way to do this? We thought about saving the total number of ads to the model itself, but it seems wasteful to dedicate a field to information that should be accessible with a simple .count() call. Yet our query is evidently so inefficient that the entire site locks down for almost a minute when it is executed. Does anyone have an idea of what we are doing wrong?
Based on the comments below I am currently working on the following solution:
def number_of_ads(self, instance):
total_ads = 0
searches = Search.objects.filter(scraping_operation=instance).annotate(Count('ads'))
for search in searches:
total_ads += search.ads__count
return total_ads
Use an annotation when getting the queryset
from django.db.models import Count
class ScrapingOperationAdmin(admin.ModelAdmin):
...
def get_queryset(self, request):
qs = super().get_queryset(request)
qs.annotate(number_of_ads=Count('searches__ads')
return qs