Skip to content

Commit e9689f8

Browse files
authored
Fixed O(n) queries when adding M2M objects (#1333)
1 parent 87327ce commit e9689f8

File tree

3 files changed

+47
-4
lines changed

3 files changed

+47
-4
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ Unreleased
2727
"Customizing the History Admin Templates" for overriding its template context (gh-1128)
2828
- Fixed the setting ``SIMPLE_HISTORY_ENABLED = False`` not preventing M2M historical
2929
records from being created (gh-1328)
30+
- For history-tracked M2M fields, adding M2M objects (using ``add()`` or ``set()``)
31+
used to cause a number of database queries that scaled linearly with the number of
32+
objects; this has been fixed to now be a constant number of queries (gh-1333)
3033

3134
3.5.0 (2024-02-19)
3235
------------------

simple_history/models.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -687,18 +687,21 @@ def create_historical_record_m2ms(self, history_instance, instance):
687687
m2m_history_model = self.m2m_models[field]
688688
original_instance = history_instance.instance
689689
through_model = getattr(original_instance, field.name).through
690+
through_model_field_names = [f.name for f in through_model._meta.fields]
691+
through_model_fk_field_names = [
692+
f.name for f in through_model._meta.fields if isinstance(f, ForeignKey)
693+
]
690694

691695
insert_rows = []
692696

693697
through_field_name = utils.get_m2m_field_name(field)
694698
rows = through_model.objects.filter(**{through_field_name: instance})
699+
rows = rows.select_related(*through_model_fk_field_names)
695700
for row in rows:
696701
insert_row = {"history": history_instance}
697702

698-
for through_model_field in through_model._meta.fields:
699-
insert_row[through_model_field.name] = getattr(
700-
row, through_model_field.name
701-
)
703+
for field_name in through_model_field_names:
704+
insert_row[field_name] = getattr(row, field_name)
702705
insert_rows.append(m2m_history_model(**insert_row))
703706

704707
pre_create_historical_m2m_records.send(

simple_history/tests/tests/test_models.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2376,6 +2376,43 @@ def test_bulk_add_remove(self):
23762376
historical_place = m2m_record.places.first()
23772377
self.assertEqual(historical_place.place, self.place)
23782378

2379+
def test_add_remove_set_and_clear_methods_make_expected_num_queries(self):
2380+
for num_places in (1, 2, 4):
2381+
with self.subTest(num_places=num_places):
2382+
start_pk = 100 + num_places
2383+
places = Place.objects.bulk_create(
2384+
Place(pk=pk, name=f"Place {pk}")
2385+
for pk in range(start_pk, start_pk + num_places)
2386+
)
2387+
self.assertEqual(len(places), num_places)
2388+
self.assertEqual(self.poll.places.count(), 0)
2389+
2390+
# The number of queries should stay the same, regardless of
2391+
# the number of places added or removed
2392+
with self.assertNumQueries(5):
2393+
self.poll.places.add(*places)
2394+
self.assertEqual(self.poll.places.count(), num_places)
2395+
2396+
with self.assertNumQueries(3):
2397+
self.poll.places.remove(*places)
2398+
self.assertEqual(self.poll.places.count(), 0)
2399+
2400+
with self.assertNumQueries(6):
2401+
self.poll.places.set(places)
2402+
self.assertEqual(self.poll.places.count(), num_places)
2403+
2404+
with self.assertNumQueries(4):
2405+
self.poll.places.set([])
2406+
self.assertEqual(self.poll.places.count(), 0)
2407+
2408+
with self.assertNumQueries(5):
2409+
self.poll.places.add(*places)
2410+
self.assertEqual(self.poll.places.count(), num_places)
2411+
2412+
with self.assertNumQueries(3):
2413+
self.poll.places.clear()
2414+
self.assertEqual(self.poll.places.count(), 0)
2415+
23792416
def test_m2m_relation(self):
23802417
# Ensure only the correct M2Ms are saved and returned for history objects
23812418
poll_2 = PollWithManyToMany.objects.create(question="Why", pub_date=today)

0 commit comments

Comments
 (0)