Effizientes Aktualisieren der Datenbank mithilfe von SQLAlchemy ORM

116

Ich starte eine neue Anwendung und möchte ein ORM verwenden - insbesondere SQLAlchemy.

Angenommen, ich habe eine Spalte 'foo' in meiner Datenbank und möchte sie erhöhen. In Straight SQLite ist dies einfach:

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

Ich habe das SQLAlchemy SQL-Builder-Äquivalent herausgefunden:

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)

Das ist etwas langsamer, aber es ist nicht viel drin.

Hier ist meine beste Vermutung für einen SQLAlchemy ORM-Ansatz:

# 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()

Dies macht das Richtige, aber es dauert knapp fünfzig Mal so lange wie die beiden anderen Ansätze. Ich nehme an, das liegt daran, dass alle Daten gespeichert werden müssen, bevor sie damit arbeiten können.

Gibt es eine Möglichkeit, das effiziente SQL mit dem ORM von SQLAlchemy zu generieren? Oder mit einem anderen Python-ORM? Oder sollte ich einfach wieder das SQL von Hand schreiben?

John Fouhy
quelle
1
Ok, ich gehe davon aus, dass die Antwort lautet "Dies ist nicht etwas, was ORMs gut machen". Naja; Ich lebe und lerne.
John Fouhy
Es wurden einige Experimente mit verschiedenen ORMs durchgeführt und wie sie unter Last und Belastung funktionieren. Sie haben keinen Link zur Hand, sind aber lesenswert.
Matthew Schinckel
Ein weiteres Problem, das beim letzten Beispiel (ORM) besteht, ist, dass es nicht atomar ist .
Marian

Antworten:

181

Das ORM von SQLAlchemy soll zusammen mit der SQL-Schicht verwendet und nicht ausgeblendet werden. Sie müssen jedoch ein oder zwei Dinge beachten, wenn Sie ORM und einfaches SQL in derselben Transaktion verwenden. Grundsätzlich wirken sich ORM-Datenänderungen von einer Seite nur dann auf die Datenbank aus, wenn Sie die Änderungen aus Ihrer Sitzung löschen. Auf der anderen Seite wirken sich SQL-Datenmanipulationsanweisungen nicht auf die Objekte in Ihrer Sitzung aus.

Also wenn du sagst

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

Es wird das tun, was es sagt, alle Objekte aus der Datenbank abrufen, alle Objekte ändern und dann, wenn es Zeit ist, die Änderungen in der Datenbank zu löschen, die Zeilen einzeln aktualisieren.

Stattdessen sollten Sie Folgendes tun:

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

Dies wird wie erwartet als eine Abfrage ausgeführt. Da mindestens die Standard-Sitzungskonfiguration alle Daten in der Sitzung beim Festschreiben abläuft, treten keine veralteten Datenprobleme auf.

In der fast veröffentlichten 0.5-Serie können Sie diese Methode auch zum Aktualisieren verwenden:

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

Dadurch wird im Grunde dieselbe SQL-Anweisung wie im vorherigen Snippet ausgeführt, es werden jedoch auch die geänderten Zeilen ausgewählt und veraltete Daten in der Sitzung verfallen. Wenn Sie wissen, dass Sie nach dem Update keine Sitzungsdaten verwenden, können Sie diese auch synchronize_session=Falsezur Update-Anweisung hinzufügen und diese Auswahl entfernen.

Ameisen Aasma
quelle
2
Wird es auf die dritte Weise ein orm-Ereignis auslösen (wie after_update)?
Ken
@ Ken, nein, wird es nicht. Weitere Informationen finden Sie im API-Dokument für Query.update docs.sqlalchemy.org/en/13/orm/… . Stattdessen haben Sie ein Ereignis für 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()

Versuchen Sie dies =)

Vin
quelle
Diese Methode hat bei mir funktioniert. Aber das Problem ist, dass es langsam ist. Es braucht eine gute Zeit für ein paar 100.000 Datensätze. Gibt es vielleicht eine schnellere Methode?
Baermathias
Vielen Dank, dieser Ansatz hat bei mir funktioniert. Es ist wirklich schlimm, dass sqlachemy keine kürzere Möglichkeit hat, die jsonSpalte zu aktualisieren
Jai Prakash
6
Für diejenigen, die bei Verwendung dieser Methode immer noch Leistungsprobleme haben: Standardmäßig wird möglicherweise zuerst für jeden Datensatz SELECT und danach nur UPDATE ausgeführt. Wenn Sie synchronize_session = False an die update () -Methode übergeben, wird dies verhindert. Stellen Sie jedoch sicher, dass Sie dies nur tun, wenn Sie die Objekte, die Sie vor dem commit () erneut aktualisieren, nicht verwenden.
Teuneboon
25

Es gibt verschiedene Möglichkeiten, mit sqlalchemy ein UPDATE durchzuführen

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)
Nima Soroush
quelle
6

Hier ist ein Beispiel, wie Sie dasselbe Problem lösen können, ohne die Felder manuell zuordnen zu müssen:

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()

Um eine Medieninstanz zu aktualisieren, können Sie Folgendes tun:

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

Ohne Tests würde ich versuchen:

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

(IIRC, commit () funktioniert ohne flush ()).

Ich habe festgestellt, dass das Ausführen einer großen Abfrage und das anschließende Iterieren in Python manchmal bis zu 2 Größenordnungen schneller sein kann als viele Abfragen. Ich gehe davon aus, dass das Iterieren über das Abfrageobjekt weniger effizient ist als das Iterieren über eine Liste, die mit der all () -Methode des Abfrageobjekts generiert wurde.

[Bitte beachten Sie den Kommentar unten - dies hat die Dinge überhaupt nicht beschleunigt].

Matthew Schinckel
quelle
2
Das Hinzufügen von .all () und das Entfernen von .flush () hat die Zeit überhaupt nicht geändert.
John Fouhy
1

Wenn dies auf den Overhead beim Erstellen von Objekten zurückzuführen ist, kann es mit SA wahrscheinlich überhaupt nicht beschleunigt werden.

Wenn es daran liegt, dass verwandte Objekte geladen werden, können Sie möglicherweise etwas mit verzögertem Laden tun. Werden aufgrund von Referenzen viele Objekte erstellt? (Wenn Sie ein Unternehmensobjekt abrufen, werden auch alle zugehörigen Personenobjekte abgerufen.)

Matthew Schinckel
quelle
Nein, der Tisch ist ganz für sich. Ich habe noch nie einen ORM benutzt - ist das nur etwas, in dem sie schlecht sind?
John Fouhy
1
Das Erstellen von Objekten ist mit einem Overhead verbunden, aber meiner Meinung nach ist es die Strafe wert - es ist fantastisch, Objekte dauerhaft in einer Datenbank speichern zu können.
Matthew Schinckel