Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django REST Framework & Lifecycle of a Django Model instance

I have a django-rest-framework app that currently makes heavy use of computed properties in my models and my serializers. For (overly-simplified) example:

models.py

class Person(models.Model):
    first_name = models.CharField()
    last_name = models.CharField()

    @property
    full_name(self):
        return first_name + last_name

serializers.py

class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = ("first_name", "last_name", "full_name")

I am interested in using Django's @cached_property instead of @property, in hopes of speeding things up, since the values being computed here shouldn't really change more than once a day.

I am unsure, though, if @cached_property will actually effect how quickly DRF returns it's JSON response. The Django docs say:

The @cached_property decorator caches the result of a method with a single self argument as a property. The cached result will persist as long as the instance does, so if the instance is passed around and the function subsequently invoked, the cached result will be returned.

So I'm wondering what is the lifecycle of a Django model instance? Will it be created every time a call is made to a DRF view? And if so, are there alternative approaches to achieving my goal?

like image 383
Dustin Michels Avatar asked Oct 16 '25 04:10

Dustin Michels


1 Answers

The @cached_property decorator appears to work with an instance in-memory, similar to how querysets are cached. Here's a small example of what I mean:

# Model that stores two numbers and returns their sum (slowly)
class MyModel(models.Model):
    a = models.IntegerField()
    b = models.IntegerField()

    @cached_property
    def sum(self):
        time.sleep(10)
        return a + b

my_model = MyModel(a=1, b=2)
my_model.save()
print(my_model.sum) # This will take 10 seconds because of the sleep
print(my_model.sum) # This will return instantly because the result is cached and it doesn't need to execute the properties code

my_model_again = MyModel.objects.first()  # Lets fetch that model again from the database.
print(my_model_again.sum)  # This will take 10 seconds because of the sleep

# We can pass the instance around into other functions, and because it is still the same object it will leverage the cached property
def double_sum(instance):
    return instance.sum + instance.sum
print(double_sum(my_model_again))  # This will print almost instantly

In this example I've used sleep to simulate a lengthy or computationally expensive calculation.

Whilst my_model and my_model_again are representing the same database row, they are different instances in memory. You have to pass the instance around to leverage the cached property.

If you want the caching to persist between all instances of that object, you could store results in a database and invalidate them every 24 hours. Here's a simple example of that using the database:

class MyModel(models.Model):
    a = models.IntegerField()
    b = models.IntegerField()
    cached_sum = models.IntegerField(default=None, null=True, blank=True)
    cached_sum_timestamp = models.DateTimeField()

    @property
    def sum(self):
        # If the sum has been calculated in the last 24 hours, return the cached sum, otherwise recalculate 
        if (
            self.cached_sum 
            and self.cached_sum_timestamp
            and self.cached_sum_timestamp > (timezone.now() - timezone.timedelta(days=1)
        ):
            return self.cached_sum

        time.sleep(10)
        self.cached_sum = self.a + self.b
        self.cached_sum_timestamp = timezone.now()
        self.save(update_fields=("cached_sum", "cached_sum_timestamp"))

        return self.cached_sum

However you'd probably also want to invalidate the cache when the model is changed... and this is possible, but it gets a bit more difficult. Cache invalidation can be quite hard to get right. Here's a fairly naive example of invalidating the cache when an instances fields change:

class MyModel(models.Model):
    a = models.IntegerField()
    b = models.IntegerField()
    cached_sum = models.IntegerField(default=None, null=True, blank=True)

    @property
    def sum(self):
        if self.cached_sum:
            return self.cached_sum

        time.sleep(10)
        self.cached_sum = self.a + self.b
        self.save(update_fields=("cached_sum", ))
        return self.cached_sum

    def save(self, *args, **kwargs):
        if self.pk:  # Compare the object we're saving with whats in the database, and if the cache should be invalidated then set cached_sum to None
            db_obj = MyModel.objects.get(pk=self.pk)
            if db_obj.a != self.a or db_obj.b != self.b:
                self.cached_sum = None
        else:
            # It would be nice to cache this when we create the object?
            if self.cached_sum is not None:
                self.cached_sum = self.sum
        return super().save(*args, **kwargs)
like image 113
A. J. Parr Avatar answered Oct 17 '25 17:10

A. J. Parr



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!