1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00

Avoided unnecessary DEFAULT usage on bulk_create().

When all values of a db_default field are DatabaseDefault, which is the case
most of the time, there is no point in specifying explicit DEFAULT for all
INSERT VALUES as that's what the database will do anyway if the field is not
specified.

In the case of Postgresql doing so can even be harmful as it prevents the usage
of the UNNEST strategy and in the case of Oracle, which doesn't support the
usage of the DEFAULT keyword, it unnecessarily requires providing literal
defaults.
This commit is contained in:
Simon Charette 2024-12-09 18:38:18 -05:00
parent e197a2b222
commit 9861770d81
No known key found for this signature in database
4 changed files with 47 additions and 21 deletions

View File

@ -671,21 +671,10 @@ class QuerySet(AltersData):
acreate.alters_data = True
def _prepare_for_bulk_create(self, objs):
from django.db.models.expressions import DatabaseDefault
connection = connections[self.db]
for obj in objs:
if not obj._is_pk_set():
# Populate new PK values.
obj.pk = obj._meta.pk.get_pk_value_on_save(obj)
if not connection.features.supports_default_keyword_in_bulk_insert:
for field in obj._meta.fields:
if field.generated:
continue
value = getattr(obj, field.attname)
if isinstance(value, DatabaseDefault):
setattr(obj, field.attname, field.db_default)
obj._prepare_related_fields_for_save(operation_name="bulk_create")
def _check_bulk_create_options(

View File

@ -1806,23 +1806,54 @@ class SQLInsertCompiler(SQLCompiler):
on_conflict=self.query.on_conflict,
)
result = ["%s %s" % (insert_statement, qn(opts.db_table))]
fields = self.query.fields or [opts.pk]
result.append("(%s)" % ", ".join(qn(f.column) for f in fields))
if self.query.fields:
value_rows = [
[
self.prepare_value(field, self.pre_save_val(field, obj))
for field in fields
if fields := self.query.fields:
from django.db.models.expressions import DatabaseDefault
supports_default_keyword_in_bulk_insert = (
self.connection.features.supports_default_keyword_in_bulk_insert
)
value_cols = []
for field in list(fields):
field_prepare = partial(self.prepare_value, field)
field_pre_save = partial(self.pre_save_val, field)
field_values = [
field_prepare(field_pre_save(obj)) for obj in self.query.objs
]
for obj in self.query.objs
]
if field.has_db_default():
# If all values are DEFAULT don't include the field and its
# values in the query as they are redundant and could prevent
# optimizations. This cannot be done if we're dealing with the
# last field as INSERT statements require at least one.
if len(fields) > 1 and all(
isinstance(value, DatabaseDefault) for value in field_values
):
fields.remove(field)
continue
elif not supports_default_keyword_in_bulk_insert:
# If the field cannot be excluded from the INSERT for the
# reasons listed above and the backend doesn't support the
# DEFAULT keyword each values must be expanded into their
# underlying expressions.
prepared_db_default = field_prepare(field.db_default)
field_values = [
(
prepared_db_default
if isinstance(value, DatabaseDefault)
else value
)
for value in field_values
]
value_cols.append(field_values)
value_rows = list(zip(*value_cols))
result.append("(%s)" % ", ".join(qn(f.column) for f in fields))
else:
# An empty object.
value_rows = [
[self.connection.ops.pk_default_value()] for _ in self.query.objs
]
fields = [None]
result.append("(%s)" % qn(opts.pk.column))
# Currently the backends just accept values when generating bulk
# queries and generate their own placeholders. Doing that isn't

View File

@ -5,7 +5,7 @@ from django.db import models
class Square(models.Model):
root = models.IntegerField()
square = models.PositiveIntegerField()
square = models.PositiveIntegerField(db_default=9)
def __str__(self):
return "%s ** 2 == %s" % (self.root, self.square)

View File

@ -27,3 +27,9 @@ class BulkCreateUnnestTests(TestCase):
[Square(root=2, square=4), Square(root=3, square=9)]
)
self.assertIn("UNNEST", ctx[0]["sql"])
def test_unnest_eligible_db_default(self):
with self.assertNumQueries(1) as ctx:
squares = Square.objects.bulk_create([Square(root=3), Square(root=3)])
self.assertIn("UNNEST", ctx[0]["sql"])
self.assertEqual([square.square for square in squares], [9, 9])