Django `bulk_update()`: обновление разных полей для каждой записи в одном запросе
У меня есть два экземпляра модели, rec1 и rec2, но у каждого из них обновлены разные поля:
rec1 = MyModel(id=1, name="John") # Only 'name' is changed
rec2 = MyModel(id=2, age=30) # Only 'age' is changed
Теперь я хочу эффективно обновить обе записи с помощью bulk_update()
. Однако bulk_update()
требует фиксированного списка полей, то есть:
updated_fields = ['name', 'age']
model.objects.bulk_update([rec1, rec2], fields=updated_fields)
Это обновляет оба поля в обеих записях, даже если:
- rec1 нужно обновить только имя
- rec2 нужно обновить только возраст
Цель: Обновляйте только измененные поля для каждой записи в одном запросе. Избегайте ненужных обновлений неизменных полей.
Issue: bulk_update() применяет один и тот же список полей ко всем записям, что приводит к избыточным обновлениям.
Вопрос: Есть ли способ эффективно обновлять только измененные поля в одной записи в одном запросе? Или есть альтернативный подход к этому в Django ORM?
model.objects.bulk_update(instances_to_update, fields=updated_fields)
, где updated_fields
- список.
я ожидал, что updated_fields будет функцией итерационного типа.
Невозможно сделать это с помощью одного оператора SQL, и поэтому ФОРМА также требует отдельных вызовов. Обновление одних и тех же полей использует функциональность execute_many
, которая реализована всеми соединителями баз данных Python, и этот вызов имеет одно и то же "тело" инструкции SQL, которое выполняется с изменяющимися параметрами.
Инструкция SQL UPDATE
для изменения различных полей должна измениться, поэтому массовое обновление невозможно.
Просто используйте обычный цикл Python for
и вызывайте по одному обновлению для каждой из ваших записей. Если вы сделаете это внутри одной и той же транзакции, разница в направлении вызова bulk_update
должна быть незначительной.
Вы можете сделать это, на самом деле это сделает Django ORM, для .bulk_update(..)
по сути, создайте запрос, который выглядит следующим образом:
from django.db.models import Case, Value, When
model.objects.filter(pk__in=[rec1.pk, rec2.pk]).update(
name=Case(
When(pk=rec1.pk, then=Value(rec1.name)),
When(pk=rec2.pk, then=Value(rec2.name)),
default=F('name'),
),
age=Case(
When(pk=rec1.pk, then=Value(rec1.age)),
When(pk=rec2.pk, then=Value(rec2.age)),
default=F('age'),
),
)
Это не очень эффективно: большинство баз данных прибегают к "линейному" поиску по CASE
-WHEN
s, и, следовательно, обновление записей может происходить очень медленно.
таким образом, мы можем ограничить количество обращений только теми, которые хотим обновить:
model.objects.filter(pk__in=[rec1.pk, rec2.pk]).update(
name=Case(When(pk=rec1.pk, then=Value(rec1.name)), default=F('name')),
age=Case(When(pk=rec2.pk, then=Value(rec2.age)), default=F('age')),
)
TL DR;
Мы хотим обновить разные поля для разных записей за одну операцию bulk_update()
. Это действительно ограничение текущей реализации Django.
Основная проблема заключается в том, что метод Django bulk_update()
требует, чтобы вы указали фиксированный список полей, которые будут применены ко всем объектам в вашем пакете обновления. Нет встроенного способа сказать "обновите поле "имя" только для записи 1, но обновите поле "возраст" только для записи 2" за одну операцию.
Когда мы используем что-то вроде
rec1 = MyModel(id=1, name="John") # Only 'name' is changed
rec2 = MyModel(id=2, age=30) # Only 'age' is changed
model.objects.bulk_update([rec1, rec2], fields=['name', 'age'])
Django сгенерирует SQL, который обновит оба поля для обеих записей, а это не то, что вам нужно.
Это выглядит примерно как
UPDATE my_table SET
name = CASE WHEN id=1 THEN 'John' WHEN id=2 THEN name END,
age = CASE WHEN id=1 THEN age WHEN id=2 THEN 30 END
WHERE id IN (1,2)
При этом обновляются оба поля для обеих записей, хотя вы хотели обновить только одно поле для каждой записи. Это неотъемлемое ограничение.
На данный момент существует несколько подходов к решению этого сценария:
Сгруппируйте объекты по полям, которые нуждаются в обновлении, и выполните отдельные вызовы функции bulk_update():
name_updates = [obj for obj in all_objects if obj.name_changed]
age_updates = [obj for obj in all_objects if obj.age_changed]
MyModel.objects.bulk_update(name_updates, ['name'])
MyModel.objects.bulk_update(age_updates, ['age'])
К сожалению, в Django просто нет встроенной поддержки для этого варианта использования. Согласно многочисленным обсуждениям, в том числе на Reddit, "Django и большинство редакторов не утруждают себя этим. Они либо используют все поля в запросе, либо только те, которые вы указываете"
Было одно обсуждение Сократить последовательность "Case-When" для bulk_update, когда значения для определенного поля совпадают. год назад примерно то же самое. Вы можете свободно использовать этот подход с пользовательским менеджером/набором запросов.
Я думаю, что наиболее эффективным способом обновления объектов является использование bulk_update дважды. Поскольку наборы в вашем примере не пересекаются, каждый объект будет обновлен только один раз.
Для запуска одного bulk_update потребуется условие if для обработки разных обращений, что приводит к дополнительным затратам на обработку
.