- May 15, 2012
- model manager signals abstract managers models signal contribute_to_class cache
- 1 (after 1 ratings)
I found a question on SO for which Justin Lilly's answer was correct but not as thorough as I'd like, so I ended up working on a simple snippet that shows how to bind signals at runtime, which is nifty when you want to bind signals to an abstract class.
Bonus: simple cache invalidation!
I have an abstract model that keeps an on-disk cache. When I delete the model, I need it to delete the cache. I want this to happen for every derived model as well.
If I connect the signal specifying the abstract model, this does not propagate to the derived models:
pre_delete.connect(clear_cache, sender=MyAbstractModel, weak=False)
If I try to connect the signal in an init, where I can get the derived class name, it works, but I'm afraid it will attempt to clear the cache as many times as I've initialized a derived model, not just once.
Where should I connect the signal?
I've created a custom manager that binds a post_save signal to every child of a class, be it abstract or not.
This is a one-off, poorly tested code, so beware! It works so far, though.
In this example, we allow an abstract model to define CachedModelManager as a manager, which then extends basic caching functionality to the model and its children. It allows you to define a list of volatile keys that should be deleted upon every save (hence the post_save signal) and adds a couple of helper functions to generate cache keys, as well as retrieving, setting and deleting keys.
This of course assumes you have a cache backend setup and working properly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
# helperapp\models.py # -*- coding: UTF-8 from django.db import models from django.core.cache import cache class CachedModelManager(models.Manager): def contribute_to_class(self, model, name): super(CachedModelManager, self).contribute_to_class(model, name) setattr(model, 'volatile_cache_keys', getattr(model, 'volatile_cache_keys', )) setattr(model, 'cache_key', getattr(model, 'cache_key', cache_key)) setattr(model, 'get_cache', getattr(model, 'get_cache', get_cache)) setattr(model, 'set_cache', getattr(model, 'set_cache', set_cache)) setattr(model, 'del_cache', getattr(model, 'del_cache', del_cache)) self._bind_flush_signal(model) def _bind_flush_signal(self, model): models.signals.post_save.connect(flush_volatile_keys, model) def flush_volatile_keys(sender, **kwargs): instance = kwargs.pop('instance', False) for key in instance.volatile_cache_keys: instance.del_cache(key) def cache_key(instance, key): if not instance.pk: name = "%s.%s" % (instance._meta.app_label, instance._meta.module_name) raise models.ObjectDoesNotExist("Can't generate a cache key for " + "this instance of '%s' " % name + "before defining a primary key.") else: return "%s.%s.%s.%s" % (instance._meta.app_label, instance._meta.module_name, instance.pk, key) def get_cache(instance, key): result = cache.get(instance.cache_key(key)) return result def set_cache(instance, key, value, timeout=60*60*24*3): result = cache.set(instance.cache_key(key), value, timeout) return result def del_cache(instance, key): result = cache.delete(instance.cache_key(key)) return result # myapp\models.py # -*- coding: UTF-8 from django.contrib.auth.models import User from django.db import models from helperapp.models import CachedModelManager class Abstract(models.Model): creator = models.ForeignKey(User) cache = CachedModelManager() class Meta: abstract = True class Community(Abstract): members = models.ManyToManyField(User) volatile_cache_keys = ['members_list',] @property def members_list(self): result = self.get_cache('members_list') if not result: result = self.members.all() self.set_cache('members_list', result) return result