diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 56cee83a5e..f6c5ae2585 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -19,9 +19,9 @@ from django.utils.translation import gettext_lazy as _ from . import Field from .mixins import FieldCacheMixin from .related_descriptors import ( - ForwardManyToOneDescriptor, ForwardOneToOneDescriptor, - ManyToManyDescriptor, ReverseManyToOneDescriptor, - ReverseOneToOneDescriptor, + ForeignKeyDeferredAttribute, ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, ManyToManyDescriptor, + ReverseManyToOneDescriptor, ReverseOneToOneDescriptor, ) from .related_lookups import ( RelatedExact, RelatedGreaterThan, RelatedGreaterThanOrEqual, RelatedIn, @@ -764,7 +764,7 @@ class ForeignKey(ForeignObject): By default ForeignKey will target the pk of the remote model but this behavior can be changed by using the ``to_field`` argument. """ - + descriptor_class = ForeignKeyDeferredAttribute # Field flags many_to_many = False many_to_one = True diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index 886e99e190..0b51b68ebe 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -67,9 +67,17 @@ from django.core.exceptions import FieldError from django.db import connections, router, transaction from django.db.models import Q, signals from django.db.models.query import QuerySet +from django.db.models.query_utils import DeferredAttribute from django.utils.functional import cached_property +class ForeignKeyDeferredAttribute(DeferredAttribute): + def __set__(self, instance, value): + if instance.__dict__.get(self.field.attname) != value and self.field.is_cached(instance): + self.field.delete_cached_value(instance) + instance.__dict__[self.field.attname] = value + + class ForwardManyToOneDescriptor: """ Accessor to the related object on the forward side of a many-to-one or diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index 07a2396c80..30f70e0bcf 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -490,6 +490,10 @@ Miscellaneous * ``intword`` template filter now translates ``1.0`` as a singular phrase and all other numeric values as plural. This may be incorrect for some languages. +* Assigning a value to a model's :class:`~django.db.models.ForeignKey` or + :class:`~django.db.models.OneToOneField` ``'_id'`` attribute now unsets the + corresponding field. Accessing the field afterwards will result in a query. + .. _deprecated-features-3.0: Features deprecated in 3.0 diff --git a/tests/many_to_one/tests.py b/tests/many_to_one/tests.py index a5215e58d7..e3121a7701 100644 --- a/tests/many_to_one/tests.py +++ b/tests/many_to_one/tests.py @@ -168,6 +168,18 @@ class ManyToOneTests(TestCase): parent.bestchild_id = child2.pk self.assertTrue(Parent.bestchild.is_cached(parent)) + def test_assign_fk_id_none(self): + parent = Parent.objects.create(name='jeff') + child = Child.objects.create(name='frank', parent=parent) + parent.bestchild = child + parent.save() + parent.bestchild_id = None + parent.save() + self.assertIsNone(parent.bestchild_id) + self.assertFalse(Parent.bestchild.is_cached(parent)) + self.assertIsNone(parent.bestchild) + self.assertTrue(Parent.bestchild.is_cached(parent)) + def test_selects(self): self.r.article_set.create(headline="John's second story", pub_date=datetime.date(2005, 7, 29)) self.r2.article_set.create(headline="Paul's story", pub_date=datetime.date(2006, 1, 17)) diff --git a/tests/one_to_one/tests.py b/tests/one_to_one/tests.py index 78b3ff1135..1215cf8001 100644 --- a/tests/one_to_one/tests.py +++ b/tests/one_to_one/tests.py @@ -217,6 +217,15 @@ class OneToOneTests(TestCase): b.place_id = self.p2.pk self.assertTrue(UndergroundBar.place.is_cached(b)) + def test_assign_o2o_id_none(self): + b = UndergroundBar.objects.create(place=self.p1) + b.place_id = None + b.save() + self.assertIsNone(b.place_id) + self.assertFalse(UndergroundBar.place.is_cached(b)) + self.assertIsNone(b.place) + self.assertTrue(UndergroundBar.place.is_cached(b)) + def test_related_object_cache(self): """ Regression test for #6886 (the related-object cache) """