Opened 5 days ago
Last modified 2 days ago
#36207 assigned Bug
refresh_from_db() doesn't clear cached ForeignObject relations
Reported by: | Jacob Walls | Owned by: | Gregory Mariani |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 5.2 |
Severity: | Normal | Keywords: | |
Cc: | Csirmaz Bendegúz, Lily Foote | Triage Stage: | Accepted |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | yes |
Easy pickings: | no | UI/UX: | no |
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):
-
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)
Change History (14)
comment:1 by , 5 days ago
Description: | modified (diff) |
---|
comment:2 by , 4 days ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:3 by , 3 days ago
I'll have a look on it. In the interval if someone want to take the ticket go ahead
comment:4 by , 3 days 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 , 3 days ago
Owner: | set to |
---|---|
Status: | new → assigned |
comment:6 by , 3 days ago
Has patch: | set |
---|
comment:7 by , 3 days 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 , 3 days 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 relation are destroyed
comment:9 by , 3 days 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 , 3 days ago
Patch needs improvement: | set |
---|
comment:11 by , 3 days ago
Cc: | added |
---|---|
Severity: | Release blocker → Normal |
This also fails on Django 5.1
-
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 , 3 days ago
Patch needs improvement: | unset |
---|
comment:13 by , 2 days 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 , 2 days ago
Summary: | refresh_from_db() doesn't refresh ForeignObject relations → refresh_from_db() doesn't clear cached ForeignObject relations |
---|
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.)