Skip to content

Commit e167127

Browse files
charettessarahboyce
authored andcommitted
Fixed #36490 -- Avoided unnecessary transaction in bulk_create.
When dealing with an heterogeneous set of object with regards to primary key assignment that fits in a single batch there's no need to wrap the single INSERT statement in a transaction.
1 parent 5e06b97 commit e167127

2 files changed

Lines changed: 61 additions & 17 deletions

File tree

django/db/models/query.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import copy
66
import operator
77
import warnings
8+
from contextlib import nullcontext
89
from functools import reduce
910
from itertools import chain, islice
1011

@@ -802,7 +803,11 @@ def bulk_create(
802803
fields = [f for f in opts.concrete_fields if not f.generated]
803804
objs = list(objs)
804805
objs_with_pk, objs_without_pk = self._prepare_for_bulk_create(objs)
805-
with transaction.atomic(using=self.db, savepoint=False):
806+
if objs_with_pk and objs_without_pk:
807+
context = transaction.atomic(using=self.db, savepoint=False)
808+
else:
809+
context = nullcontext()
810+
with context:
806811
self._handle_order_with_respect_to(objs)
807812
if objs_with_pk:
808813
returned_columns = self._batched_insert(
@@ -1919,30 +1924,36 @@ def _batched_insert(
19191924
batch_size = min(batch_size, max_batch_size) if batch_size else max_batch_size
19201925
inserted_rows = []
19211926
bulk_return = connection.features.can_return_rows_from_bulk_insert
1922-
for item in [objs[i : i + batch_size] for i in range(0, len(objs), batch_size)]:
1923-
if bulk_return and (
1924-
on_conflict is None or on_conflict == OnConflict.UPDATE
1925-
):
1926-
inserted_rows.extend(
1927+
batches = [objs[i : i + batch_size] for i in range(0, len(objs), batch_size)]
1928+
if len(batches) > 1:
1929+
context = transaction.atomic(using=self.db, savepoint=False)
1930+
else:
1931+
context = nullcontext()
1932+
with context:
1933+
for item in batches:
1934+
if bulk_return and (
1935+
on_conflict is None or on_conflict == OnConflict.UPDATE
1936+
):
1937+
inserted_rows.extend(
1938+
self._insert(
1939+
item,
1940+
fields=fields,
1941+
using=self.db,
1942+
on_conflict=on_conflict,
1943+
update_fields=update_fields,
1944+
unique_fields=unique_fields,
1945+
returning_fields=self.model._meta.db_returning_fields,
1946+
)
1947+
)
1948+
else:
19271949
self._insert(
19281950
item,
19291951
fields=fields,
19301952
using=self.db,
19311953
on_conflict=on_conflict,
19321954
update_fields=update_fields,
19331955
unique_fields=unique_fields,
1934-
returning_fields=self.model._meta.db_returning_fields,
19351956
)
1936-
)
1937-
else:
1938-
self._insert(
1939-
item,
1940-
fields=fields,
1941-
using=self.db,
1942-
on_conflict=on_conflict,
1943-
update_fields=update_fields,
1944-
unique_fields=unique_fields,
1945-
)
19461957
return inserted_rows
19471958

19481959
def _chain(self):

tests/bulk_create/tests.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from django.db.models.functions import Lower, Now
1515
from django.test import (
1616
TestCase,
17+
TransactionTestCase,
1718
override_settings,
1819
skipIfDBFeature,
1920
skipUnlessDBFeature,
@@ -884,3 +885,35 @@ def test_db_default_field_excluded(self):
884885
def test_db_default_primary_key(self):
885886
(obj,) = DbDefaultPrimaryKey.objects.bulk_create([DbDefaultPrimaryKey()])
886887
self.assertIsInstance(obj.id, datetime)
888+
889+
890+
@skipUnlessDBFeature("supports_transactions", "has_bulk_insert")
891+
class BulkCreateTransactionTests(TransactionTestCase):
892+
available_apps = ["bulk_create"]
893+
894+
def test_no_unnecessary_transaction(self):
895+
with self.assertNumQueries(1):
896+
Country.objects.bulk_create(
897+
[Country(id=1, name="France", iso_two_letter="FR")]
898+
)
899+
with self.assertNumQueries(1):
900+
Country.objects.bulk_create([Country(name="Canada", iso_two_letter="CA")])
901+
902+
def test_objs_with_and_without_pk(self):
903+
with self.assertNumQueries(4):
904+
Country.objects.bulk_create(
905+
[
906+
Country(id=1, name="France", iso_two_letter="FR"),
907+
Country(name="Canada", iso_two_letter="CA"),
908+
]
909+
)
910+
911+
def test_multiple_batches(self):
912+
with self.assertNumQueries(4):
913+
Country.objects.bulk_create(
914+
[
915+
Country(name="France", iso_two_letter="FR"),
916+
Country(name="Canada", iso_two_letter="CA"),
917+
],
918+
batch_size=1,
919+
)

0 commit comments

Comments
 (0)