[python] Inserimento di massa con SQLAlchemy ORM



4 Answers

Per quanto ne so, non c'è modo di ottenere l'ORM per emettere inserti di massa. Credo che la ragione di fondo sia che SQLAlchemy ha bisogno di tenere traccia dell'identità di ogni oggetto (ad esempio, nuove chiavi primarie) e gli inserimenti di massa interferiscono con quello. Ad esempio, supponendo che la tua tabella foo contenga una colonna id e sia mappata su una classe Foo :

x = Foo(bar=1)
print x.id
# None
session.add(x)
session.flush()
# BEGIN
# INSERT INTO foo (bar) VALUES(1)
# COMMIT
print x.id
# 1

Poiché SQLAlchemy ha rilevato il valore per x.id senza emettere un'altra query, possiamo dedurre che ha ottenuto il valore direttamente x.id INSERT . Se non hai bisogno dell'accesso successivo agli oggetti creati tramite le stesse istanze, puoi saltare il livello ORM per il tuo inserto:

Foo.__table__.insert().execute([{'bar': 1}, {'bar': 2}, {'bar': 3}])
# INSERT INTO foo (bar) VALUES ((1,), (2,), (3,))

SQLAlchemy non può abbinare queste nuove righe con nessun oggetto esistente, quindi dovrai interrogarli di nuovo su ogni operazione successiva.

Per quanto riguarda i dati obsoleti, è utile ricordare che la sessione non ha un modo integrato per sapere quando il database viene modificato al di fuori della sessione. Per accedere a dati modificati esternamente tramite istanze esistenti, le istanze devono essere contrassegnate come scadute . Ciò accade di default su session.commit() , ma può essere fatto manualmente chiamando session.expire_all() o session.expire(instance) . Un esempio (omesso SQL):

x = Foo(bar=1)
session.add(x)
session.commit()
print x.bar
# 1
foo.update().execute(bar=42)
print x.bar
# 1
session.expire(x)
print x.bar
# 42

session.commit() scade x , quindi la prima istruzione print apre implicitamente una nuova transazione e richiede nuovamente gli attributi di x . Se si commenta la prima dichiarazione di stampa, si noterà che il secondo raccoglie il valore corretto, poiché la nuova query non viene emessa fino a dopo l'aggiornamento.

Questo ha senso dal punto di vista dell'isolamento transazionale: dovresti solo rilevare le modifiche esterne tra le transazioni. Se ciò ti causa problemi, ti suggerirei di chiarire o ripensare i limiti delle transazioni della tua applicazione invece di raggiungere immediatamente session.expire_all() .

Question

Esiste un modo per far sì che SQLAlchemy esegua un inserimento bulk piuttosto che inserire ogni singolo oggetto. vale a dire,

fare:

INSERT INTO `foo` (`bar`) VALUES (1), (2), (3)

piuttosto che:

INSERT INTO `foo` (`bar`) VALUES (1)
INSERT INTO `foo` (`bar`) VALUES (2)
INSERT INTO `foo` (`bar`) VALUES (3)

Ho appena convertito del codice per usare sqlalchemy piuttosto che raw sql e anche se ora è molto più bello lavorare con esso sembra essere più lento ora (fino a un fattore di 10), mi chiedo se questo è il motivo.

Potrei essere in grado di migliorare la situazione utilizzando le sessioni in modo più efficiente. Al momento ho autoCommit=False e faccio un session.commit() dopo che ho aggiunto qualcosa. Anche se sembra che i dati diventino obsoleti se il DB viene modificato altrove, come se facessi una nuova query, ottenevo ancora vecchi risultati?

Grazie per l'aiuto!




Tutte le strade portano a Roma , ma alcuni di loro attraversano montagne, richiedono traghetti ma se si vuole arrivare in fretta basta prendere l'autostrada.

In questo caso l'autostrada utilizza la funzione execute_batch() di psycopg2 . La documentazione dice che è il migliore:

L'attuale implementazione di executemany() è (usando un understatement estremamente caritatevole) non particolarmente performante. Queste funzioni possono essere utilizzate per accelerare l'esecuzione ripetuta di una dichiarazione rispetto a un insieme di parametri. Riducendo il numero di roundtrip del server, le prestazioni possono essere di ordine di grandezza migliori rispetto all'utilizzo di executemany() .

Nel mio test execute_batch() è approssimativamente due volte più veloce di executemany() e offre l'opzione per configurare il page_size per ulteriori tweaking (se si vuole spremere l'ultimo 2-3% delle prestazioni dal driver).

La stessa funzione può essere facilmente abilitata se si utilizza SQLAlchemy impostando use_batch_mode=True come parametro quando si crea un'istanza del motore con create_engine()




Di solito lo faccio usando add_all .

from app import session
from models import User

objects = [User(name="u1"), User(name="u2"), User(name="u3")]
session.add_all(objects)
session.commit()



SQLAlchemy ha introdotto quello nella versione 1.0.0 :

Operazioni di massa: documenti SQLAlchemy

Con queste operazioni, ora puoi fare inserimenti o aggiornamenti collettivi!

Ad esempio (se si desidera l'overhead più basso per gli INSERT della tabella semplice), è possibile utilizzare Session.bulk_insert_mappings() :

loadme = [
        (1, 'a')
    ,   (2, 'b')
    ,   (3, 'c')
    ]

dicts = []
for i in range(len(loadme)):
    dicts.append(dict(bar=loadme[i][0], fly=loadme[i][1]))

s = Session()
s.bulk_insert_mappings(Foo, dicts)
s.commit()

Oppure, se vuoi, salta le tuple del loadme e scrivi i dizionari direttamente in dicts (ma trovo più facile lasciare tutta la parola dei dati e caricare un elenco di dizionari in un ciclo).




La risposta di Piere è corretta, ma un problema è che bulk_save_objects di default non restituisce le chiavi primarie degli oggetti, se questo ti preoccupa. Impostare return_defaults su True per ottenere questo comportamento.

La documentazione è here .

foos = [Foo(bar='a',), Foo(bar='b'), Foo(bar='c')]
session.bulk_save_objects(foos, return_defaults=True)
for foo in foos:
    assert foo.id is not None
session.commit()





Related