Tuesday 17 October 2017

Django models: I will cache you if I can

This post presents a generalised approach to making cache keys for Django model from fields and related one-to-one fields. In the last post  a Django model based approach to cache keys was utilised. However, that results in code duplication for the purpose of generating cache keys. At first look that appears inevitable since each model is cached in its own way. However as more requirements come in, a pattern can be identified and code duplication can be avoided. This new approach to making cache keys is enabled by a base class that inherits from models.Model and provides mechanisms to generate keys. The cache key generation takes into account what model fields the key has to be based on. 

For example, if there is a Player model with fields id, email, username. We may want a cache key that would look like 'model.used.id.<the_id>'. Same for email and username. However, what if many fields of a model need to be in the cache key? This post presents a solution. Again, what if there is a one-to-one model which has a foreign key back to Player and that model needs to be cached ? For example, consider a model PlayerStats with fields id, user, stats. When this model needs to be queried it is likely to be based on the user. So, it makes sense to have a cache key for PlayerStats based on its linked player. Since this approach takes in any field, this becomes easy. The base class is

The class methods give back the cache key template and the instance method generates the cache key for the instance. The template is needed when some model's fields+values are available and we need to check the cache for those. Example is a web request asking for User with id=10 and we want to check that if that user is in cache.

The Player model will inherit from CacheableModel,

The PlayerStats looks like this

Now all that remains is a utility that takes in Model classes and calls the two class/instance methods.

- Now when Player needs to be queried on id, say from a Django view it becomes as easy as.

model_ins_from_cache_by_fields(Player, {'id': steamid})

- For Player stats for a specific player, this becomes

player = model_ins_from_cache_by_fields(Player, {'id': steamid})
player_totals = model_ins_from_cache_by_fields(PlayerTotals, {'player': player})

That is saving a lot of "if in cache else" code from repeating in views.

Some things to keep in mind:

1) Notice that the __str__ of the models is used for building keys. So make sure that __str__ of models do not have spaces and control characters.

2) Since it is based on __str__, if the __str__ represention is too long then the cache key becomes too long, possibly to the extent of increasing the time of hashing for the key.

Both these are addressable and is left as homework for reader. ;-)