Skip to content

Commit e03e5c7

Browse files
charettessarahboyce
authored andcommitted
Fixed #33312 -- Raised explicit exception when copying deferred model instances.
Previously save() would crash with an attempted forced update message, and both save(force_insert=True) and bulk_create() would crash with DoesNotExist errors trying to retrieve rows with an empty primary key (id IS NULL). Implementing deferred field model instance copying might be doable in certain cases (e.g. when all the deferred fields are db generated) but that's not trivial to implement in a backward compatible way. Thanks Adam Sołtysik for the report and test and Clifford for the review.
1 parent 0b2ed4f commit e03e5c7

File tree

5 files changed

+42
-3
lines changed

5 files changed

+42
-3
lines changed

django/db/models/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,7 @@ def save(
859859
not force_insert
860860
and deferred_non_generated_fields
861861
and using == self._state.db
862+
and self._is_pk_set()
862863
):
863864
field_names = set()
864865
pk_fields = self._meta.pk_fields

django/db/models/query_utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,10 @@ def __get__(self, instance, cls=None):
220220
# might be able to reuse the already loaded value. Refs #18343.
221221
val = self._check_parent_chain(instance)
222222
if val is None:
223-
if not instance._is_pk_set() and self.field.generated:
223+
if not instance._is_pk_set():
224224
raise AttributeError(
225-
"Cannot read a generated field from an unsaved model."
225+
f"Cannot retrieve deferred field {field_name!r} "
226+
"from an unsaved model."
226227
)
227228
instance.refresh_from_db(fields=[field_name])
228229
else:

docs/topics/db/queries.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,6 +1590,21 @@ For example, assuming ``entry`` is already duplicated as above::
15901590
detail.entry = entry
15911591
detail.save()
15921592

1593+
Note that it is not possible to copy instances of models with deferred fields
1594+
using this pattern unless values are assigned to them:
1595+
1596+
.. code-block:: pycon
1597+
1598+
>>> blog = Blog.objects.defer("name")[0]
1599+
>>> blog.pk = None
1600+
>>> blog._state.adding = True
1601+
>>> blog.save()
1602+
Traceback (most recent call last):
1603+
...
1604+
AttributeError: Cannot retrieve deferred field 'name' from an unsaved model.
1605+
>>> blog.name = "Another Blog"
1606+
>>> blog.save()
1607+
15931608
.. _topics-db-queries-update:
15941609

15951610
Updating multiple objects at once

tests/defer_regress/tests.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,25 @@ def test_delete_defered_proxy_model(self):
366366
Proxy.objects.only("value").get(pk=self.item_pk).delete()
367367
self.assertEqual(self.pre_delete_senders, [Proxy])
368368
self.assertEqual(self.post_delete_senders, [Proxy])
369+
370+
371+
class DeferCopyInstanceTests(TestCase):
372+
@classmethod
373+
def setUpTestData(cls):
374+
SimpleItem.objects.create(name="test", value=42)
375+
cls.deferred_item = SimpleItem.objects.defer("value").first()
376+
cls.deferred_item.pk = None
377+
cls.deferred_item._state.adding = True
378+
cls.expected_msg = (
379+
"Cannot retrieve deferred field 'value' from an unsaved model."
380+
)
381+
382+
def test_save(self):
383+
with self.assertRaisesMessage(AttributeError, self.expected_msg):
384+
self.deferred_item.save(force_insert=True)
385+
with self.assertRaisesMessage(AttributeError, self.expected_msg):
386+
self.deferred_item.save()
387+
388+
def test_bulk_create(self):
389+
with self.assertRaisesMessage(AttributeError, self.expected_msg):
390+
SimpleItem.objects.bulk_create([self.deferred_item])

tests/model_fields/test_generatedfield.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def _refresh_if_needed(self, m):
180180

181181
def test_unsaved_error(self):
182182
m = self.base_model(a=1, b=2)
183-
msg = "Cannot read a generated field from an unsaved model."
183+
msg = "Cannot retrieve deferred field 'field' from an unsaved model."
184184
with self.assertRaisesMessage(AttributeError, msg):
185185
m.field
186186

0 commit comments

Comments
 (0)