Refs #19544 -- Added a fast path for through additions if supported.
The single query insertion path is taken if the backend supports inserts that ignore conflicts and m2m_changed signals don't have to be sent.
This commit is contained in:
parent
28712d8acf
commit
de7f6b51b2
|
@ -1051,10 +1051,9 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
|||
return obj, created
|
||||
update_or_create.alters_data = True
|
||||
|
||||
def _get_missing_target_ids(self, source_field_name, target_field_name, db, objs):
|
||||
def _get_target_ids(self, target_field_name, objs):
|
||||
"""
|
||||
Return the subset of ids of `objs` that aren't already assigned to
|
||||
this relationship.
|
||||
Return the set of ids of `objs` that the target field references.
|
||||
"""
|
||||
from django.db.models import Model
|
||||
target_ids = set()
|
||||
|
@ -1081,6 +1080,13 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
|||
)
|
||||
else:
|
||||
target_ids.add(obj)
|
||||
return target_ids
|
||||
|
||||
def _get_missing_target_ids(self, source_field_name, target_field_name, db, target_ids):
|
||||
"""
|
||||
Return the subset of ids of `objs` that aren't already assigned to
|
||||
this relationship.
|
||||
"""
|
||||
vals = self.through._default_manager.using(db).values_list(
|
||||
target_field_name, flat=True
|
||||
).filter(**{
|
||||
|
@ -1089,6 +1095,35 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
|||
})
|
||||
return target_ids.difference(vals)
|
||||
|
||||
def _get_add_plan(self, db, source_field_name):
|
||||
"""
|
||||
Return a boolean triple of the way the add should be performed.
|
||||
|
||||
The first element is whether or not bulk_create(ignore_conflicts)
|
||||
can be used, the second whether or not signals must be sent, and
|
||||
the third element is whether or not the immediate bulk insertion
|
||||
with conflicts ignored can be performed.
|
||||
"""
|
||||
# Conflicts can be ignored when the intermediary model is
|
||||
# auto-created as the only possible collision is on the
|
||||
# (source_id, target_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.
|
||||
can_ignore_conflicts = (
|
||||
connections[db].features.supports_ignore_conflicts and
|
||||
self.through._meta.auto_created is not False
|
||||
)
|
||||
# Don't send the signal when inserting duplicate data row
|
||||
# for symmetrical reverse entries.
|
||||
must_send_signals = (self.reverse or source_field_name == self.source_field_name) and (
|
||||
signals.m2m_changed.has_listeners(self.through)
|
||||
)
|
||||
# Fast addition through bulk insertion can only be performed
|
||||
# if no m2m_changed listeners are connected for self.through
|
||||
# as they require the added set of ids to be provided via
|
||||
# pk_set.
|
||||
return can_ignore_conflicts, must_send_signals, (can_ignore_conflicts and not must_send_signals)
|
||||
|
||||
def _add_items(self, source_field_name, target_field_name, *objs, through_defaults=None):
|
||||
# source_field_name: the PK fieldname in join table for the source object
|
||||
# target_field_name: the PK fieldname in join table for the target object
|
||||
|
@ -1097,37 +1132,40 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
|||
|
||||
# If there aren't any objects, there is nothing to do.
|
||||
if objs:
|
||||
target_ids = self._get_target_ids(target_field_name, objs)
|
||||
db = router.db_for_write(self.through, instance=self.instance)
|
||||
missing_target_ids = self._get_missing_target_ids(source_field_name, target_field_name, db, objs)
|
||||
can_ignore_conflicts, must_send_signals, can_fast_add = self._get_add_plan(db, source_field_name)
|
||||
if can_fast_add:
|
||||
self.through._default_manager.using(db).bulk_create([
|
||||
self.through(**{
|
||||
'%s_id' % source_field_name: self.related_val[0],
|
||||
'%s_id' % target_field_name: target_id,
|
||||
})
|
||||
for target_id in target_ids
|
||||
], ignore_conflicts=True)
|
||||
return
|
||||
|
||||
missing_target_ids = self._get_missing_target_ids(
|
||||
source_field_name, target_field_name, db, target_ids
|
||||
)
|
||||
with transaction.atomic(using=db, savepoint=False):
|
||||
if self.reverse or source_field_name == self.source_field_name:
|
||||
# Don't send the signal when we are inserting the
|
||||
# duplicate data row for symmetrical reverse entries.
|
||||
if must_send_signals:
|
||||
signals.m2m_changed.send(
|
||||
sender=self.through, action='pre_add',
|
||||
instance=self.instance, reverse=self.reverse,
|
||||
model=self.model, pk_set=missing_target_ids, using=db,
|
||||
)
|
||||
|
||||
# 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
|
||||
# Add the ones that aren't there already.
|
||||
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)
|
||||
], ignore_conflicts=can_ignore_conflicts)
|
||||
|
||||
if self.reverse or source_field_name == self.source_field_name:
|
||||
# Don't send the signal when we are inserting the
|
||||
# duplicate data row for symmetrical reverse entries.
|
||||
if must_send_signals:
|
||||
signals.m2m_changed.send(
|
||||
sender=self.through, action='post_add',
|
||||
instance=self.instance, reverse=self.reverse,
|
||||
|
|
|
@ -118,13 +118,26 @@ class ManyToManyTests(TestCase):
|
|||
)
|
||||
|
||||
@skipUnlessDBFeature('supports_ignore_conflicts')
|
||||
def test_add_ignore_conflicts(self):
|
||||
def test_fast_add_ignore_conflicts(self):
|
||||
"""
|
||||
A single query is necessary to add auto-created through instances if
|
||||
the database backend supports bulk_create(ignore_conflicts) and no
|
||||
m2m_changed signals receivers are connected.
|
||||
"""
|
||||
with self.assertNumQueries(1):
|
||||
self.a1.publications.add(self.p1, self.p2)
|
||||
|
||||
@skipUnlessDBFeature('supports_ignore_conflicts')
|
||||
def test_slow_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}
|
||||
# Disable fast-add to test the case where the slow add path is taken.
|
||||
add_plan = (True, False, False)
|
||||
with mock.patch.object(manager_cls, '_get_missing_target_ids', return_value=missing_target_ids) as mocked:
|
||||
self.a1.publications.add(self.p1)
|
||||
with mock.patch.object(manager_cls, '_get_add_plan', return_value=add_plan):
|
||||
self.a1.publications.add(self.p1)
|
||||
mocked.assert_called_once()
|
||||
|
||||
def test_related_sets(self):
|
||||
|
|
Loading…
Reference in New Issue