Usage examples

Consider the following Django Model for a pizza:

from decimal import Decimal
from django.db import models
from django_dbcache_fields.decorators import dbcache

class Pizza(models.Model):
    TYPES = (
        ('regular', 'regular'),
        ('calzone', 'calzone'),
    )
    name = models.CharField(max_length=100)
    pizza_type = models.CharField(max_length=10, choices=TYPES)
    base_price = models.DecimalField(max_digits=4, decimal_places=2)

def get_total_price(self):
    supplement = Decimal()
    if self.pizza_type == 'calzone':
        supplement = Decimal(1)

    return self.base_price + supplement

The get_total_price() method calculated the final price simply by returning the base_price with a supplement for calzone pizza’s.

It’s not very exciting nor very computational complex but for the sake of simplicity, this is our use case.

Every call to get_total_price() performs the same calculation over and over again. Also, you can not simply order the list of pizza’s by their total price (without annotations and conditional expressions) on database level.

Basic usage

Let’s decorate the get_total_price() function with dbcache. To cache the total price, we need to indicate what type of Django Field will be used.

We assume the total price will never exceeds 6 digits, so we’ll use a Django DecimalField with the appropriate parameters. The field should always be allowed to be null. For good measure it can also be blank in case you add the field in the Django admin.

class Pizza(models.Model):
    # ...
    @dbcache(models.DecimalField(max_digits=6, decimal_places=2,
             blank=True, null=True))
    def get_total_price(self):
        # ...

The total price will now be stored in the database. Under the hood, a new field was added to the Pizza model: _get_total_price_cached.

So, our get_total_price() function works as it normally would:

>>> pizza = Pizza.objects.create(
...     name='margarita', base_price=Decimal(10), pizza_type='calzone')
>>> pizza.get_total_price()
Decimal('11')

In addition, this resulting value is cached on the model field created by the dbcache decorator, which also allows us to perform ORM-operations with it.

>>> pizza._get_total_price_cached
Decimal('11')
>>> Pizza.objects.filter(_get_total_price_cached__gte=Decimal(10))
<QuerySet [<Pizza: Pizza object>]>

The cached field is updated everytime a new instance is created or when the instance is saved.

If the cached value is None, it’s considered to be invalid. As a consequence, calling the get_total_price() method will perform its calculations as it normally would and will update the cached value in the database (using an update query, so it does not trigger a save signal).

More precise cache invalidation

In its simplest use case, dbcache updates the cached value everytime the instance is saved or when the decorated function is called and the cached value is None.

Changing the name of a Pizza however, will not cause any change to the total price. For this use case, we can pass a function to the dbcache decorator to indicate whether a change to the instance caused a price change.

Such a dirty function can be a very simple function or lambda, and should accept 2 arguments: The instance and the field_name of the cached field.

def is_pizza_price_changed(instance, field_name):
    return instance._original_base_price != instance.base_price

class Pizza(models.Model):
    def __init__(self, *args, **kwargs)
        super(Pizza, self).__init__(*args, **kwargs)
        # Store the original base price on the instance.
        self._original_base_price = self.base_price
    # ...
    @dbcache(models.DecimalField(max_digits=6, decimal_places=2,
             blank=True, null=True), dirty_func=is_pizza_price_changed)
    def get_total_price(self):
        # ...

The function is_pizza_price_changed(…) is passed to the dirty_func parameter of the dbcache decorator. This causes the following behaviour:

>>> pizza.name = 'hawaii'
>>> pizza.save()  # The cached field will not be updated
>>> pizza.get_total_price()
Decimal('11')

>>> pizza.base_price = Decimal(5)
>>> pizza.save()  # The cached field will be updated
>>> pizza.get_total_price()
Decimal('6')

>>> pizza.pizza_type = 'regular'
>>> pizza.save()  # The cached field will not be updated
>>> pizza.get_total_price()
Decimal('11')

Note that in the last example, the total price is not correct. The cached value was not invalidated due to an incomplete dirty function. The dirty function should have taken the pizza_type into account as well since it can affect the total price.

Methods that depend on other models

Consider this slightly altered version of our Pizza model. The pizza_type is no longer a choice field but instead a related model: PizzaType.

from django.db import models
from django_dbcache_fields.decorators import dbcache

class PizzaType(models.Model):
    name = models.CharField(max_length=100)
    supplement = models.DecimalField(max_digits=4, decimal_places=2)

class Pizza(models.Model):
    name = models.CharField(max_length=100)
    base_price = models.DecimalField(max_digits=4, decimal_places=2)
    pizza_type = models.ForeignKey(PizzaType)

    @dbcache(models.DecimalField(max_digits=6, decimal_places=2,
            blank=True, null=True), invalidated_by=['myapp.PizzaType'])
    def get_total_price(self):
        return self.base_price + self.pizza_type.supplement

The function PizzaType is passed to the invalidated_by parameter of the dbcache decorator. Any update to a PizzaType will cause all cached get_total_price values to be invalidated.

On the next call of get_total_price(), the invalidated cached value will be updated for this Pizza instance. Any save on the instance, would cause the same update.

Caveat

It’s worth noting that the value of the dbcache generated field can always be None. Be careful when using ORM-functions that rely on a filled value.

Also, a QuerySet.update() does not trigger cached field invalidation. In the above example PizzaType.objects.update(supplement=Decimal()) will result in incorrect total prices for pizza’s.