From 28712d8acfffa9cdabb88cb610bae14913fa185d Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Fri, 15 Feb 2019 01:01:25 -0500 Subject: [PATCH] Refs #19544 -- Ignored auto-created through additions conflicts if supported. This prevents IntegrityError caused by race conditions between missing ids retrieval and bulk insertions. --- django/db/models/fields/related_descriptors.py | 10 ++++++++-- tests/many_to_many/tests.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index 96b8a37583..52cb91d3a8 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -1110,14 +1110,20 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): model=self.model, pk_set=missing_target_ids, using=db, ) - # Add the ones that aren't there already + # Add the ones that aren't there already. Conflicts can be + # ignored when the intermediary model is auto-created as + # the only possible collision is on the (sid_id, tid_id) + # tuple. The same assertion doesn't hold for user-defined + # intermediary models as they could have other fields + # causing conflicts which must be surfaced. + ignore_conflicts = self.through._meta.auto_created is not False self.through._default_manager.using(db).bulk_create([ self.through(**through_defaults, **{ '%s_id' % source_field_name: self.related_val[0], '%s_id' % target_field_name: target_id, }) for target_id in missing_target_ids - ]) + ], ignore_conflicts=ignore_conflicts) if self.reverse or source_field_name == self.source_field_name: # Don't send the signal when we are inserting the diff --git a/tests/many_to_many/tests.py b/tests/many_to_many/tests.py index 933eb23a7a..adde2ac563 100644 --- a/tests/many_to_many/tests.py +++ b/tests/many_to_many/tests.py @@ -117,6 +117,16 @@ class ManyToManyTests(TestCase): ] ) + @skipUnlessDBFeature('supports_ignore_conflicts') + def test_add_ignore_conflicts(self): + manager_cls = self.a1.publications.__class__ + # Simulate a race condition between the missing ids retrieval and + # the bulk insertion attempt. + missing_target_ids = {self.p1.id} + with mock.patch.object(manager_cls, '_get_missing_target_ids', return_value=missing_target_ids) as mocked: + self.a1.publications.add(self.p1) + mocked.assert_called_once() + def test_related_sets(self): # Article objects have access to their related Publication objects. self.assertQuerysetEqual(self.a1.publications.all(), [''])