Refs #16508 -- Added invalidation of stale cached instances of GenericForeignKey targets.
This commit is contained in:
parent
7ec330eeb9
commit
b9f8635f58
|
@ -208,7 +208,7 @@ class GenericForeignKey(object):
|
||||||
lambda obj: (obj._get_pk_val(), obj.__class__),
|
lambda obj: (obj._get_pk_val(), obj.__class__),
|
||||||
gfk_key,
|
gfk_key,
|
||||||
True,
|
True,
|
||||||
self.cache_attr)
|
self.name)
|
||||||
|
|
||||||
def is_cached(self, instance):
|
def is_cached(self, instance):
|
||||||
return hasattr(instance, self.cache_attr)
|
return hasattr(instance, self.cache_attr)
|
||||||
|
@ -217,21 +217,30 @@ class GenericForeignKey(object):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
try:
|
# Don't use getattr(instance, self.ct_field) here because that might
|
||||||
return getattr(instance, self.cache_attr)
|
# reload the same ContentType over and over (#5570). Instead, get the
|
||||||
except AttributeError:
|
# content type ID here, and later when the actual instance is needed,
|
||||||
rel_obj = None
|
# use ContentType.objects.get_for_id(), which has a global cache.
|
||||||
|
|
||||||
# Make sure to use ContentType.objects.get_for_id() to ensure that
|
|
||||||
# lookups are cached (see ticket #5570). This takes more code than
|
|
||||||
# the naive ``getattr(instance, self.ct_field)``, but has better
|
|
||||||
# performance when dealing with GFKs in loops and such.
|
|
||||||
f = self.model._meta.get_field(self.ct_field)
|
f = self.model._meta.get_field(self.ct_field)
|
||||||
ct_id = getattr(instance, f.get_attname(), None)
|
ct_id = getattr(instance, f.get_attname(), None)
|
||||||
|
pk_val = getattr(instance, self.fk_field)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rel_obj = getattr(instance, self.cache_attr)
|
||||||
|
except AttributeError:
|
||||||
|
rel_obj = None
|
||||||
|
else:
|
||||||
|
if rel_obj and (ct_id != self.get_content_type(obj=rel_obj, using=instance._state.db).id or
|
||||||
|
rel_obj._meta.pk.to_python(pk_val) != rel_obj._get_pk_val()):
|
||||||
|
rel_obj = None
|
||||||
|
|
||||||
|
if rel_obj is not None:
|
||||||
|
return rel_obj
|
||||||
|
|
||||||
if ct_id is not None:
|
if ct_id is not None:
|
||||||
ct = self.get_content_type(id=ct_id, using=instance._state.db)
|
ct = self.get_content_type(id=ct_id, using=instance._state.db)
|
||||||
try:
|
try:
|
||||||
rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
|
rel_obj = ct.get_object_for_this_type(pk=pk_val)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
setattr(instance, self.cache_attr, rel_obj)
|
setattr(instance, self.cache_attr, rel_obj)
|
||||||
|
|
|
@ -555,6 +555,23 @@ id="id_generic_relations-taggeditem-content_type-object_id-1-id" /></p>""" % tag
|
||||||
with self.assertRaises(IntegrityError):
|
with self.assertRaises(IntegrityError):
|
||||||
TaggedItem.objects.create(tag="shiny", content_object=quartz)
|
TaggedItem.objects.create(tag="shiny", content_object=quartz)
|
||||||
|
|
||||||
|
def test_cache_invalidation_for_content_type_id(self):
|
||||||
|
# Create a Vegetable and Mineral with the same id.
|
||||||
|
new_id = max(Vegetable.objects.order_by('-id')[0].id,
|
||||||
|
Mineral.objects.order_by('-id')[0].id) + 1
|
||||||
|
broccoli = Vegetable.objects.create(id=new_id, name="Broccoli")
|
||||||
|
diamond = Mineral.objects.create(id=new_id, name="Diamond", hardness=7)
|
||||||
|
tag = TaggedItem.objects.create(content_object=broccoli, tag="yummy")
|
||||||
|
tag.content_type = ContentType.objects.get_for_model(diamond)
|
||||||
|
self.assertEqual(tag.content_object, diamond)
|
||||||
|
|
||||||
|
def test_cache_invalidation_for_object_id(self):
|
||||||
|
broccoli = Vegetable.objects.create(name="Broccoli")
|
||||||
|
cauliflower = Vegetable.objects.create(name="Cauliflower")
|
||||||
|
tag = TaggedItem.objects.create(content_object=broccoli, tag="yummy")
|
||||||
|
tag.object_id = cauliflower.id
|
||||||
|
self.assertEqual(tag.content_object, cauliflower)
|
||||||
|
|
||||||
|
|
||||||
class CustomWidget(forms.TextInput):
|
class CustomWidget(forms.TextInput):
|
||||||
pass
|
pass
|
||||||
|
|
Loading…
Reference in New Issue