Django bulk_create с игнорированием строк, которые вызывают IntegrityError? - программирование
Подтвердить что ты не робот

Django bulk_create с игнорированием строк, которые вызывают IntegrityError?

Я использую bulk_create для загрузки тысяч или строк в базу данных postgresql. К сожалению, некоторые из строк вызывают IntegrityError и останавливают процесс bulk_create. Мне было интересно, есть ли способ сказать django игнорировать такие строки и сохранить как можно большую часть партии?

4b9b3361

Ответ 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 и возвращает вставленные записи.