diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 57392fd62f..529898ea27 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -112,9 +112,9 @@ class RelatedField(object): def do_related_class(self, other, cls): self.set_attributes_from_rel() - related = RelatedObject(other, cls, self) + self.related = RelatedObject(other, cls, self) if not cls._meta.abstract: - self.contribute_to_related_class(other, related) + self.contribute_to_related_class(other, self.related) def get_db_prep_lookup(self, lookup_type, value): # If we are doing a lookup on a Related Field, we must be @@ -184,7 +184,6 @@ class SingleRelatedObjectDescriptor(object): def __get__(self, instance, instance_type=None): if instance is None: return self - try: return getattr(instance, self.cache_name) except AttributeError: @@ -232,6 +231,7 @@ class ReverseSingleRelatedObjectDescriptor(object): def __get__(self, instance, instance_type=None): if instance is None: return self + cache_name = self.field.get_cache_name() try: return getattr(instance, cache_name) @@ -272,6 +272,29 @@ class ReverseSingleRelatedObjectDescriptor(object): (value, instance._meta.object_name, self.field.name, self.field.rel.to._meta.object_name)) + # If we're setting the value of a OneToOneField to None, we need to clear + # out the cache on any old related object. Otherwise, deleting the + # previously-related object will also cause this object to be deleted, + # which is wrong. + if value is None: + # Look up the previously-related object, which may still be available + # since we've not yet cleared out the related field. + # Use the cache directly, instead of the accessor; if we haven't + # populated the cache, then we don't care - we're only accessing + # the object to invalidate the accessor cache, so there's no + # need to populate the cache just to expire it again. + related = getattr(instance, self.field.get_cache_name(), None) + + # If we've got an old related object, we need to clear out its + # cache. This cache also might not exist if the related object + # hasn't been accessed yet. + if related: + cache_name = '_%s_cache' % self.field.related.get_accessor_name() + try: + delattr(related, cache_name) + except AttributeError: + pass + # Set the value of the related field try: val = getattr(value, self.field.rel.get_related_field().attname) diff --git a/tests/regressiontests/one_to_one_regress/tests.py b/tests/regressiontests/one_to_one_regress/tests.py new file mode 100644 index 0000000000..a0a0c0584f --- /dev/null +++ b/tests/regressiontests/one_to_one_regress/tests.py @@ -0,0 +1,22 @@ +from django.test import TestCase +from regressiontests.one_to_one_regress.models import Place, UndergroundBar + +class OneToOneDeletionTests(TestCase): + def test_reverse_relationship_cache_cascade(self): + """ + Regression test for #9023: accessing the reverse relationship shouldn't + result in a cascading delete(). + """ + place = Place.objects.create(name="Dempsey's", address="623 Vermont St") + bar = UndergroundBar.objects.create(place=place, serves_cocktails=False) + + # The bug in #9023: if you access the one-to-one relation *before* + # setting to None and deleting, the cascade happens anyway. + place.undergroundbar + bar.place.name='foo' + bar.place = None + bar.save() + place.delete() + + self.assertEqual(Place.objects.all().count(), 0) + self.assertEqual(UndergroundBar.objects.all().count(), 1)