Handle deferred fields in FieldTracker.has_changed() and previous() #313
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Problem
Currently
has_changed()
andprevious()
do not return correct values when working with Django deferred fields.previous()
always returnsNone
, even if the deferred value in the database is notNone
.has_changed()
always returnsTrue
, even if the field has not changed. It also has the side-effect of unnecessarily fetching deferred fields from the database.This means that calling
tracker.changed()
unnecessarily loads all deferred fields, which can have a meaningful performance impact.Here is a simplified real-life example that shows why this would be useful to fix:
The goal here is to use
defer()
combined withtracker.changed()
to avoid fetching and re-savinggiant_text_field
unless necessary, which has a huge performance impact for this kind of code. But becausehas_changed()
always returnsTrue
for deferred fields, this code will fetch and store an unchangedgiant_text_field
for every instance.Solution
I updated
FieldTrackerTests.test_with_deferred()
to show the different waysdefer()
can interact withhas_changed()
andprevious()
. I then updatedhas_changed()
andprevious()
to handle those cases. The new behavior is thatprevious()
returns the correct previous value for a deferred field, instead ofNone
, andhas_changed()
correctly returns whether the field has had a different value assigned to it, while avoiding unnecessary database access.Backwards compatibility
The new code will not affect anyone who is not using FieldTracker with deferred fields.
The change could impact existing users if they are relying on the old behavior, in particular if they are relying on
previous()
always returningNone
for deferred values. I don't think it would make sense to do that, but maybe it's possible?I did not attempt to apply these fixed for the old
ModelTracker
.Implementation details/coding decisions
There are two cases we have to handle here. The simpler is the read-only case: an object is fetched with
defer()
and then the tracker is consulted, without any local assignments to the deferred field. In that case we can just return False fromhas_changed()
, because a deferred field by definition hasn't changed. Forprevious()
we can just examine the value of the field and thereby un-defer it.Things get more complicated if local code writes to the deferred field, as in my
instance.giant_text_field =
example above. If local code has written a value, thenhas_changed()
should go ahead with the comparison usingprevious()
. To return a correct result,previous()
then has to actually fetch the old value from the database to find out if the new value is different.So that leads to two implementation details. First, to test whether local code has written to the deferred field, I'm checking whether the field is in
instance.__dict__
, which is the test used by Django'sget_deferred_fields()
and thus is probably correct.Second, to fetch the old value from the database after assignment, I'm using Django's
refresh_from_db()
function with a temp variable to shuffle around the values. This is messy, but I think cleaner than doing the fetch ourselves and re-implementing all the cases handled byrefresh_from_db()
.Commandments
CHANGES.rst
file to describe the changes, and quote according issue withGH-<issue_number>
.