Skip to content

Commit a8a81aa

Browse files
committed
Fixed django#18343 -- Cleaned up deferred model implementation
Generic cleanup and dead code removal in deferred model field loading and model.__reduce__(). Also fixed an issue where if an inherited model with a parent field chain parent_ptr_id -> id would be deferred loaded, then accessing the id field caused caused a database query, even if the id field's value is already loaded in the parent_ptr_id field.
1 parent 7a4233b commit a8a81aa

File tree

4 files changed

+52
-24
lines changed

4 files changed

+52
-24
lines changed

django/db/models/base.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -404,20 +404,14 @@ def __reduce__(self):
404404
# and as a result, the super call will cause an infinite recursion.
405405
# See #10547 and #12121.
406406
defers = []
407-
pk_val = None
408407
if self._deferred:
409408
from django.db.models.query_utils import deferred_class_factory
410409
factory = deferred_class_factory
411410
for field in self._meta.fields:
412411
if isinstance(self.__class__.__dict__.get(field.attname),
413412
DeferredAttribute):
414413
defers.append(field.attname)
415-
if pk_val is None:
416-
# The pk_val and model values are the same for all
417-
# DeferredAttribute classes, so we only need to do this
418-
# once.
419-
obj = self.__class__.__dict__[field.attname]
420-
model = obj.model_ref()
414+
model = self._meta.proxy_for_model
421415
else:
422416
factory = simple_class_factory
423417
return (model_unpickle, (model, defers, factory), data)

django/db/models/query_utils.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
circular import difficulties.
77
"""
88

9-
import weakref
10-
119
from django.db.backends import util
1210
from django.utils import tree
1311

@@ -70,36 +68,39 @@ class DeferredAttribute(object):
7068
"""
7169
def __init__(self, field_name, model):
7270
self.field_name = field_name
73-
self.model_ref = weakref.ref(model)
74-
self.loaded = False
7571

7672
def __get__(self, instance, owner):
7773
"""
7874
Retrieves and caches the value from the datastore on the first lookup.
7975
Returns the cached value.
8076
"""
8177
from django.db.models.fields import FieldDoesNotExist
78+
non_deferred_model = instance._meta.proxy_for_model
79+
opts = non_deferred_model._meta
8280

8381
assert instance is not None
84-
cls = self.model_ref()
8582
data = instance.__dict__
8683
if data.get(self.field_name, self) is self:
8784
# self.field_name is the attname of the field, but only() takes the
8885
# actual name, so we need to translate it here.
8986
try:
90-
cls._meta.get_field_by_name(self.field_name)
91-
name = self.field_name
87+
f = opts.get_field_by_name(self.field_name)[0]
9288
except FieldDoesNotExist:
93-
name = [f.name for f in cls._meta.fields
94-
if f.attname == self.field_name][0]
95-
# We use only() instead of values() here because we want the
96-
# various data coersion methods (to_python(), etc.) to be called
97-
# here.
98-
val = getattr(
99-
cls._base_manager.filter(pk=instance.pk).only(name).using(
100-
instance._state.db).get(),
101-
self.field_name
102-
)
89+
f = [f for f in opts.fields
90+
if f.attname == self.field_name][0]
91+
name = f.name
92+
# Lets see if the field is part of the parent chain. If so we
93+
# might be able to reuse the already loaded value. Refs #18343.
94+
val = self._check_parent_chain(instance, name)
95+
if val is None:
96+
# We use only() instead of values() here because we want the
97+
# various data coersion methods (to_python(), etc.) to be
98+
# called here.
99+
val = getattr(
100+
non_deferred_model._base_manager.only(name).using(
101+
instance._state.db).get(pk=instance.pk),
102+
self.field_name
103+
)
103104
data[self.field_name] = val
104105
return data[self.field_name]
105106

@@ -110,6 +111,20 @@ def __set__(self, instance, value):
110111
"""
111112
instance.__dict__[self.field_name] = value
112113

114+
def _check_parent_chain(self, instance, name):
115+
"""
116+
Check if the field value can be fetched from a parent field already
117+
loaded in the instance. This can be done if the to-be fetched
118+
field is a primary key field.
119+
"""
120+
opts = instance._meta
121+
f = opts.get_field_by_name(name)[0]
122+
link_field = opts.get_ancestor_link(f.model)
123+
if f.primary_key and f != link_field:
124+
return getattr(instance, link_field.attname)
125+
return None
126+
127+
113128
def select_related_descend(field, restricted, requested, reverse=False):
114129
"""
115130
Returns True if this field should be used to descend deeper for

tests/modeltests/defer/tests.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,18 @@ def test_defer_proxy(self):
158158
self.assert_delayed(child, 1)
159159
self.assertEqual(child.name, 'p1')
160160
self.assertEqual(child.value, 'xx')
161+
162+
def test_defer_inheritance_pk_chaining(self):
163+
"""
164+
When an inherited model is fetched from the DB, its PK is also fetched.
165+
When getting the PK of the parent model it is useful to use the already
166+
fetched parent model PK if it happens to be available. Tests that this
167+
is done.
168+
"""
169+
s1 = Secondary.objects.create(first="x1", second="y1")
170+
bc = BigChild.objects.create(name="b1", value="foo", related=s1,
171+
other="bar")
172+
bc_deferred = BigChild.objects.only('name').get(pk=bc.pk)
173+
with self.assertNumQueries(0):
174+
bc_deferred.id
175+
self.assertEqual(bc_deferred.pk, bc_deferred.id)

tests/modeltests/field_subclassing/tests.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def test_defer(self):
1717
self.assertTrue(isinstance(d.data, list))
1818
self.assertEqual(d.data, [1, 2, 3])
1919

20+
d = DataModel.objects.defer("data").get(pk=d.pk)
21+
self.assertTrue(isinstance(d.data, list))
22+
self.assertEqual(d.data, [1, 2, 3])
23+
# Refetch for save
2024
d = DataModel.objects.defer("data").get(pk=d.pk)
2125
d.save()
2226

0 commit comments

Comments
 (0)