-
-
Notifications
You must be signed in to change notification settings - Fork 32.6k
Fixed #36207 -- Cleared cached ForeignObject relations via refresh_from_db(). #19245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed #36207 -- Cleared cached ForeignObject relations via refresh_from_db(). #19245
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test passes and actually test the behavior (so it fails on master but pass on the branch PR).
The code is similar to the surrounding code and looks good
django/db/models/options.py
Outdated
@cached_property | ||
def virtual_relations(self): | ||
""" | ||
Return many-to-one related objects that may be virtual, i.e. using | ||
ForeignObjectRel without ManyToOneRel (ForeignKey). | ||
|
||
Private API intended only to be used by Django itself. | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be added to FORWARD_PROPERTIES
so the cache can be expired? I think it does because it invokes _get_fields()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to add context to my earlier note — I did a deeper dive into how FORWARD_PROPERTIES
affects cache invalidation in practice.
Removing most field-related entries from the set didn’t cause any test failures. This means missing test coverage, no?
The only field-related entry whose removal did cause test failures was _forward_fields_map
, which broke two tests: schema.tests.SchemaTests.test_m2m_repoint_*
with RuntimeWarning: Model 'schema.localbookwithm2m_tags' was already registered. Reloading models is not advised as it can lead to inconsistencies, most notably with related models.
. I haven’t pinned down the exact reasons, but it likely stems from _get_field()
, which builds the forward field map being invoked during the final stages of model preparation, via Options._prepare()
.
Cache expiration (for field-related properties) is done by Options.add_field()
, but this is only called during model class creation, via Field.contribute_to_class()
, which is probably why this is mostly untested/ difficult to test.
By contrast, removing manager-related entries did break tests, confirming that those properties are actively used at runtime and need proper invalidation.
While omitting virtual_relations
doesn’t break anything now, it does call _get_fields()
and could theoretically return stale results if the model were mutated. That said, it’s hard to write a test for this without doing post-setup mutation (). Still, we might want to explore better test coverage for this area overall.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, great catch. We also appear to be missing db_returning_fields
, possibly others. Would you be willing to open a ticket with your findings about the test coverage gap & need for a testing strategy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added "virtual_relations"
to FORWARD_PROPERTIES
and squashed it in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @jacobtylerwalls for the updates!
Would you be willing to open a ticket with your findings about the test coverage gap & need for a testing strategy?
I'm happy to open a ticket to highlight these findings, though I don't yet have a concrete testing strategy.
Given the uncertainty around testing FORWARD_PROPERTIES
, I think this is ready for a merger's review - marking as RFC. It's up to their discretion whether adding an entry to this (largely untested) set is acceptable, despite the testing challenges.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened ticket-36369
32f5bbc
to
324cf32
Compare
django/db/models/base.py
Outdated
@@ -753,7 +753,7 @@ def refresh_from_db(self, using=None, fields=None, from_queryset=None): | |||
|
|||
db_instance = db_instance_qs.get() | |||
non_loaded_fields = db_instance.get_deferred_fields() | |||
for field in self._meta.concrete_fields: | |||
for field in chain(self._meta.concrete_fields, self._meta.virtual_relations): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for field in chain(self._meta.concrete_fields, self._meta.virtual_relations): | |
for field in self._meta.fields: |
I was wondering why we can't do the above. Virtual fields to me are non-concrete fields, so this would include some virtual fields that are not relations but not sure if that's an issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I should have worked out that those two were complements of each other. Nice!
And extra nice to avoid adding this new property, since I now see it was removed once already in 933dc62 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, I noticed while playing with this model -- if there are multiple foreign objects pointing to the same column (e.g. through inheritance), multiple identical queries will be issued on refresh. I'm going to see if that's what the "cached relations" bit just below this is supposed to be handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a little test to demonstrate. Guarding the setattr
seems to prevent new behavior changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How should I see the multiple queries issue? I can see that when I revert the guard, then I get an error running the new test:
raise FieldDoesNotExist(
django.core.exceptions.FieldDoesNotExist: BasePerson has no field named 'abstract_non_concrete_id'
But not sure where the multiple queries come to play (this is SQLite).
I'm hoping instead of adding a test to where we are testing model meta, perhaps we can modify the Membership
model instead:
--- a/tests/foreign_object/models/person.py
+++ b/tests/foreign_object/models/person.py
@@ -68,6 +68,13 @@ class Membership(models.Model):
to_fields=["id", "group_country"],
on_delete=models.CASCADE,
)
+ not_concrete_country = models.ForeignObject(
+ Country,
+ on_delete=models.CASCADE,
+ from_fields=["non_concrete_id"], # Field does not exist.
+ to_fields=["id"],
+ related_name="non_concrete_country",
+ )
class Meta:
ordering = ("date_joined", "invite_reason")
This is enough of a change for the same error, but still have an open question on the queries to know if this is "enough".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How should I see the multiple queries issue?
Ah, I forgot to push up the edits I made to those models. I had to add the missing fields, e.g. the one you marked here as "does not exist". I think this wouldn't function otherwise? I suppose they're missing because that file just has SimpleTestCase
s and isn't exercising queries?
I can just move the self.assertNumQueries to my original test and have coverage without touching any of these models.
ffd305f
to
cec9568
Compare
cec9568
to
dc2b083
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! This looks great ⭐
Trac ticket number
ticket-36207
Branch description
Before, model fields using the private API
models.ForeignObject
to relate foreign objects without enforcing a foreign key (as modeled in the composite pk docs) didn't have their relations cleared viarefresh_from_db()
.Alternative to #19207
Checklist
main
branch.