web-dev-qa-db-ja.com

Django IntegrityErrorを引き起こす無視行を持つbulk_create?

Bulk_createを使用して数千または行をpostgresql DBにロードしています。残念ながら、一部の行はIntegrityErrorを引き起こし、bulk_createプロセスを停止しています。 Djangoにそのような行を無視してバッチのできるだけ多くを保存するように指示する方法があったかどうか疑問に思っていましたか?

41
Meitham

これはDjango 2.2で可能になりました

Django 2.2は documentation からignore_conflictsメソッドに新しいbulk_createオプションを追加します:

これをサポートするデータベース(PostgreSQL <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)
43
Cesar Canassa

(注:私はDjangoを使用しないので、より適切なフレームワーク固有の回答があるかもしれません)

Djangoでは、PostgreSQLが最初のエラーでトランザクション全体を中止するため、INSERTエラーを無視するだけではこれを行うことはできません。

Djangoはこれらのアプローチの1つを必要とします:

  1. INSERT各行を個別のトランザクションで処理し、エラーを無視します(非常に遅い);
  2. 各挿入の前に SAVEPOINT を作成します(スケーリングの問題が発生する可能性があります);
  3. プロシージャまたはクエリを使用して、行がまだ存在しない場合(複雑で低速)にのみ挿入します。または
  4. 一括挿入または(より良い) COPY データをTEMPORARYテーブルに入れ、それをメインテーブルのサーバー側にマージします。

アップサートのようなアプローチ(3)は良い考えのように見えますが、 アップサートと挿入-存在しない場合は驚くほど複雑です です。

個人的には、私は(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 行を作成する同時挿入が、上記のステートメントによって実行された挿入と競合して失敗することを防ぎます。同時SELECTsを防止します防止しませんだけですSELECT ... FOR UPDATEINSERTUPDATEおよびDELETEなので、テーブルからの読み取りは通常どおり続行されます。

同時書き込みを長時間ブロックする余裕がない場合は、代わりに書き込み可能なCTEを使用して、行の範囲をtemptableからrealtableにコピーし、失敗した場合は各ブロックを再試行します。

8
Craig Ringer

手動のSQLと一時テーブルを含まない、このための迅速で汚い回避策の1つは、データの一括挿入を試みることです。失敗した場合は、シリアル挿入に戻ります。

objs = [(Event), (Event), (Event)...]

try:
    Event.objects.bulk_create(objs)

except IntegrityError:
    for obj in objs:
        try:
            obj.save()
        except IntegrityError:
            continue

エラーがたくさんある場合、これはそれほど効率的ではない可能性があります(一括して挿入するよりも逐次挿入に時間がかかります)が、重複の少ない高カーディナリティのデータセットを使用しているため、これによりほとんどの問題が解決されます問題。

4
Ivan

または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
1

前の回答Django 2.2プロジェクト:

私は最近この状況に遭遇し、一意性をチェックするための2番目のリスト配列で自分の道を見つけました。

私の場合、モデルには一意の一括チェックがあり、一括作成の配列に重複データがあるため、一括作成は整合性エラー例外をスローします。

そこで、オブジェクトの一括作成リストの他にチェックリストを作成することにしました。これがサンプルコードです。一意のキーはownerおよびbrandであり、この例では所有者はユーザーオブジェクトインスタンスとブランドは文字列インスタンスです。

create_list = []
create_list_check = []
for brand in brands:
    if (owner.id, brand) not in create_list_check:
        create_list_check.append((owner.id, brand))
        create_list.append(ProductBrand(owner=owner, name=brand))

if create_list:
    ProductBrand.objects.bulk_create(create_list)
0
Sencer H.

Django 1.11でも、これを行う方法はありません。RawSQLを使用するよりも良いオプションを見つけました。それは djnago-query-builder を使用しています。これには- psert メソッド

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のみをサポートします

以下は Gist です。これは、IntegrityErrorsの無視をサポートし、挿入されたレコードを返す一括挿入に使用します。

0
Noortheen Raja