New cache reloading only for models on startup and at ever attribute addition

This commit is contained in:
ksamuel 2010-09-08 01:18:27 +00:00
parent 5fbd515d3f
commit 0cc260c5b1
4 changed files with 183 additions and 57 deletions

152
models.py
View file

@ -1,3 +1,4 @@
import inspect
import re
from datetime import datetime
@ -9,6 +10,12 @@ from django.contrib.contenttypes import generic
from .fields import EavSlugField, EavDatatypeField
def get_unique_class_identifier(cls):
"""
Return a unique identifier for a class
"""
return '.'.join((inspect.getfile(cls), cls.__name__))
class EavAttributeLabel(models.Model):
name = models.CharField(_(u"name"), db_index=True,
@ -89,6 +96,7 @@ class EavAttribute(models.Model):
return
self.labels.remove(label_obj)
def get_value_for_entity(self, entity):
'''
Passed any object that may be used as an 'entity' object (is linked
@ -99,12 +107,17 @@ class EavAttribute(models.Model):
'''
ct = ContentType.objects.get_for_model(entity)
qs = self.eavvalue_set.filter(entity_ct=ct, entity_id=entity.pk)
if qs.count() == 1:
return qs[0]
raise AttributeError(u"You should have one and only one value"\
count = qs.count()
if count > 1:
raise AttributeError(u"You should have one and only one value "\
u"for the attribute %s and the entity %s. Found "\
u"%s" % (self, entity, qs.count()))
if count:
return qs[0]
return None
def save_value(self, entity, value):
"""
Save any value for an entity, calling the appropriate method
@ -157,10 +170,11 @@ class EavValue(models.Model):
'''
class Meta:
unique_together = ('entity_ct', 'entity_id', 'attribute',
'value_text', 'value_float', 'value_date',
'value_bool')
#class Meta:
# we can't a TextField on mysql with a unique constraint
#unique_together = ('entity_ct', 'entity_id', 'attribute',
# 'value_text', 'value_float', 'value_date',
# 'value_bool')
entity_ct = models.ForeignKey(ContentType, related_name='value_entities')
entity_id = models.IntegerField()
entity = generic.GenericForeignKey(ct_field='entity_ct', fk_field='entity_id')
@ -202,20 +216,83 @@ class EavValue(models.Model):
class EavEntity(object):
_cache = {}
def __init__(self, instance):
self.model = instance
self.ct = ContentType.objects.get_for_model(instance)
# TODO: memoize
def __getattr__(self, name):
if not name.startswith('_'):
for slug in self.get_all_attribute_slugs():
attribute = self.get_attribute_by_slug(name)
value = attribute.get_value_for_entity(self.model)
return value.value if value else None
if attribute:
value = attribute.get_value_for_entity(self.model)
if value:
return attribute.get_value_for_entity(self.model).value
return None
raise AttributeError(_(u"%s EAV does not have attribute " \
u"named \"%s\".") % \
(self.model._meta.object_name, name))
@classmethod
def get_attr_cache_for_model(cls, model_cls):
"""
Return the attribute cache for this model
"""
return cls._cache.setdefault(get_unique_class_identifier(cls), {})
@classmethod
def update_attr_cache_for_model(cls, model_cls):
"""
Create or update the attributes cache for this entity class.
"""
cache = cls.get_attr_cache_for_model(model_cls)
cache['attributes'] = cls.get_eav_attributes().select_related()
cache['slug_mapping'] = dict((s.slug, s) for s in cache['attributes'])
return cache
@classmethod
def flush_attr_cache_for_model(cls, model_cls):
"""
Flush the attributes cache for this entity class
"""
cache = cls.get_attr_cache_for_model(model_cls)
cache.clear()
return cache
def update_attr_cache(self):
"""
Create or update the attributes cache for the entity class linked
to the current instance.
"""
return self.__class__.update_attr_cache_for_model(self.model.__class__)
def flush_attr_cache(self):
"""
Flush the attributes cache for the entity class linked
to the current instance.
"""
return self.__class__.flush_attr_cache_for_model(self.model.__class__)
def get_attr_cache(self):
"""
Return the attribute cache for the entity class linked
to the current instance.
"""
return self.__class__.get_attr_cache_for_model(self.model.__class__)
def save(self):
for attribute in self.get_all_attributes():
if hasattr(self, attribute.slug):
@ -223,39 +300,62 @@ class EavEntity(object):
attribute.save_value(self.model, attribute_value)
def get_all_attributes(self):
try:
if self._attributes_cache is not None:
return self._attributes_cache
except AttributeError:
pass
self._attributes_cache = self.__class__.get_eav_attributes().select_related()
self._attributes_cache_dict = dict((s.slug, s) for s in self._attributes_cache)
return self._attributes_cache
"""
Return the current cache or if it doesn't exists, update it
and returns it.
"""
cache = self.get_attr_cache()
if not cache:
cache = self.update_attr_cache()
return cache['attributes']
def get_values(self):
return EavValue.objects.filter(entity_ct=self.ct,
entity_id=self.model.pk).select_related()
def get_all_attribute_slugs(self):
if not hasattr(self, '_attributes_cache_dict'):
self.get_all_attributes()
return self._attributes_cache_dict.keys()
"""
Returns all attributes slugs for the entity
class linked to the current instance.
"""
cache = self.get_attr_cache()
if not cache:
cache = self.update_attr_cache()
return cache['slug_mapping']
def get_attribute_by_slug(self, slug):
if not hasattr(self, '_attributes_cache_dict'):
self.get_all_attributes()
return self._attributes_cache_dict[slug]
"""
Returns an attribute object knowing the slug for the entity
class linked to the current instance.
"""
return self.get_all_attribute_slugs().get(slug, None)
def get_attribute_by_id(self, attribute_id):
"""
Returns an attribute object knowing the pk for the entity
class linked to the current instance.
"""
for attr in self.get_all_attributes():
if attr.pk == attribute_id:
return attr
def __iter__(self):
"Iterates over non-empty EAV attributes. Normal fields are not included."
return self.get_values().__iter__()
return iter(self.get_values())
@staticmethod
def save_handler(sender, *args, **kwargs):
kwargs['instance'].eav.save()
# TODO: remove that, it's just for testing with nose
class Patient(models.Model):
class Meta:
app_label = 'eav_ng'
name = models.CharField(max_length=20)
def __unicode__(self):
return self.name

View file

@ -124,7 +124,9 @@ class EavBasicTests(TestCase):
def test_eavregistry_ataches_and_detaches_eav_attribute(self):
EavRegistry.unregister(Patient)
print "hello"
p = Patient()
print "hello"
self.assertFalse(hasattr(p, 'eav'))
EavRegistry.register(Patient)

View file

@ -150,15 +150,13 @@ class EavSetterAndGetterTests(TestCase):
def test_can_filter_attribute_availability_for_entity(self):
attribute = EavAttribute.objects\
.create(datatype=EavAttribute.TYPE_TEXT,
name='Country', slug='country')
self.patient.eav.city = 'Paris'
self.patient.eav.city = 'Tunis'
self.patient.save()
self.assertEqual(Patient.objects.get(pk=self.patient.pk).eav.city,
'Paris')
'Tunis')
EavRegistry.unregister(Patient)
@ -174,11 +172,13 @@ class EavSetterAndGetterTests(TestCase):
p.eav.city = 'Paris'
p.eav.country = 'USA'
p.save()
p = Patient.objects.get(pk=self.patient.pk)
p = Patient.objects.get(pk=p.pk)
self.assertFalse(p.eav.city)
self.assertEqual(p.eav.country, 'USA')
p = Patient()
# todo: test multiple children

View file

@ -1,6 +1,6 @@
from django.db.models.signals import post_init, post_save
from django.db.models.signals import post_init, post_save, post_delete
from .managers import EntityManager
from .models import EavEntity, EavAttribute
from .models import EavEntity, EavAttribute, get_unique_class_identifier
class EavConfig(object):
@ -27,38 +27,53 @@ class EavRegistry(object):
"""
Attache EAV toolkit to an instance after init.
"""
cls_id = get_unique_class_identifier(sender)
instance = kwargs['instance']
cls = instance.__class__
admin_cls = EavRegistry.cache[cls.__name__]['admin_cls']
config_cls = EavRegistry.cache[cls_id]['config_cls']
setattr(instance, admin_cls.proxy_field_name, EavEntity(instance))
setattr(instance, config_cls.proxy_field_name, EavEntity(instance))
@staticmethod
def register(model_cls, admin_cls=EavConfig):
def update_attribute_cache(sender, *args, **kwargs):
"""
Update the attribute cache for all the models every time we
create an attribute.
"""
for cache in EavRegistry.cache.itervalues():
EavEntity.update_attr_cache_for_model(cache['model_cls'])
@staticmethod
def register(model_cls, config_cls=EavConfig):
"""
Inject eav features into the given model and attach a signal
listener to it for setup.
"""
if model_cls.__name__ in EavRegistry.cache:
cls_id = get_unique_class_identifier(model_cls)
if cls_id in EavRegistry.cache:
return
post_init.connect(EavRegistry.attach, sender=model_cls)
post_save.connect(EavEntity.save_handler, sender=model_cls)
EavRegistry.cache[model_cls.__name__] = { 'admin_cls':
admin_cls }
EavRegistry.cache[cls_id] = { 'config_cls': config_cls,
'model_cls': model_cls }
if hasattr(model_cls, admin_cls.manager_field_name):
mgr = getattr(model_cls, admin_cls.manager_field_name)
EavRegistry.cache[model_cls.__name__]['old_mgr'] = mgr
if hasattr(model_cls, config_cls.manager_field_name):
mgr = getattr(model_cls, config_cls.manager_field_name)
EavRegistry.cache[cls_id]['old_mgr'] = mgr
setattr(model_cls, admin_cls.proxy_field_name, EavEntity)
setattr(model_cls, config_cls.proxy_field_name, EavEntity)
setattr(getattr(model_cls, admin_cls.proxy_field_name),
'get_eav_attributes', admin_cls.get_eav_attributes)
setattr(getattr(model_cls, config_cls.proxy_field_name),
'get_eav_attributes', config_cls.get_eav_attributes)
mgr = EntityManager()
mgr.contribute_to_class(model_cls, admin_cls.manager_field_name)
mgr.contribute_to_class(model_cls, config_cls.manager_field_name)
EavEntity.update_attr_cache_for_model(model_cls)
@staticmethod
@ -67,28 +82,37 @@ class EavRegistry(object):
Inject eav features into the given model and attach a signal
listener to it for setup.
"""
if not model_cls.__name__ in EavRegistry.cache:
cls_id = get_unique_class_identifier(model_cls)
if not cls_id in EavRegistry.cache:
return
cache = EavRegistry.cache[model_cls.__name__]
admin_cls = cache['admin_cls']
cache = EavRegistry.cache[cls_id]
config_cls = cache['config_cls']
post_init.disconnect(EavRegistry.attach, sender=model_cls)
post_save.disconnect(EavEntity.save_handler, sender=model_cls)
try:
delattr(model_cls, admin_cls.manager_field_name)
delattr(model_cls, config_cls.manager_field_name)
except AttributeError:
pass
if 'old_mgr' in cache:
cache['old_mgr'].contribute_to_class(model_cls,
admin_cls.manager_field_name)
config_cls.manager_field_name)
try:
delattr(model_cls, admin_cls.proxy_field_name)
delattr(model_cls, config_cls.proxy_field_name)
except AttributeError:
pass
EavRegistry.cache.pop(model_cls.__name__)
EavEntity.flush_attr_cache_for_model(model_cls)
EavRegistry.cache.pop(cls_id)
# todo : test cache
# todo : tst unique identitfier
# todo: test update attribute cache on attribute creation
post_save.connect(EavRegistry.update_attribute_cache, sender=EavAttribute)
post_delete.connect(EavRegistry.update_attribute_cache, sender=EavAttribute)