Я использую bulk_create для загрузки тысяч или строк в базу данных postgresql. К сожалению, некоторые из строк вызывают IntegrityError и останавливают процесс bulk_create. Мне было интересно, есть ли способ сказать django игнорировать такие строки и сохранить как можно большую часть партии?
Django bulk_create с игнорированием строк, которые вызывают IntegrityError?
Ответ 1
Теперь это возможно на Django 2.2
Django 2.2 добавляет новую опцию ignore_conflicts
в метод bulk_create
из документации:
В базах данных, которые его поддерживают (все, кроме PostgreSQL & lt; 9.5 и Oracle), установка для параметра ignore_conflicts значения True указывает базе данных игнорировать сбой при вставке любых строк, которые не соответствуют ограничениям, таким как повторяющиеся уникальные значения. Включение этого параметра отключает установку первичного ключа для каждого экземпляра модели (если база данных обычно его поддерживает).
Пример:
Entry.objects.bulk_create([
Entry(headline='This is a test'),
Entry(headline='This is only a test'),
], ignore_conflicts=True)
Ответ 2
(Примечание: я не использую Django, поэтому могут быть более подходящие ответы на конкретные рамки)
Для Django это невозможно, просто игнорируя ошибки INSERT
, потому что PostgreSQL прерывает всю транзакцию при первой ошибке.
Для Django потребуется один из следующих подходов:
-
INSERT
каждая строка в отдельной транзакции и игнорирует ошибки (очень медленно); - Создайте
SAVEPOINT
перед каждой вставкой (может иметь проблемы с масштабированием); - Использовать процедуру или запрос для вставки только в том случае, если строка еще не существует (сложная и медленная); или
- Загрузите данные в таблицу < <24 >
COPY
в таблицуTEMPORARY
, а затем объедините ее на серверную серверную таблицу.
Восстанавливающий подход (3) кажется хорошей идеей, но upsert и insert-if-not-exist удивительно сложны.
Лично я бы взял (4): я бы вставлял лишнюю таблицу в новую отдельную таблицу, возможно, UNLOGGED
или TEMPORARY
, тогда я бы запускал некоторый ручной SQL для:
LOCK TABLE realtable IN EXCLUSIVE MODE;
INSERT INTO realtable
SELECT * FROM temptable WHERE NOT EXISTS (
SELECT 1 FROM realtable WHERE temptable.id = realtable.id
);
LOCK TABLE ... IN EXCLUSIVE MODE
предотвращает параллельную вставку, которая создает строку, вызывающую конфликт со вставкой, сделанной вышеуказанным оператором, и сбой. Он не предотвращает одновременные SELECT
s, только SELECT ... FOR UPDATE
, INSERT
, UPDATE
и DELETE
, поэтому чтение из таблицы выполняется как обычно.
Если вы не можете позволить себе блокировать одновременную запись слишком долго, вместо этого вы можете использовать записываемый CTE для копирования диапазонов строк из temptable
в realtable
, повторите попытку каждого блока, если это не удалось.
Ответ 3
Один быстрый и грязный обходной путь для этого, который не включает ручные SQL и временные таблицы, состоит в том, чтобы просто попытаться вставить данные в объем. Если это не удается, вернитесь к последовательному вводу.
objs = [(Event), (Event), (Event)...]
try:
Event.objects.bulk_create(objs)
except IntegrityError:
for obj in objs:
try:
obj.save()
except IntegrityError:
continue
Если у вас много и много ошибок, это может быть не так эффективно (вы будете тратить больше времени на вставку, чем на то, чтобы делать это навалом), но я работаю с набором данных с высокой производительностью с несколькими дубликатами, поэтому это решает большинство моих проблем.
Ответ 4
Или 5. Разделите и победите
Я не тестировал и не тестировал это полностью, но для меня это очень хорошо. YMMV, в зависимости от того, сколько ошибок вы ожидаете получить в объемной операции.
def psql_copy(records):
count = len(records)
if count < 1:
return True
try:
pg.copy_bin_values(records)
return True
except IntegrityError:
if count == 1:
# found culprit!
msg = "Integrity error copying record:\n%r"
logger.error(msg % records[0], exc_info=True)
return False
finally:
connection.commit()
# There was an integrity error but we had more than one record.
# Divide and conquer.
mid = count / 2
return psql_copy(records[:mid]) and psql_copy(records[mid:])
# or just return False
Ответ 5
Даже в Django 1.11 нет способа сделать это. Я нашел лучший вариант, чем использование Raw SQL. Это используя djnago-query-builder. У него есть метод upsert method
from querybuilder.query import Query
q = Query().from_table(YourModel)
# replace with your real objects
rows = [YourModel() for i in range(10)]
q.upsert(rows, ['unique_fld1', 'unique_fld2'], ['fld1_to_update', 'fld2_to_update'])
Примечание. Библиотека поддерживает только postgreSQL
Вот сущность, которую я использую для массовой вставки, которая поддерживает игнорирование IntegrityErrors и возвращает вставленные записи.