Skip to content

Commit 6325659

Browse files
authored
Removed n+1 query from bulk_create_with_history utility (#975)
1 parent 37150f2 commit 6325659

File tree

4 files changed

+58
-5
lines changed

4 files changed

+58
-5
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ Authors
113113
- Steven Buss (`sbuss <https://github.com/sbuss>`_)
114114
- Steven Klass
115115
- Tim Schilling (`tim-schilling <https://github.com/tim-schilling>`_)
116+
- Todd Wolfson (`twolfson <https://github.com/twolfson>`_)
116117
- Tommy Beadle (`tbeadle <https://github.com/tbeadle>`_)
117118
- Trey Hunner (`treyhunner <https://github.com/treyhunner>`_)
118119
- Ulysses Vilela

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Changes
44
Unreleased
55
----------
66

7+
- Removed n+1 query from ``bulk_create_with_history`` utility (gh-975)
78
- Started using ``exists`` query instead of ``count`` in ``populate_history`` command (gh-982)
89

910
3.1.1 (2022-04-23)

simple_history/tests/tests/test_utils.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,38 @@ def test_bulk_create_history_with_duplicates_ignore_conflicts(self):
186186
self.assertEqual(PollWithUniqueQuestion.objects.count(), 2)
187187
self.assertEqual(PollWithUniqueQuestion.history.count(), 2)
188188

189+
def test_bulk_create_history_with_no_ids_return(self):
190+
pub_date = timezone.now()
191+
objects = [
192+
Poll(question="Question 1", pub_date=pub_date),
193+
Poll(question="Question 2", pub_date=pub_date),
194+
Poll(question="Question 3", pub_date=pub_date),
195+
Poll(question="Question 4", pub_date=pub_date),
196+
Poll(question="Question 5", pub_date=pub_date),
197+
]
198+
199+
_bulk_create = Poll._default_manager.bulk_create
200+
201+
def mock_bulk_create(*args, **kwargs):
202+
_bulk_create(*args, **kwargs)
203+
return [
204+
Poll(question="Question 1", pub_date=pub_date),
205+
Poll(question="Question 2", pub_date=pub_date),
206+
Poll(question="Question 3", pub_date=pub_date),
207+
Poll(question="Question 4", pub_date=pub_date),
208+
Poll(question="Question 5", pub_date=pub_date),
209+
]
210+
211+
with patch.object(
212+
Poll._default_manager, "bulk_create", side_effect=mock_bulk_create
213+
):
214+
with self.assertNumQueries(3):
215+
result = bulk_create_with_history(objects, Poll)
216+
self.assertEqual(
217+
[poll.question for poll in result], [poll.question for poll in objects]
218+
)
219+
self.assertNotEqual(result[0].id, None)
220+
189221

190222
class BulkCreateWithHistoryTransactionTestCase(TransactionTestCase):
191223
def setUp(self):
@@ -242,7 +274,7 @@ def test_bulk_create_no_ids_return(self, hist_manager_mock):
242274
model = Mock(
243275
_default_manager=Mock(
244276
bulk_create=Mock(return_value=[Place(name="Place 1")]),
245-
filter=Mock(return_value=objects),
277+
filter=Mock(return_value=Mock(order_by=Mock(return_value=objects))),
246278
),
247279
_meta=Mock(get_fields=Mock(return_value=[])),
248280
)

simple_history/utils.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import django
44
from django.db import transaction
5-
from django.db.models import ForeignKey, ManyToManyField
5+
from django.db.models import Case, ForeignKey, ManyToManyField, Q, When
66
from django.forms.models import model_to_dict
77

88
from simple_history.exceptions import AlternativeManagerError, NotHistoricalModelError
@@ -111,16 +111,35 @@ def bulk_create_with_history(
111111
default_date=default_date,
112112
)
113113
if second_transaction_required:
114-
obj_list = []
115114
with transaction.atomic(savepoint=False):
116-
for obj in objs_with_id:
115+
# Generate a common query to avoid n+1 selections
116+
# https://github.com/jazzband/django-simple-history/issues/974
117+
cumulative_filter = None
118+
obj_when_list = []
119+
for i, obj in enumerate(objs_with_id):
117120
attributes = dict(
118121
filter(
119122
lambda x: x[1] is not None,
120123
model_to_dict(obj, exclude=exclude_fields).items(),
121124
)
122125
)
123-
obj_list += model_manager.filter(**attributes)
126+
q = Q(**attributes)
127+
cumulative_filter = (cumulative_filter | q) if cumulative_filter else q
128+
# https://stackoverflow.com/a/49625179/1960509
129+
# DEV: If an attribute has `then` as a key
130+
# then they'll also run into issues with `bulk_update`
131+
# due to shared implementation
132+
# https://github.com/django/django/blob/4.0.4/django/db/models/query.py#L624-L638
133+
obj_when_list.append(When(**attributes, then=i))
134+
obj_list = (
135+
list(
136+
model_manager.filter(cumulative_filter).order_by(
137+
Case(*obj_when_list)
138+
)
139+
)
140+
if objs_with_id
141+
else []
142+
)
124143
history_manager.bulk_history_create(
125144
obj_list,
126145
batch_size=batch_size,

0 commit comments

Comments
 (0)