Aggiornamento efficiente del database utilizzando SQLAlchemy ORM


116

Sto avviando una nuova applicazione e sto cercando di utilizzare un ORM, in particolare SQLAlchemy.

Diciamo che ho una colonna "pippo" nel mio database e voglio incrementarla. In straight sqlite, questo è facile:

db = sqlite3.connect('mydata.sqlitedb')
cur = db.cursor()
cur.execute('update table stuff set foo = foo + 1')

Ho capito l'equivalente del generatore SQL di SQLAlchemy:

engine = sqlalchemy.create_engine('sqlite:///mydata.sqlitedb')
md = sqlalchemy.MetaData(engine)
table = sqlalchemy.Table('stuff', md, autoload=True)
upd = table.update(values={table.c.foo:table.c.foo+1})
engine.execute(upd)

Questo è leggermente più lento, ma non contiene molto.

Ecco la mia ipotesi migliore per un approccio ORM SQLAlchemy:

# snip definition of Stuff class made using declarative_base
# snip creation of session object
for c in session.query(Stuff):
    c.foo = c.foo + 1
session.flush()
session.commit()

Questo fa la cosa giusta, ma ci vuole poco meno di cinquanta volte il tempo che gli altri due si avvicinano. Presumo sia perché deve portare tutti i dati in memoria prima di poter lavorare con esso.

Esiste un modo per generare l'SQL efficiente utilizzando ORM di SQLAlchemy? O usi un altro ORM python? O dovrei semplicemente tornare a scrivere l'SQL a mano?


1
Ok, presumo che la risposta sia "questo non è qualcosa che gli ORM fanno bene". Oh bene; Vivo e imparo.
John Fouhy

Ci sono stati alcuni esperimenti eseguiti su diversi ORM e come si comportano sotto carico e costrizione. Non ho un link a portata di mano, ma vale la pena leggerlo.
Matthew Schinckel

Un altro problema che esiste con l'ultimo esempio (ORM) è che non è atomico .
Marian

Risposte:


181

L'ORM di SQLAlchemy è pensato per essere utilizzato insieme al livello SQL, non per nasconderlo. Ma devi tenere a mente una o due cose quando usi ORM e SQL semplice nella stessa transazione. Fondamentalmente, da un lato, le modifiche ai dati ORM colpiranno il database solo quando scarichi le modifiche dalla tua sessione. Dall'altro lato, le istruzioni di manipolazione dei dati SQL non influiscono sugli oggetti che si trovano nella sessione.

Quindi se dici

for c in session.query(Stuff).all():
    c.foo = c.foo+1
session.commit()

farà quello che dice, andrà a prendere tutti gli oggetti dal database, modificherà tutti gli oggetti e poi, quando sarà il momento di scaricare le modifiche al database, aggiornerà le righe una per una.

Invece dovresti fare questo:

session.execute(update(stuff_table, values={stuff_table.c.foo: stuff_table.c.foo + 1}))
session.commit()

Ciò verrà eseguito come una query come ci si aspetterebbe e poiché almeno la configurazione della sessione predefinita fa scadere tutti i dati nella sessione al momento del commit, non si hanno problemi con i dati obsoleti.

Nella serie 0.5 quasi rilasciata potresti anche usare questo metodo per l'aggiornamento:

session.query(Stuff).update({Stuff.foo: Stuff.foo + 1})
session.commit()

Ciò fondamentalmente eseguirà la stessa istruzione SQL dello snippet precedente, ma selezionerà anche le righe modificate e farà scadere i dati non aggiornati nella sessione. Se sai che non stai utilizzando alcun dato di sessione dopo l'aggiornamento, puoi anche aggiungerlo synchronize_session=Falseall'istruzione di aggiornamento e sbarazzarti di quella selezione.


2
nel terzo modo, attiverà l'evento orm (come after_update)?
Ken

@Ken, no, non lo farà. Consulta il documento API per Query.update docs.sqlalchemy.org/en/13/orm/… . Invece hai un evento per after_bulk_update docs.sqlalchemy.org/en/13/orm/…
TrilceAC

