Adnan
Adnan

Reputation: 23

Django Performance | Recalculating a field value in all related models after updating a model in Django

I want to learn what is the best approach to recalculate a model field value(total_cost in my Product model class) in all related models after updating a model. I wrote a piece of code and it works but it takes long time. How can I speed up my code? Here is model classes

#models.py
class Product(..):
    total_cost = models.FloatField(...)
    materials = models.ManyToManyField(Material, through="MaterialProduct")


    def calculate_cost(self):
        return round(calc_material_price(self.materialproduct_set.all()),2)

    def calc_material_price(self, mylist):
        res = 0
        for m in mylist:
            res += m.material.price * m.material_rate
        return res

class Material(..):
    price = models.FloatField(...)

class MaterialProduct(..):
    product = models.ForeignKey(Product, ..)
    material = models.ForeignKey(Material, ..)
    material_rate = models.FloatField()

class UpdateProductMaterialSerializer(..):
    #...
    def update(self, instance, validated_data):
        #in front-end total cost calculated and I assign it here directly
        instance.total_cost = self.initial_data.get("total_cost")
        instance.save()
        mylist_data = self.initial_data.get("mylist")
        material_list_for_update = []
        for item in mylist_data:
            material = item.get("material")
            material_instance = Material.objects.get(pk=material["id"])
            # I use this list for updating all Products
            material_list_for_update.append(material_instance)

            material_rate = item.get("material_rate")
            mp = MaterialProduct.objects.filter(product=instance, material=material_instance).first()
            mp.material_rate = material_rate
            mp.save()# here my one product is updated successfully


            # TO UPDATE ALL PRODUCTS I call below method
            # But this takes 15 seconds
            update_all_product_by_sent_materials(material_list_for_update)
        return instance

    def update_all_product_by_sent_materials(self, material_list):
        for material in material_list:
            for mp in MaterialProduct.objects.filter(material=material):
                mp.material.total_cost = mp.material.calculate_cost()
                mp.material.save()


Upvotes: 2

Views: 461

Answers (1)

ruddra
ruddra

Reputation: 51998

Maybe I should use total_cost as property in my Product model class and use it.

I think its a good idea, and not to store this value in DB as well. But it will not reduce time to calculate the cost. Maybe you can use aggregation to reduce the time like this:

from django.db.models import Sum, F
....

@property
def calculate_cost(self):
    cost = self.materialproduct_set.aggregate(cost=Sum(F('material__price') * F('material_rate')))['cost']
    return round(cost, 2)

Basically it will aggregate the value from DB so you don't have to make a loop and calculate manually. Also, it will reduce DB hits so performance should improve.


Improved answer based on this SO Answer:

from django.db.models import F, Func, Sum

class Round(Func):
    function = 'ROUND'
    template='%(function)s(%(expressions)s, 2)'

# rest of the code

@property
def calculate_cost(self):
    cost = self.materialproduct_set.aggregate(cost=Round(Sum(F('material__price') * F('material_rate'))))['cost']
    return cost

Upvotes: 3

Related Questions