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?
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)
(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:
INSERT
chaque ligne d'une transaction distincte et ignore les erreurs (très lent);SAVEPOINT
avant chaque insertion (peut avoir des problèmes de mise à l'échelle);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 SELECT
s 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.
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.
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
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)
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.