91
session.query(Clients).filter(Clients.id == client_id_list).update({'status': status})
session.commit()

Prova questo =)


Questo metodo ha funzionato per me. Ma il problema è che è lento. Serve un bel po 'di tempo per pochi 100.000 record di dati. Esiste forse un metodo più veloce?
baermathias

Grazie mille questo approccio ha funzionato per me. È davvero brutto che sqlachemy non abbia un modo più breve per aggiornare la jsoncolonna
Jai Prakash,

6
Per coloro che hanno ancora problemi di prestazioni quando si utilizza questo metodo: per impostazione predefinita, questo potrebbe eseguire prima una SELEZIONE per ogni record e successivamente solo l'AGGIORNAMENTO. Passare synchronize_session = False al metodo update () impedisce che ciò accada, ma assicurati di farlo solo se non usi gli oggetti che aggiorni di nuovo prima del commit ().
teuneboon

25

Esistono diversi modi per AGGIORNARE utilizzando sqlalchemy

1) for c in session.query(Stuff).all():
       c.foo += 1
   session.commit()

2) session.query().\
       update({"foo": (Stuff.foo + 1)})
   session.commit()

3) conn = engine.connect()
   stmt = Stuff.update().\
       values(Stuff.foo = (Stuff.foo + 1))
   conn.execute(stmt)

6

Ecco un esempio di come risolvere lo stesso problema senza dover mappare manualmente i campi:

from sqlalchemy import Column, ForeignKey, Integer, String, Date, DateTime, text, create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.attributes import InstrumentedAttribute

engine = create_engine('postgres://postgres@localhost:5432/database')
session = sessionmaker()
session.configure(bind=engine)

Base = declarative_base()


class Media(Base):
  __tablename__ = 'media'
  id = Column(Integer, primary_key=True)
  title = Column(String, nullable=False)
  slug = Column(String, nullable=False)
  type = Column(String, nullable=False)

  def update(self):
    s = session()
    mapped_values = {}
    for item in Media.__dict__.iteritems():
      field_name = item[0]
      field_type = item[1]
      is_column = isinstance(field_type, InstrumentedAttribute)
      if is_column:
        mapped_values[field_name] = getattr(self, field_name)

    s.query(Media).filter(Media.id == self.id).update(mapped_values)
    s.commit()

Quindi, per aggiornare un'istanza Media, puoi fare qualcosa del genere:

media = Media(id=123, title="Titular Line", slug="titular-line", type="movie")
media.update()

1

Senza test, proverei:

for c in session.query(Stuff).all():
     c.foo = c.foo+1
session.commit()

(IIRC, commit () funziona senza flush ()).

Ho scoperto che a volte eseguire una query di grandi dimensioni e quindi iterare in Python può essere fino a 2 ordini di grandezza più veloce di molte query. Presumo che l'iterazione sull'oggetto query sia meno efficiente rispetto all'iterazione su un elenco generato dal metodo all () dell'oggetto query.

[Si prega di notare il commento qui sotto - questo non ha accelerato affatto le cose].


2
L'aggiunta di .all () e la rimozione di .flush () non hanno modificato affatto l'ora.
John Fouhy,

1

Se è a causa dell'overhead in termini di creazione di oggetti, probabilmente non può essere accelerato affatto con SA.

Se è perché sta caricando oggetti correlati, potresti essere in grado di fare qualcosa con il caricamento lento. Ci sono molti oggetti creati a causa di riferimenti? (IE, l'acquisizione di un oggetto Company ottiene anche tutti gli oggetti People correlati).


Nah, il tavolo è tutto da solo. Non ho mai usato un ORM prima - è solo qualcosa in cui non sono bravi?
John Fouhy,

1
C'è un sovraccarico dovuto alla creazione di oggetti, ma secondo me ne vale la pena: essere in grado di memorizzare oggetti in modo persistente in un database è fantastico.
Matthew Schinckel
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.