Django ORM: пакетная вставка данных и игнорирование дублей

Используя метод bulk_create() стандартного менеджера модели для сохранения в базе данных множества объектов одним запросом, вы обнаружите, что он не работает, если попытаться с его помощью добавить записи, которые уже существуют в базе данных. Какие есть альтернативы?

Недостаток bulk_create()

Джангистам, конечно, знаком метод стандартного менеджера модели bulk_create(), но он в случае случайного дублирования ключа при вставке данных отменяет транзакцию. Это означает, что если в рассматриваемом текстовом файле есть записи, которые уже сохранены в базе данных, новые данные могут быть не добавлены.

Варианты решения

Задача на самом деле нетривиальная. Часто рассматривают следующие варианты решения:

  1. Вставка каждой строки в рамках отдельной транзакции с перехватом ошибок и последующим игнорированием (очень медленно).
  2. Создание точки сохранения перед каждой вставкой (могут быть проблемы с масштабированием).
  3. Использование процедуры или запроса для вставки только в том случае, если строка еще не существует (сложно и медленно).
  4. Пакетная вставка или копирование (лучше) данных во временную таблицу, затем обьединение их в основную таблицу.

Реализация любого из этих вариантов в проекте Django создает новые проблемы.

Компромиссный вариант

Ребята из Ambition выпустили в свет проект django-manager-utils. Он позволяет выполнять различные функции, которые не поддерживаются стандартным менеджером модели Django. Среди таких функций — bulk_upsert(). Она выполняет пакетное обновление или вставку данных согласно переданному списку объектов модели. Интерфейс следующий:

def bulk_upsert(queryset, model_objs, unique_fields, update_fields=None, 
                return_upserts=False, return_upserts_distinct=False,
                sync=False, native=False):
    # see official docs

В наборе запросов queryset выполяется поиск совпадений согласно переданному списку объектов моделей (model_objs) и значений полей, определенных в unique_fields. Если совпадения найдены, соответствующие объекты приобретают значения из model_objs. Остальные объекты рассматриваются как новые и добавляются в массив. В списке update_fields дополнительно можно указать, какие конкретно поля следует обновлять.

Из недостатков — решение, увы, не в полную меру работает в случае бекенда MySQL.

Вывод

bulk_upsert из пакета django-manager-utils — хороший выбор, если вы используете PostgreSQL и объем импортируемых данных (скажем, 10 000 записей) вы считаете недостаточным для того, чтобы усложнять свой проект, прибегая к таким решениям, как, например, процедуры или временные таблицы.