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 Jacob Walls)

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):  
    155155        self.assertEqual(4, token.permission_set.count())
    156156        self.assertEqual(4, user.permission_set.count())
    157157        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 Jacob Walls, 5 days ago

Description: modified (diff)

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.)

comment:2 by Clifford Gama, 4 days ago

Triage Stage: UnreviewedAccepted

comment:3 by Gregory Mariani, 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 Gregory Mariani, 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 Gregory Mariani, 3 days ago

Owner: set to Gregory Mariani
Status: newassigned

comment:6 by Gregory Mariani, 3 days ago

Has patch: set

comment:7 by Jacob Walls, 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.

Last edited 3 days ago by Jacob Walls (previous) (diff)

comment:8 by Gregory Mariani, 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.
Version 2, edited 3 days ago by Gregory Mariani (previous) (next) (diff)

comment:9 by Jacob Walls, 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 Jacob Walls, 3 days ago

Patch needs improvement: set

comment:11 by Sarah Boyce, 3 days ago

Cc: Csirmaz Bendegúz Lily Foote added
Severity: Release blockerNormal

This also fails on Django 5.1

  • tests/foreign_object/tests.py

    a b class MultiColumnFKTests(TestCase):  
    437437        normal_groups_lists = [list(p.groups.all()) for p in Person.objects.all()]
    438438        self.assertEqual(groups_lists, normal_groups_lists)
    439439
     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
    440449    @translation.override("fi")
    441450    def test_translations(self):
    442451        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 Gregory Mariani, 3 days ago

Patch needs improvement: unset

comment:13 by Jacob Walls, 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 Jacob Walls, 2 days ago

Summary: refresh_from_db() doesn't refresh ForeignObject relationsrefresh_from_db() doesn't clear cached ForeignObject relations
Note: See TracTickets for help on using tickets.
Back to Top