web-dev-qa-db-fra.com

Django bulk_create avec ignorer les lignes qui provoquent IntegrityError?

J'utilise bulk_create pour charger des milliers ou des lignes dans une base de données postgresql. Malheureusement, certaines lignes provoquent IntegrityError et arrêtent le processus bulk_create. Je me demandais s'il y avait un moyen de dire à Django d'ignorer ces lignes et d'économiser autant de lot que possible?

41
Meitham

Ceci est désormais possible sur Django 2.2

Django 2.2 ajoute un nouveau ignore_conflicts à l'option bulk_create, à partir de la documentation :

Sur les bases de données qui le prennent en charge (toutes sauf PostgreSQL <9.5 et Oracle), la définition du paramètre ignore_conflicts sur True indique à la base de données d'ignorer l'échec de l'insertion des lignes qui échouent aux contraintes telles que les valeurs uniques en double. L'activation de ce paramètre désactive la définition de la clé primaire sur chaque instance de modèle (si la base de données la prend normalement en charge).

Exemple:

Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
], ignore_conflicts=True)
43
Cesar Canassa

(Remarque: je n'utilise pas Django, il peut donc y avoir des réponses spécifiques au framework plus appropriées)

Il n'est pas possible pour Django de le faire en ignorant simplement les échecs INSERT car PostgreSQL abandonne la transaction entière sur la première erreur.

Django aurait besoin d'une de ces approches:

  1. INSERT chaque ligne d'une transaction distincte et ignore les erreurs (très lent);
  2. Créez un SAVEPOINT avant chaque insertion (peut avoir des problèmes de mise à l'échelle);
  3. Utilisez une procédure ou une requête pour insérer uniquement si la ligne n'existe pas déjà (compliquée et lente); ou
  4. Insertion en masse ou (mieux) COPY les données dans une table TEMPORARY, puis fusionnez-les dans la table principale côté serveur.

L'approche upsert-like (3) semble être une bonne idée, mais psert et insert-if-not-exist sont étonnamment compliqués .

Personnellement, je prendrais (4): je ferais de l'insertion en bloc dans une nouvelle table séparée, probablement UNLOGGED ou TEMPORARY, puis j'exécuterais du SQL manuel pour:

LOCK TABLE realtable IN EXCLUSIVE MODE;

INSERT INTO realtable 
SELECT * FROM temptable WHERE NOT EXISTS (
    SELECT 1 FROM realtable WHERE temptable.id = realtable.id
);

Le LOCK TABLE ... IN EXCLUSIVE MODE empêche une insertion simultanée qui crée une ligne de provoquer un conflit avec une insertion effectuée par l'instruction ci-dessus et d'échouer. Il n'empêche pas d'empêcher SELECTs simultanés, seulement SELECT ... FOR UPDATE, INSERT, UPDATE et DELETE, les lectures de la table se poursuivent donc normalement.

Si vous ne pouvez pas vous permettre de bloquer les écritures simultanées trop longtemps, vous pouvez utiliser un CTE accessible en écriture pour copier des plages de lignes de temptable vers realtable, en réessayant chaque bloc s'il échoue.

8
Craig Ringer

Une solution de contournement rapide et sale pour cela qui n'implique pas SQL manuel et des tables temporaires consiste à simplement essayer d'insérer en masse les données. S'il échoue, revenez à l'insertion série.

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

try:
    Event.objects.bulk_create(objs)

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

Si vous avez beaucoup, beaucoup d'erreurs, cela peut ne pas être aussi efficace (vous passerez plus de temps à insérer en série qu'en le faisant en vrac), mais je travaille sur un ensemble de données à haute cardinalité avec quelques doublons, donc cela résout la plupart de mes problèmes.

4
Ivan

Ou 5. Diviser pour mieux régner

Je n'ai pas testé ou comparé cela à fond, mais cela fonctionne assez bien pour moi. YMMV, en fonction notamment du nombre d'erreurs que vous prévoyez d'obtenir dans une opération en bloc.

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

Réponse tardive pour les projets pré Django 2.2:

Je suis tombé sur cette situation récemment et j'ai trouvé ma sortie avec un tableau de liste d'appuyeurs pour vérifier l'unicité.

Dans mon cas, le modèle a cette vérification unique et la création en bloc génère une exception d'erreur d'intégrité car le tableau de création en bloc contient des données en double.

J'ai donc décidé de créer une liste de contrôle en plus de créer une liste d'objets en vrac. Voici l exemple de code; Les clés uniques sont propriétaire et marque , et dans cet exemple le propriétaire est une instance et une marque d'objet utilisateur est une instance de chaîne:

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.

Même dans Django 1.11 il n'y a aucun moyen de le faire. J'ai trouvé une meilleure option que d'utiliser Raw SQL. Il utilise djnago-query-builder . Il a un - psert méthode

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'])

Remarque: la bibliothèque ne prend en charge que postgreSQL

Voici un Gist que j'utilise pour l'insertion en bloc qui prend en charge ignorer IntegrityErrors et renvoie les enregistrements insérés.

0
Noortheen Raja