Opened 6 weeks ago
Last modified 3 weeks ago
#36207 assigned Bug
refresh_from_db() doesn't clear cached ForeignObject relations
Description (last modified by ) ¶
Although it's an internal API ForeignObject
is now quasi-public. When trying it out in a non-composite-pk situation I found that refresh_from_db()
didn't clear out a related ForeignObject.
Here is a rough test using the composite_pk models (EDIT: prior version of test wasn't demonstrating anything):
-
TabularUnified tests/composite_pk/test_models.py
diff --git a/tests/composite_pk/test_models.py b/tests/composite_pk/test_models.py index 27157a52ad..7cd97a31a9 100644
a b class CompositePKModelsTests(TestCase): 155 155 self.assertEqual(4, token.permission_set.count()) 156 156 self.assertEqual(4, user.permission_set.count()) 157 157 self.assertEqual(4, comment.permission_set.count()) 158 159 def test_refresh_foreign_object(self): 160 self.comment_1.user = None 161 self.comment_1.refresh_from_db() 162 self.assertEqual(self.comment_1.user, self.user_1)
E ====================================================================== ERROR: test_refresh_foreign_object (composite_pk.test_models.CompositePKModelsTests.test_refresh_foreign_object) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/.../django/tests/composite_pk/test_models.py", line 161, in test_refresh_foreign_object self.comment_1.refresh_from_db() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^ File "/Users/.../py313/lib/python3.13/site-packages/django/db/models/base.py", line 737, in refresh_from_db db_instance = db_instance_qs.get() File "/Users/.../py313/lib/python3.13/site-packages/django/db/models/query.py", line 633, in get raise self.model.DoesNotExist( "%s matching query does not exist." % self.model._meta.object_name ) composite_pk.models.tenant.Comment.DoesNotExist: Comment matching query does not exist. ---------------------------------------------------------------------- Ran 1 test in 0.008s FAILED (errors=1)
According to the ticket's flags, the next step(s) to move this issue forward are:
- For anyone except the patch author to review the patch using the patch review checklist and either mark the ticket as "Ready for checkin" if everything looks good, or leave comments for improvement and mark the ticket as "Patch needs improvement".
Change History (15)
comment:1 by , 6 weeks ago
Description: | modified (diff) |
---|
comment:2 by , 5 weeks ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:3 by , 5 weeks ago
I'll have a look on it. In the interval if someone want to take the ticket go ahead
comment:4 by , 5 weeks ago
@Jacob
Does this test is also correct with the logic you expect ?
def test_refresh_foreign_object(self): # Retrieve the comment via the DB to ensure from_db() is called and _original_pk is set comment = Comment.objects.get(pk=self.comment_1.pk) # Remove the underlying 'user_id' from __dict__ to simulate a cleared ForeignObject cache comment.__dict__.pop("user_id", None) # Call refresh_from_db() to reload the original values from the database comment.refresh_from_db() # Assert that refresh_from_db() has restored 'user' to its original value self.assertEqual(comment.user, self.user_1)
comment:5 by , 5 weeks ago
Owner: | set to |
---|---|
Status: | new → assigned |
comment:6 by , 5 weeks ago
Has patch: | set |
---|
comment:7 by , 5 weeks ago
Thanks for picking this up. I think being able to set .user = None
is important. Maybe we need to adjust this query from .get()
to .filter(...).first()
to support that? The test model I was reusing was not nullable, but you can imagine another test model having a nullable ForeignObject.
comment:8 by , 5 weeks ago
What's the behavior expected ?
Because comment.user = None
will set the value None
and obviously self.assertEqual(comment.user, self.user_1)
will raise with my update:
composite_pk.models.tenant.Comment.user.RelatedObjectDoesNotExist: Comment has no user.
Here the model Comment l30
https://github.com/django/django/blob/51cab4ad51616f8fdb050631be5c710b93685ec3/tests/composite_pk/models/tenant.py
the CASCADE relations are:
from_fields=("tenant_id", "user_id"), to_fields=("tenant_id", "id")
So if we set user_id to None, the relations are destroyed for a ForeignObject.
Maybe I've misunderstood something
comment:9 by , 5 weeks ago
Because comment.user = None will set the value None
But the test case I sketched doesn't save that change to the database, so my expectation was that refresh_from_db()
would return the same thing that a fresh query would return. Or at least this was my expectation based on how ForeignKey
works.
So if we set user_id to None, the relations are destroyed for a ForeignObject.
If this is how ForeignObject is supposed to work, then I feel we need to document it, because from the composite PK guide that I linked it's not obvious. However my impression is that it's not supposed to work like .add()
, which immediately alters data.
comment:10 by , 5 weeks ago
Patch needs improvement: | set |
---|
comment:11 by , 5 weeks ago
Cc: | added |
---|---|
Severity: | Release blocker → Normal |
This also fails on Django 5.1
-
TabularUnified tests/foreign_object/tests.py
a b class MultiColumnFKTests(TestCase): 437 437 normal_groups_lists = [list(p.groups.all()) for p in Person.objects.all()] 438 438 self.assertEqual(groups_lists, normal_groups_lists) 439 439 440 def test_refresh_foreign_object(self): 441 member = Membership.objects.create( 442 membership_country=self.usa, person=self.bob, group_id=None 443 ) 444 self.assertEqual(member.person, self.bob) 445 member.person = None 446 member.refresh_from_db() 447 self.assertEqual(member.person, self.bob) 448 440 449 @translation.override("fi") 441 450 def test_translations(self): 442 451 a1 = Article.objects.create(pub_date=datetime.date.today())
We documented that this is an internal API, and therefore not supported by our deprecation policy.
I don't think this is supported by our "new feature in 5.2" policy of back porting bug fixes. I am therefore demoting this from a release blocker to "normal".
If this note implies this should work "perfectly", we should remove the suggestion to use ForeignObject
.
#35956 aims to add ForeignKey support.
comment:12 by , 5 weeks ago
Patch needs improvement: | unset |
---|
comment:13 by , 5 weeks ago
Patch needs improvement: | set |
---|
I think I confounded things by involving None
in my draft test. I didn't notice that both sides of the relation involved CompositePrimaryKey. Django doesn't support setting a model's PK to None and then refreshing from db--and I'm not suggesting to add support for that.
I left a review on the PR improve the assertion and suggest an alternative approach. It works for me, but some further investigation/validation would be very welcome.
comment:14 by , 5 weeks ago
Summary: | refresh_from_db() doesn't refresh ForeignObject relations → refresh_from_db() doesn't clear cached ForeignObject relations |
---|
comment:15 by , 3 weeks ago
Patch needs improvement: | unset |
---|
I opened a PR with my proposed alternative.
The variant where I found this didn't cause
refresh_from_db()
to throw, it just silently kept the old value. (This was with a nullable ForeignObject.)