From ed2018037d152eef7e68f339b4562f8aadc2b7a0 Mon Sep 17 00:00:00 2001 From: Hannes Ljungberg Date: Sat, 27 Nov 2021 16:04:39 +0100 Subject: [PATCH] Fixed #33322 -- Fixed loss of assigned related object when saving relation with bulk_update(). --- django/db/models/base.py | 4 +++- django/db/models/query.py | 2 ++ tests/queries/test_bulk_update.py | 32 ++++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index 29bbde5e6b..237a04cfd7 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -931,11 +931,13 @@ class Model(metaclass=ModelBase): using=using, raw=raw, ) - def _prepare_related_fields_for_save(self, operation_name): + def _prepare_related_fields_for_save(self, operation_name, fields=None): # Ensure that a model instance without a PK hasn't been assigned to # a ForeignKey or OneToOneField on this model. If the field is # nullable, allowing the save would result in silent data loss. for field in self._meta.concrete_fields: + if fields and field not in fields: + continue # If the related field isn't cached, then an instance hasn't been # assigned and there's no need to worry about this check. if field.is_relation and field.is_cached(self): diff --git a/django/db/models/query.py b/django/db/models/query.py index fc36945f56..fb6639793a 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -549,6 +549,8 @@ class QuerySet: raise ValueError('bulk_update() cannot be used with primary key fields.') if not objs: return 0 + for obj in objs: + obj._prepare_related_fields_for_save(operation_name='bulk_update', fields=fields) # PK is used twice in the resulting update query, once in the filter # and once in the WHEN. Each field will also have one CAST. connection = connections[self.db] diff --git a/tests/queries/test_bulk_update.py b/tests/queries/test_bulk_update.py index 6ca7f201c1..1d669e0bbd 100644 --- a/tests/queries/test_bulk_update.py +++ b/tests/queries/test_bulk_update.py @@ -7,7 +7,8 @@ from django.test import TestCase, skipUnlessDBFeature from .models import ( Article, CustomDbColumn, CustomPk, Detail, Individual, JSONFieldNullable, - Member, Note, Number, Order, Paragraph, SpecialCategory, Tag, Valid, + Member, Note, Number, Order, Paragraph, RelatedObject, SingleObject, + SpecialCategory, Tag, Valid, ) @@ -250,3 +251,32 @@ class BulkUpdateTests(TestCase): obj.json_field = {'c': obj.json_field['a'] + 1} JSONFieldNullable.objects.bulk_update(objs, ['json_field']) self.assertCountEqual(JSONFieldNullable.objects.filter(json_field__has_key='c'), objs) + + def test_nullable_fk_after_related_save(self): + parent = RelatedObject.objects.create() + child = SingleObject() + parent.single = child + parent.single.save() + RelatedObject.objects.bulk_update([parent], fields=['single']) + self.assertEqual(parent.single_id, parent.single.pk) + parent.refresh_from_db() + self.assertEqual(parent.single, child) + + def test_unsaved_parent(self): + parent = RelatedObject.objects.create() + parent.single = SingleObject() + msg = ( + "bulk_update() prohibited to prevent data loss due to unsaved " + "related object 'single'." + ) + with self.assertRaisesMessage(ValueError, msg): + RelatedObject.objects.bulk_update([parent], fields=['single']) + + def test_unspecified_unsaved_parent(self): + parent = RelatedObject.objects.create() + parent.single = SingleObject() + parent.f = 42 + RelatedObject.objects.bulk_update([parent], fields=['f']) + parent.refresh_from_db() + self.assertEqual(parent.f, 42) + self.assertIsNone(parent.single)