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?
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With