Sto usando bulk_create per caricare migliaia o righe in un DB postgresql. Sfortunatamente alcune delle righe stanno causando IntegrityError e interrompendo il processo bulk_create. Mi stavo chiedendo se ci fosse un modo per dire a Django di ignorare tali file e salvare il più possibile il lotto?Django bulk_create con righe ignoranti che causano IntegrityError?
risposta
(Nota: io non uso Django, quindi non ci può essere più adatto risposte specifiche del quadro)
Non è possibile per Django per fare questo semplicemente ignorando INSERT
fallimenti perché PostgreSQL interrompe l'intera transazione su il primo errore.
Django avrebbe bisogno di uno di questi approcci:
INSERT
ciascuna riga in una transazione separata e ignorare gli errori (molto lento);- Creare un
SAVEPOINT
prima di ogni inserimento (può avere problemi di ridimensionamento); - Utilizzare una procedura o una query da inserire solo se la riga non esiste già (complicata e lenta); oppure
- Inserimento di massa o (migliore)
COPY
i dati in una tabellaTEMPORARY
, quindi unirli nella tabella principale lato server.
L'approccio di tipo upsert (3) sembra una buona idea, ma upsert and insert-if-not-exists are surprisingly complicated.
Personalmente, mi piacerebbe prendere (4): avevo bulk-inserimento in una nuova tabella separata, probabilmente UNLOGGED
o TEMPORARY
, allora mi piacerebbe correre un po 'di SQL manuale:
LOCK TABLE realtable IN EXCLUSIVE MODE;
INSERT INTO realtable
SELECT * FROM temptable WHERE NOT EXISTS (
SELECT 1 FROM realtable WHERE temptable.id = realtable.id
);
Il LOCK TABLE ... IN EXCLUSIVE MODE
impedisce che un inserto concorrente che crea una riga provochi un conflitto con un inserto fatto dall'istruzione precedente e in mancanza. Lo standard non previene contemporaneamente SELECT
s, solo SELECT ... FOR UPDATE
, INSERT
, UPDATE
e DELETE
, pertanto le letture dal tavolo continuano normalmente.
Se non puoi permetterti di bloccare le scritture concorrenti troppo a lungo, puoi invece utilizzare un CTE scrivibile per copiare intervalli di righe da temptable
a realtable
, riprovando ogni blocco se non riesce.
Grazie @ craig-ringer ho finito per cancellare la mia lista di oggetti python prima di inserirli nel DB, qualcosa di simile al tuo # 3, ma in puro python. – Meitham
C'è un esempio dettagliato di (4) su https://rodmtech.net/docs/django/django-bulk_create-without-integrityerror-rollback/ – eugene
O 5. divide et impera
non ho la prova o benchmark questo fondo, ma si comporta abbastanza bene per me. YMMV, in base in particolare al numero di errori che ci si aspetta di ottenere in un'operazione di massa.
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
Una soluzione veloce-e-sporco per questo che non coinvolge SQL manuale e tabelle temporanee è solo tentativo di bulk inserire i dati. Se fallisce, ripristina l'inserimento seriale.
objs = [(Event), (Event), (Event)...]
try:
Event.objects.bulk_create(objs)
except IntegrityError:
for obj in objs:
try:
obj.save()
except IntegrityError:
continue
se si dispone di un sacco di errori di questo potrebbe non essere così efficiente (si spenderà più tempo in serie l'inserimento di farlo in massa), ma sto lavorando attraverso un insieme di dati ad alta cardinalità con poche duplica quindi risolve la maggior parte dei miei problemi.
Anche in Django 1.11 non c'è modo di farlo.Ho trovato un'opzione migliore rispetto all'utilizzo di SQL raw. Si utilizza djnago-query-builder. Ha un metodo di upsert
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'])
Nota: la libreria di supporto solo postgreSQL
che potrebbe non essere possibile perché PostgreSQL interrompe la transazione sul primo errore. Django avrebbe bisogno di (a) creare un SAVEPOINT prima di ogni inserimento, che rallenta le cose e costa risorse; oppure (b) utilizzare una procedura o una query da inserire solo se la riga non esiste già. Personalmente, inserirei bulk in una nuova tabella separata, probabilmente 'UNLOGGED' o' TEMPORARY', quindi 'INSERT INTO realtable SELECT * FROM temptable WHERE NOT EXISTS (SELEZIONA 1 DA REALtable WHERE temptable.id = realtable.id)' o simili. –