SQLAlchemy 2.0 Dokumentation
Änderungen und Migration
- SQLAlchemy 2.0 - Major Migration Guide
- Was ist neu in SQLAlchemy 2.0?
- 2.0 Changelog
- 1.4 Changelog
- 1.3 Changelog
- 1.2 Changelog
- 1.1 Changelog
- 1.0 Changelog
- 0.9 Changelog
- 0.8 Changelog
- 0.7 Changelog
- 0.6 Changelog
- 0.5 Changelog
- 0.4 Changelog
- 0.3 Changelog
- 0.2 Changelog
- 0.1 Changelog
- Was ist neu in SQLAlchemy 1.4?¶
- Haupt-API-Änderungen und Features - Allgemein
- Python 3.6 ist die minimale Python 3-Version; Python 2.7 wird weiterhin unterstützt
- ORM Query ist intern mit select, update, delete vereinheitlicht; 2.0-Stil-Ausführung verfügbar
- ORM
Session.execute()verwendet in allen Fällen „Future“-StilResult-Sets - Transparente SQL-Kompilierungs-Caching hinzugefügt zu allen DQL, DML-Anweisungen in Core, ORM
- Deklarativ ist jetzt in die ORM integriert mit neuen Funktionen
- Python Dataclasses, attrs unterstützt mit deklarativen, imperativen Mappings
- Asynchrone E/A-Unterstützung für Core und ORM
- Viele Core- und ORM-Anweisungsobjekte führen nun einen Großteil ihrer Konstruktion und Validierung in der Kompilierungsphase durch
- Interne Importkonventionen repariert, sodass Code-Linter korrekt funktionieren können
- Unterstützung für SQL-Reguläre-Ausdrucks-Operatoren
- SQLAlchemy 2.0 Deprecations Mode
- API- und Verhaltensänderungen - Core
- Eine SELECT-Anweisung wird nicht mehr implizit als FROM-Klausel betrachtet
- select().join() und outerjoin() fügen JOIN-Kriterien zur aktuellen Abfrage hinzu, anstatt eine Unterabfrage zu erstellen
- Das URL-Objekt ist nun unveränderlich
- select(), case() akzeptieren nun positionelle Ausdrücke
- Alle IN-Ausdrücke rendern Parameter für jeden Wert in der Liste „on the fly“ (z. B. Parametererweiterung)
- Die integrierte FROM-Linting-Funktion warnt vor möglichen kartesischen Produkten in einer SELECT-Anweisung
- Neues Ergebnisobjekt
- RowProxy ist kein "Proxy" mehr; heißt jetzt Row und verhält sich wie ein erweitertes benanntes Tupel.
- SELECT-Objekte und abgeleitete FROM-Klauseln erlauben doppelte Spalten und Spaltenbeschriftungen
- Verbesserte Spaltenbenennung für einfache Spaltenausdrücke mit CAST oder ähnlichem
- Neue "Post-Compile"-gebundene Parameter für LIMIT/OFFSET in Oracle, SQL Server
- Verbindungsebene Transaktionen können nun basierend auf Untertransaktionen inaktiv sein
- Enum- und Boolean-Datentypen werden nicht mehr standardmäßig mit „constraint erstellen“ erstellt
- Neue Features - ORM
- Verhaltensänderungen - ORM
- Das „KeyedTuple“-Objekt, das von Query zurückgegeben wird, wird durch Row ersetzt
- Session-Features neues „autobegin“-Verhalten
- Viewonly-Beziehungen synchronisieren keine Backrefs
- cascade_backrefs-Verhalten für die Entfernung in 2.0 als veraltet markiert
- Eager-Loader werden während des Unexpire-Vorgangs emittiert
- Spalten-Loader wie
deferred(),with_expression()wirken nur, wenn sie auf der äußersten, vollständigen Entitätsabfrage angegeben sind - Der Zugriff auf ein nicht initialisiertes Sammlungsattribut eines transienten Objekts mutiert __dict__ nicht mehr
- Der Fehler „New instance conflicts with existing identity“ ist nun eine Warnung
- Persistenzbezogene Cascade-Operationen sind bei viewonly=True nicht erlaubt
- Strengeres Verhalten beim Abfragen von Vererbungsmappings mit benutzerdefinierten Abfragen
- Dialekt-Änderungen
- pg8000 Mindestversion ist 1.16.6, unterstützt nur Python 3
- psycopg2 Version 2.7 oder höher ist für den PostgreSQL psycopg2-Dialekt erforderlich
- Der psycopg2-Dialekt hat keine Einschränkungen mehr bezüglich der gebundenen Parameternamen
- Der psycopg2-Dialekt verfügt standardmäßig über „execute_values“ mit RETURNING für INSERT-Anweisungen
- „join rewriting“-Logik aus dem SQLite-Dialekt entfernt; Importe aktualisiert
- Sequenzunterstützung für MariaDB 10.3 hinzugefügt
- Sequenzunterstützung getrennt von IDENTITY für SQL Server hinzugefügt
- Haupt-API-Änderungen und Features - Allgemein
- Was ist neu in SQLAlchemy 1.3?
- Was ist neu in SQLAlchemy 1.2?
- Was ist neu in SQLAlchemy 1.1?
- Was ist neu in SQLAlchemy 1.0?
- Was ist neu in SQLAlchemy 0.9?
- Was ist neu in SQLAlchemy 0.8?
- Was ist neu in SQLAlchemy 0.7?
- Was ist neu in SQLAlchemy 0.6?
- Was ist neu in SQLAlchemy 0.5?
- Was ist neu in SQLAlchemy 0.4?
Projektversionen
- Vorher: 0.1 Changelog
- Nächste: Was ist neu in SQLAlchemy 1.3?
- Nach oben: Startseite
- Auf dieser Seite
- Was ist neu in SQLAlchemy 1.4?
- Haupt-API-Änderungen und Features - Allgemein
- Python 3.6 ist die minimale Python 3-Version; Python 2.7 wird weiterhin unterstützt
- ORM Query ist intern mit select, update, delete vereinheitlicht; 2.0-Stil-Ausführung verfügbar
- ORM
Session.execute()verwendet in allen Fällen „Future“-StilResult-Sets - Transparente SQL-Kompilierungs-Caching hinzugefügt zu allen DQL, DML-Anweisungen in Core, ORM
- Deklarativ ist jetzt in die ORM integriert mit neuen Funktionen
- Python Dataclasses, attrs unterstützt mit deklarativen, imperativen Mappings
- Asynchrone E/A-Unterstützung für Core und ORM
- Viele Core- und ORM-Anweisungsobjekte führen nun einen Großteil ihrer Konstruktion und Validierung in der Kompilierungsphase durch
- Interne Importkonventionen repariert, sodass Code-Linter korrekt funktionieren können
- Unterstützung für SQL-Reguläre-Ausdrucks-Operatoren
- SQLAlchemy 2.0 Deprecations Mode
- API- und Verhaltensänderungen - Core
- Eine SELECT-Anweisung wird nicht mehr implizit als FROM-Klausel betrachtet
- select().join() und outerjoin() fügen JOIN-Kriterien zur aktuellen Abfrage hinzu, anstatt eine Unterabfrage zu erstellen
- Das URL-Objekt ist nun unveränderlich
- select(), case() akzeptieren nun positionelle Ausdrücke
- Alle IN-Ausdrücke rendern Parameter für jeden Wert in der Liste „on the fly“ (z. B. Parametererweiterung)
- Die integrierte FROM-Linting-Funktion warnt vor möglichen kartesischen Produkten in einer SELECT-Anweisung
- Neues Ergebnisobjekt
- RowProxy ist kein "Proxy" mehr; heißt jetzt Row und verhält sich wie ein erweitertes benanntes Tupel.
- SELECT-Objekte und abgeleitete FROM-Klauseln erlauben doppelte Spalten und Spaltenbeschriftungen
- Verbesserte Spaltenbenennung für einfache Spaltenausdrücke mit CAST oder ähnlichem
- Neue "Post-Compile"-gebundene Parameter für LIMIT/OFFSET in Oracle, SQL Server
- Verbindungsebene Transaktionen können nun basierend auf Untertransaktionen inaktiv sein
- Enum- und Boolean-Datentypen werden nicht mehr standardmäßig mit „constraint erstellen“ erstellt
- Neue Features - ORM
- Verhaltensänderungen - ORM
- Das „KeyedTuple“-Objekt, das von Query zurückgegeben wird, wird durch Row ersetzt
- Session-Features neues „autobegin“-Verhalten
- Viewonly-Beziehungen synchronisieren keine Backrefs
- cascade_backrefs-Verhalten für die Entfernung in 2.0 als veraltet markiert
- Eager-Loader werden während des Unexpire-Vorgangs emittiert
- Spalten-Loader wie
deferred(),with_expression()wirken nur, wenn sie auf der äußersten, vollständigen Entitätsabfrage angegeben sind - Der Zugriff auf ein nicht initialisiertes Sammlungsattribut eines transienten Objekts mutiert __dict__ nicht mehr
- Der Fehler „New instance conflicts with existing identity“ ist nun eine Warnung
- Persistenzbezogene Cascade-Operationen sind bei viewonly=True nicht erlaubt
- Strengeres Verhalten beim Abfragen von Vererbungsmappings mit benutzerdefinierten Abfragen
- Dialekt-Änderungen
- pg8000 Mindestversion ist 1.16.6, unterstützt nur Python 3
- psycopg2 Version 2.7 oder höher ist für den PostgreSQL psycopg2-Dialekt erforderlich
- Der psycopg2-Dialekt hat keine Einschränkungen mehr bezüglich der gebundenen Parameternamen
- Der psycopg2-Dialekt verfügt standardmäßig über „execute_values“ mit RETURNING für INSERT-Anweisungen
- „join rewriting“-Logik aus dem SQLite-Dialekt entfernt; Importe aktualisiert
- Sequenzunterstützung für MariaDB 10.3 hinzugefügt
- Sequenzunterstützung getrennt von IDENTITY für SQL Server hinzugefügt
- Haupt-API-Änderungen und Features - Allgemein
Was ist neu in SQLAlchemy 1.4?¶
Über dieses Dokument
Dieses Dokument beschreibt die Änderungen zwischen SQLAlchemy Version 1.3 und SQLAlchemy Version 1.4.
Version 1.4 verfolgt einen anderen Fokus als andere SQLAlchemy-Releases, da sie in vielerlei Hinsicht als potenzieller Migrationspunkt für eine dramatischere Reihe von API-Änderungen dient, die derzeit für die Version 2.0 von SQLAlchemy geplant sind. Der Fokus von SQLAlchemy 2.0 ist eine modernisierte und verschlankte API, die viele Nutzungsmuster entfernt, die lange Zeit nicht empfohlen wurden, sowie die besten Ideen in SQLAlchemy als First-Class-API-Features mainstreamt. Ziel ist es, dass es viel weniger Mehrdeutigkeit gibt, wie die API verwendet werden soll, und dass eine Reihe von impliziten Verhaltensweisen und selten verwendeten API-Flags, die die Interna verkomplizieren und die Leistung beeinträchtigen, entfernt werden.
Für den aktuellen Status von SQLAlchemy 2.0 siehe SQLAlchemy 2.0 - Hauptmigrationsanleitung.
Haupt-API-Änderungen und Features - Allgemein¶
Python 3.6 ist die minimale Python 3-Version; Python 2.7 wird weiterhin unterstützt¶
Da Python 3.5 im September 2020 sein EOL erreichte, setzt SQLAlchemy 1.4 nun Version 3.6 als minimale Python 3-Version. Python 2.7 wird weiterhin unterstützt, jedoch wird die SQLAlchemy 1.4-Serie die letzte Serie sein, die Python 2 unterstützt.
ORM Query ist intern mit select, update, delete vereinheitlicht; 2.0-Stil-Ausführung verfügbar¶
Die größte konzeptionelle Änderung an SQLAlchemy für Version 2.0 und im Wesentlichen auch für 1.4 ist, dass die große Trennung zwischen dem Select-Konstrukt in Core und dem Query-Objekt im ORM entfernt wurde, sowie zwischen den Query.update() und Query.delete()-Methoden in Bezug darauf, wie sie zu Update und Delete stehen.
In Bezug auf Select und Query hatten diese beiden Objekte seit vielen Versionen ähnliche, weitgehend überlappende APIs und sogar eine gewisse Fähigkeit, zwischen dem einen und dem anderen zu wechseln, während sie sich in ihren Nutzungsmustern und Verhaltensweisen stark unterschieden. Der historische Hintergrund dafür war, dass das Query-Objekt eingeführt wurde, um Mängel im Select-Objekt zu überwinden, das früher das Kernstück der Abfrage von ORM-Objekten war, außer dass sie in Bezug auf nur Table-Metadaten abgefragt werden mussten. Query hatte jedoch nur eine vereinfachte Schnittstelle zum Laden von Objekten, und erst im Laufe vieler Hauptversionen erhielt es schließlich die meiste Flexibilität des Select-Objekts, was dann zu der anhaltenden Unannehmlichkeit führte, dass diese beiden Objekte sehr ähnlich, aber immer noch weitgehend inkompatibel miteinander wurden.
In Version 1.4 werden alle Core- und ORM SELECT-Anweisungen direkt aus einem Select-Objekt gerendert; wenn das Query-Objekt verwendet wird, kopiert es zur Laufzeit seinen Zustand in ein Select, das dann intern mit 2.0-Stil-Ausführung aufgerufen wird. Zukünftig wird das Query-Objekt nur noch legacy sein, und Anwendungen werden ermutigt, zur 2.0-Stil-Ausführung zu wechseln, die es Core-Konstrukten ermöglicht, frei gegen ORM-Entitäten verwendet zu werden.
with Session(engine, future=True) as sess:
stmt = (
select(User)
.where(User.name == "sandy")
.join(User.addresses)
.where(Address.email_address.like("%gmail%"))
)
result = sess.execute(stmt)
for user in result.scalars():
print(user)Dinge, die man über das obige Beispiel beachten sollte
Die Objekte
Sessionundsessionmakerverfügen nun über eine vollständige Kontextmanager-Funktionalität (d. h. diewith:-Anweisung); siehe die überarbeitete Dokumentation unter Öffnen und Schließen einer Session für ein Beispiel.Innerhalb der 1.4-Serie verwendet jede 2.0-Stil ORM-Aufruf eine
Session, die das FlagSession.futureaufTruegesetzt hat; dieses Flag zeigt an, dass dieSession2.0-Stil-Verhaltensweisen aufweisen soll, einschließlich der Möglichkeit, ORM-Abfragen vonexecuteaus aufzurufen, sowie einiger Änderungen bei Transaktionsfunktionen. In Version 2.0 wird dieses Flag immerTruesein.Das
select()-Konstrukt benötigt keine Klammern mehr um die Spaltenklausel; siehe select(), case() akzeptieren nun positionelle Ausdrücke für Hintergrundinformationen zu dieser Verbesserung.Das Objekt
select()/Selectverfügt über eine MethodeSelect.join(), die der desQuery-Objekts ähnelt und sogar ein ORM-Beziehungsattribut unterstützt (ohne die Trennung zwischen Core und ORM zu verletzen!) - siehe select().join() und outerjoin() fügen JOIN-Kriterien zur aktuellen Abfrage hinzu, anstatt eine Unterabfrage zu erstellen für Hintergrundinformationen dazu.Anweisungen, die mit ORM-Entitäten arbeiten und ORM-Ergebnisse zurückgeben sollen, werden über
Session.execute()aufgerufen. Siehe Abfragen für eine Einführung. Siehe auch die folgende Notiz unter ORM Session.execute() verwendet in allen Fällen „Future“-Stil Result-Sets.ein
Result-Objekt wird zurückgegeben, anstatt einer einfachen Liste, das selbst eine viel fortschrittlichere Version des vorherigenResultProxy-Objekts ist; dieses Objekt wird nun sowohl für Core- als auch für ORM-Ergebnisse verwendet. Siehe Neues Result-Objekt, RowProxy ist keine „Proxy“ mehr; heißt jetzt Row und verhält sich wie ein erweitertes benanntes Tupel und Das „KeyedTuple“-Objekt, das von Query zurückgegeben wird, wird durch Row ersetzt für Informationen dazu.
In der gesamten Dokumentation von SQLAlchemy wird es viele Verweise auf 1.x-Stil und 2.0-Stil-Ausführung geben. Dies dient zur Unterscheidung zwischen den beiden Abfragestilen und zur Dokumentation des neuen Aufrufstils für die Zukunft. In SQLAlchemy 2.0 mag das Query-Objekt als Legacy-Konstrukt bestehen bleiben, wird aber in den meisten Dokumentationen nicht mehr hervorgehoben werden.
Ähnliche Anpassungen wurden an „Bulk Updates und Deletes“ vorgenommen, sodass Core update() und delete() für Bulk-Operationen verwendet werden können. Ein Bulk-Update wie das Folgende
session.query(User).filter(User.name == "sandy").update(
{"password": "foobar"}, synchronize_session="fetch"
)kann nun im 2.0-Stil (und tatsächlich läuft das obige intern auf diese Weise) wie folgt erreicht werden
with Session(engine, future=True) as sess:
stmt = (
update(User)
.where(User.name == "sandy")
.values(password="foobar")
.execution_options(synchronize_session="fetch")
)
sess.execute(stmt)Beachten Sie die Verwendung der Methode Executable.execution_options(), um ORM-bezogene Optionen zu übergeben. Die Verwendung von „execution options“ ist nun sowohl in Core als auch in ORM viel verbreiteter, und viele ORM-bezogene Methoden von Query werden nun als execution options implementiert (siehe Query.execution_options() für einige Beispiele).
Siehe auch
ORM Session.execute() verwendet in allen Fällen „Future“-Stil Result-Sets¶
Wie in RowProxy ist keine „Proxy“ mehr; heißt jetzt Row und verhält sich wie ein erweitertes benanntes Tupel erwähnt, verfügen die Objekte Result und Row nun über „benanntes Tupel“-Verhalten, wenn sie mit einer Engine verwendet werden, bei der der Parameter create_engine.future auf True gesetzt ist. Diese „benannten Tupel“-Zeilen enthalten insbesondere eine Verhaltensänderung, nämlich dass Python-Containment-Ausdrücke mit in, wie z. B.
>>> engine = create_engine("...", future=True)
>>> conn = engine.connect()
>>> row = conn.execute.first()
>>> "name" in row
TrueDer obige Containment-Test verwendet **Wert-Containment**, nicht **Schlüssel-Containment**; die row müsste einen **Wert** von „name“ haben, um True zurückzugeben.
Unter SQLAlchemy 1.4, wenn der Parameter create_engine.future auf False gesetzt ist, werden Legacy-Stil LegacyRow-Objekte zurückgegeben, die das teilweise benannte Tupel-Verhalten früherer SQLAlchemy-Versionen aufweisen, bei dem Containment-Prüfungen weiterhin Schlüssel-Containment verwenden; "name" in row würde True zurückgeben, wenn die Zeile eine **Spalte** mit dem Namen „name“ hat, anstatt eines Wertes.
Bei der Verwendung von Session.execute() ist der vollständige benannte Tupel-Stil **bedingungslos** aktiviert, was bedeutet, dass "name" in row als Test **Wert-Containment** verwendet und **nicht** Schlüssel-Containment. Dies dient dazu, dass Session.execute() nun ein Result zurückgibt, das auch ORM-Ergebnisse berücksichtigt, bei denen selbst Legacy-ORM-Ergebniszeilen wie die von Query.all() zurückgegebenen Wert-Containment verwenden.
Dies ist eine Verhaltensänderung von SQLAlchemy 1.3 zu 1.4. Um weiterhin Sammlungen mit Schlüssel-Containment zu erhalten, verwenden Sie die Methode Result.mappings(), um ein MappingResult zu erhalten, das Zeilen als Dictionaries zurückgibt.
for dict_row in session.execute(text("select id from table")).mappings():
assert "id" in dict_rowTransparente SQL-Kompilierungs-Caching hinzugefügt zu allen DQL, DML-Anweisungen in Core, ORM¶
Eine der umfassendsten Änderungen, die jemals in einer einzigen SQLAlchemy-Version eingeführt wurden, eine monatelange Reorganisation und Refactoring aller Abfragesysteme von der Basis von Core bis hin zum ORM ermöglicht nun, dass der Großteil der Python-Berechnungen, die zur Erzeugung von SQL-Strings und zugehörigen Anweisungsmetadaten aus einer vom Benutzer erstellten Anweisung erforderlich sind, im Speicher zwischengespeichert werden, sodass nachfolgende Aufrufe einer identischen Anweisungskonstruktion 35-60 % weniger CPU-Ressourcen verbrauchen.
Dieses Caching geht über die Konstruktion des SQL-Strings hinaus und umfasst auch die Konstruktion von Result-Fetching-Strukturen, die die SQL-Konstruktion mit dem Result-Set verknüpfen, und im ORM umfasst es die Berücksichtigung von ORM-aktivierten Attribut-Loadern, Beziehungs-Eager-Loadern und anderen Optionen sowie Objektkonstruktionsroutinen, die jedes Mal aufgebaut werden müssen, wenn eine ORM-Abfrage ausgeführt wird und ORM-Objekte aus Result-Sets konstruiert werden.
Um die allgemeine Idee des Features einzuführen, gegebenen Code aus der Performance-Suite wie folgt, der eine sehr einfache Abfrage „n“-mal aufruft, für einen Standardwert von n=10000. Die Abfrage gibt nur eine einzige Zeile zurück, da der Overhead, den wir reduzieren wollen, der von **vielen kleinen Abfragen** ist. Die Optimierung ist nicht so signifikant für Abfragen, die viele Zeilen zurückgeben.
session = Session(bind=engine)
for id_ in random.sample(ids, n):
result = session.query(Customer).filter(Customer.id == id_).one()Dieses Beispiel in der 1.3-Version von SQLAlchemy auf einem Dell XPS13 unter Linux wird wie folgt abgeschlossen
test_orm_query : (10000 iterations); total time 3.440652 secIn 1.4 schließt der obige Code ohne Modifikation ab
test_orm_query : (10000 iterations); total time 2.367934 secDieser erste Test zeigt, dass reguläre ORM-Abfragen bei Verwendung von Caching über viele Iterationen im Bereich von **30 % schneller** laufen können.
Eine zweite Variante des Features ist die optionale Verwendung von Python-Lambdas, um die Konstruktion der Abfrage selbst zu verzögern. Dies ist eine fortschrittlichere Variante des Ansatzes, der von der „Baked Query“-Erweiterung verwendet wird, die in Version 1.0.0 eingeführt wurde. Das „Lambda“-Feature kann in einem Stil verwendet werden, der dem von gebackenen Abfragen sehr ähnlich ist, außer dass es ad-hoc für jede SQL-Konstruktion verfügbar ist. Es beinhaltet zusätzlich die Fähigkeit, jede Invokation des Lambdas nach gebundenen Literalwerten zu scannen, die sich bei jeder Invokation ändern, sowie Änderungen an anderen Konstrukten, wie z. B. das Abfragen aus einer anderen Entität oder Spalte jedes Mal, ohne den eigentlichen Code jedes Mal ausführen zu müssen.
Die Verwendung dieser API sieht wie folgt aus
session = Session(bind=engine)
for id_ in random.sample(ids, n):
stmt = lambda_stmt(lambda: future_select(Customer))
stmt += lambda s: s.where(Customer.id == id_)
session.execute(stmt).scalar_one()Der obige Code wird abgeschlossen
test_orm_query_newstyle_w_lambdas : (10000 iterations); total time 1.247092 secDieser Test zeigt, dass die Verwendung des neueren „select()“-Stils von ORM-Abfragen in Verbindung mit einer vollständigen „baked“-Stil-Invocation, die die gesamte Konstruktion cacht, über viele Iterationen im Bereich von **60 % schneller** laufen kann und eine Leistung erzielt, die etwa der des gebackenen Abfragesystems entspricht, das nun durch das native Caching-System abgelöst wurde.
Das neue System nutzt die vorhandene Ausführungsoption Connection.execution_options.compiled_cache und fügt dem Engine direkt einen Cache hinzu, der mit dem Parameter Engine.query_cache_size konfiguriert wird.
Ein erheblicher Teil der API- und Verhaltensänderungen in 1.4 wurde zur Unterstützung dieses neuen Features vorgenommen.
Siehe auch
Deklarative ist nun in das ORM mit neuen Features integriert¶
Nach etwa zehn Jahren Popularität ist das Paket sqlalchemy.ext.declarative nun in den Namensraum sqlalchemy.orm integriert, mit Ausnahme der deklarativen „Extension“-Klassen, die als deklarative Extensions verbleiben.
Die neuen Klassen, die sqlalchemy.orm hinzugefügt wurden, umfassen
registry– eine neue Klasse, die die Rolle der „deklarativen Basis“-Klasse ablöst und als Registry von gemappten Klassen dient, auf die über einen Stringnamen inrelationship()-Aufrufen verwiesen werden kann und die unabhängig vom Stil ist, in dem eine bestimmte Klasse gemappt wurde.declarative_base()– dies ist die gleiche deklarative Basisklasse, die während der gesamten Laufzeit des deklarativen Systems verwendet wurde, außer dass sie nun intern auf einregistry-Objekt verweist und von der Methoderegistry.generate_base()implementiert wird, die von einerregistrydirekt aufgerufen werden kann. Die Funktiondeclarative_base()erstellt diese Registry automatisch, sodass keine Auswirkungen auf bestehenden Code entstehen. Der Namesqlalchemy.ext.declarative.declarative_baseist weiterhin vorhanden und gibt eine 2.0-Deprecationswarnung aus, wenn der 2.0-Deprecationsmodus aktiviert ist.declared_attr()– derselbe „declared attr“-Funktionsaufruf ist nun Teil vonsqlalchemy.orm. Der Namesqlalchemy.ext.declarative.declared_attrist weiterhin vorhanden und gibt eine 2.0-Deprecationswarnung aus, wenn der 2.0-Deprecationsmodus aktiviert ist.Andere Namen, die in
sqlalchemy.ormverschoben wurden, umfassenhas_inherited_table(),synonym_for(),DeclarativeMeta,as_declarative().
Zusätzlich ist die Funktion instrument_declarative() veraltet und wird durch registry.map_declaratively() ersetzt. Die Klassen ConcreteBase, AbstractConcreteBase und DeferredReflection bleiben als Extensions im Paket Deklarative Extensions erhalten.
Mapping-Stile wurden nun so organisiert, dass sie alle von der registry-Objekt abgeleitet werden und fallen in diese Kategorien
- Deklarative Zuordnung
- Verwendung der Basisklasse
declarative_base()mit Metaklasse
- Verwendung der Basisklasse
- Verwendung des
registry.mapped()-Dekorators für deklarative Mappings Deklarative Tabelle
- Imperative Tabelle (Hybrid)
- Verwendung des
Die vorhandene Funktion für klassische Mappings sqlalchemy.orm.mapper() bleibt bestehen, jedoch wird es als veraltet angesehen, sqlalchemy.orm.mapper() direkt aufzurufen; die neue Methode registry.map_imperatively() leitet die Anfrage nun über die sqlalchemy.orm.registry(), damit sie eindeutig mit anderen deklarativen Mappings integriert wird.
Der neue Ansatz interoperiert mit 3rd-Party-Klasseninstrumentierungssystemen, die notwendigerweise auf der Klasse stattfinden müssen, bevor der Mapping-Prozess beginnt, und ermöglicht es deklarativen Mappings, über einen Dekorator anstelle einer deklarativen Basis zu funktionieren, sodass Pakete wie dataclasses und attrs mit deklarativen Mappings verwendet werden können, zusätzlich zur Arbeit mit klassischen Mappings.
Die deklarative Dokumentation wurde nun vollständig in die ORM-Mapper-Konfigurationsdokumentation integriert und enthält Beispiele für alle Mapping-Stile, die an einem Ort organisiert sind. Siehe den Abschnitt Übersicht über ORM gemappte Klassen für den Anfang der neu organisierten Dokumentation.
Python Dataclasses, attrs unterstützt mit deklarativen, imperativen Mappings¶
Zusammen mit den neuen deklarativen Dekorator-Stilen, die in Deklarative ist nun in das ORM mit neuen Features integriert eingeführt wurden, ist der Mapper nun explizit mit dem Python-Modul dataclasses vertraut und erkennt Attribute, die auf diese Weise konfiguriert sind, und beginnt, sie zu mappen, ohne sie zu überspringen, wie es zuvor der Fall war. Im Falle des attrs-Moduls entfernt attrs bereits seine eigenen Attribute von der Klasse und war daher bereits mit SQLAlchemy-klassischen Mappings kompatibel. Mit der Einführung des registry.mapped()-Dekorators können nun auch beide Attributsysteme mit deklarativen Mappings interoperieren.
Asynchrone E/A-Unterstützung für Core und ORM¶
SQLAlchemy unterstützt jetzt mit seinem völlig neuen asyncio-Frontend-Interface Python asyncio-kompatible Datenbanktreiber für die Verwendung von Connection in Core und von Session in ORM unter Verwendung der Objekte AsyncConnection und AsyncSession.
Hinweis
Die neue asyncio-Funktion sollte für die ersten Veröffentlichungen von SQLAlchemy 1.4 als Alpha-Level betrachtet werden. Dies sind brandneue Funktionen, die einige bisher unbekannte Programmiertechniken verwenden.
Die anfänglich unterstützte Datenbank-API ist der asyncpg asyncio-Treiber für PostgreSQL.
Die internen Funktionen von SQLAlchemy sind vollständig integriert, indem die greenlet-Bibliothek verwendet wird, um den Ausführungsfluss innerhalb der SQLAlchemy-Interna anzupassen und die asyncio await-Schlüsselwörter vom Datenbanktreiber zur Endbenutzer-API weiterzuleiten, die über async-Methoden verfügt. Mit diesem Ansatz ist der asyncpg-Treiber innerhalb der eigenen Testsuite von SQLAlchemy voll funktionsfähig und bietet Kompatibilität mit den meisten psycopg2-Funktionen. Der Ansatz wurde von den Entwicklern des greenlet-Projekts geprüft und verbessert, wofür SQLAlchemy dankbar ist.
Die für den Benutzer sichtbare async-API konzentriert sich auf IO-orientierte Methoden wie AsyncEngine.connect() und AsyncConnection.execute(). Die neuen Core-Konstrukte unterstützen ausschließlich die Verwendung im 2.0-Stil; das bedeutet, dass alle Anweisungen mit einem Verbindungsobjekt, in diesem Fall AsyncConnection, aufgerufen werden müssen.
Innerhalb des ORM wird die Abfrageausführung im 2.0-Stil unterstützt, unter Verwendung von select()-Konstrukten in Verbindung mit AsyncSession.execute(); das Legacy Query-Objekt selbst wird von der Klasse AsyncSession nicht unterstützt.
ORM-Funktionen wie das verzögerte Laden verknüpfter Attribute sowie die Entwertung abgelaufener Attribute sind per Definition im traditionellen asyncio-Programmiermodell nicht zulässig, da sie IO-Operationen darstellen, die implizit im Geltungsbereich einer Python getattr()-Operation ausgeführt würden. Um dies zu überwinden, sollte die traditionelle asyncio-Anwendung von Techniken des Eager Loading Gebrauch machen und auf die Verwendung von Funktionen wie expire on commit verzichten, damit solche Ladevorgänge nicht benötigt werden.
Für den asyncio-Anwendungsentwickler, der sich entscheidet, von der Tradition abzuweichen, bietet die neue API eine strikt optionale Funktion, sodass Anwendungen, die solche ORM-Funktionen nutzen möchten, dazu in der Lage sind, datenbankbezogenen Code in Funktionen zu organisieren, die dann mithilfe der Methode AsyncSession.run_sync() in Greenlets ausgeführt werden können. Siehe das Beispiel greenlet_orm.py unter Asyncio Integration zur Demonstration.
Unterstützung für asynchrone Cursor wird ebenfalls über die neuen Methoden AsyncConnection.stream() und AsyncSession.stream() bereitgestellt, die ein neues AsyncResult-Objekt unterstützen, das selbst aufrufbare Versionen gängiger Methoden wie AsyncResult.all() und AsyncResult.fetchmany() bereitstellt. Sowohl Core als auch ORM sind mit der Funktion integriert, die der Verwendung von "Server-Side-Cursorn" im traditionellen SQLAlchemy entspricht.
Viele Core- und ORM-Anweisungsobjekte führen nun einen Großteil ihrer Konstruktion und Validierung in der Kompilierungsphase durch¶
Eine Hauptinitiative in der Serie 1.4 ist die Annäherung an das Modell sowohl der Core SQL-Anweisungen als auch der ORM Query, um ein effizientes, cachbares Modell der Anweisungserstellung und -kompilierung zu ermöglichen, wobei der Kompilierungsschritt basierend auf einem Schlüssel, der vom erstellten Anweisungsobjekt generiert wird, zwischengespeichert wird, wobei letzteres für jede Verwendung neu erstellt wird. Zu diesem Ziel wird ein Großteil der Python-Berechnungen, die bei der Erstellung von Anweisungen auftreten, insbesondere die der ORM Query sowie des select()-Konstrukts bei der Erzeugung von ORM-Abfragen, in die Kompilierungsphase der Anweisung verlagert, die nur auftritt, nachdem die Anweisung aufgerufen wurde, und nur, wenn die kompilierte Form der Anweisung noch nicht zwischengespeichert wurde.
Aus Sicht des Endbenutzers bedeutet dies, dass einige Fehlermeldungen, die aufgrund von übergebenen Argumenten für das Objekt auftreten können, nicht mehr sofort ausgelöst werden, sondern erst, wenn die Anweisung zum ersten Mal aufgerufen wird. Diese Bedingungen sind immer strukturell und nicht datengesteuert, sodass kein Risiko besteht, dass eine solche Bedingung aufgrund einer zwischengespeicherten Anweisung übersehen wird.
Fehlerbedingungen, die in diese Kategorie fallen, sind
Wenn ein
_selectable.CompoundSelectkonstruiert wird (z. B. UNION, EXCEPT usw.) und die übergebenen SELECT-Anweisungen nicht die gleiche Anzahl von Spalten haben, wird jetzt einCompileErrorausgelöst; zuvor wurde sofort nach der Anweisungserstellung einArgumentErrorausgelöst.Verschiedene Fehlerbedingungen, die beim Aufruf von
Query.join()auftreten können, werden zur Zeit der Anweisungskompilierung ausgewertet und nicht erst, wenn die Methode zum ersten Mal aufgerufen wird.
Andere Dinge, die sich ändern könnten, betreffen das Query-Objekt direkt
Das Verhalten kann sich beim Zugriff auf den
Query.statement-Accessor geringfügig unterscheiden. Das zurückgegebeneSelect-Objekt ist nun eine direkte Kopie desselben Zustands, der imQueryvorhanden war, ohne dass eine ORM-spezifische Kompilierung durchgeführt wird (was bedeutet, dass es dramatisch schneller ist). Allerdings wird dasSelectnicht mehr denselben internen Zustand wie in 1.3 haben, einschließlich Dingen wie der expliziten Angabe der FROM-Klauseln, falls diese in derQuerynicht explizit angegeben wurden. Dies bedeutet, dass Code, der sich auf die Manipulation diesesSelect-Statements stützt, wie z. B. Aufrufe von Methoden wieSelect.with_only_columns(), die FROM-Klausel berücksichtigen muss.
Interne Importkonventionen repariert, damit Code-Linter korrekt funktionieren¶
SQLAlchemy verwendet seit langem einen Parameter-Injektions-Decorator, um gegenseitig abhängige Modulimporte aufzulösen, wie z. B.
@util.dependency_for("sqlalchemy.sql.dml")
def insert(self, dml, *args, **kw): ...Wobei die obige Funktion so umgeschrieben würde, dass sie den Parameter dml nicht mehr außerhalb hat. Dies würde Code-Linting-Tools verwirren, die einen fehlenden Parameter für Funktionen sehen. Ein neuer Ansatz wurde intern implementiert, sodass die Signatur der Funktion nicht mehr geändert wird und das Modulobjekt stattdessen innerhalb der Funktion beschafft wird.
Unterstützung für SQL-Reguläre-Ausdrucks-Operatoren¶
Eine lang erwartete Funktion zur rudimentären Unterstützung von Datenbank-Regulären-Ausdrucks-Operatoren, um die Operationen der Suiten ColumnOperators.like() und ColumnOperators.match() zu ergänzen. Die neuen Funktionen umfassen ColumnOperators.regexp_match(), die eine reguläre Ausdrucks-Match-Funktion implementiert, und ColumnOperators.regexp_replace(), die eine reguläre Ausdrucks-String-Ersetzungsfunktion implementiert.
Zu den unterstützten Backends gehören SQLite, PostgreSQL, MySQL / MariaDB und Oracle. Das SQLite-Backend unterstützt nur "regexp_match", aber nicht "regexp_replace".
Die Syntax und Flags für reguläre Ausdrücke sind nicht Backend-agnostisch. Eine zukünftige Funktion wird es ermöglichen, mehrere Syntaxen für reguläre Ausdrücke gleichzeitig anzugeben, um zwischen verschiedenen Backends on the fly zu wechseln.
Für SQLite wird die Funktion re.search() von Python ohne zusätzliche Argumente als Implementierung festgelegt.
Siehe auch
ColumnOperators.regexp_match()
ColumnOperators.regexp_replace()
Unterstützung für reguläre Ausdrücke - Hinweise zur SQLite-Implementierung
SQLAlchemy 2.0 Deprecation Mode¶
Eines der Hauptziele der Version 1.4 ist es, eine "Übergangs"-Version bereitzustellen, damit Anwendungen schrittweise zu SQLAlchemy 2.0 migrieren können. Zu diesem Zweck ist ein Hauptmerkmal der Version 1.4 der "2.0 Deprecation Mode", eine Reihe von Deprecation-Warnungen, die gegen jedes erkennbare API-Muster ausgegeben werden, das sich in Version 2.0 anders verhalten wird. Die Warnungen verwenden alle die Klasse RemovedIn20Warning. Da sich diese Warnungen auf grundlegende Muster auswirken, einschließlich der Konstrukte select() und Engine, können selbst einfache Anwendungen viele Warnungen erzeugen, bis entsprechende API-Änderungen vorgenommen werden. Der Warnmodus ist daher standardmäßig deaktiviert, bis der Entwickler die Umgebungsvariable SQLALCHEMY_WARN_20=1 aktiviert.
Eine vollständige Anleitung zur Verwendung des 2.0 Deprecation Mode finden Sie unter Migration zu 2.0 Schritt Zwei - RemovedIn20Warnings aktivieren.
API- und Verhaltensänderungen - Core¶
Eine SELECT-Anweisung wird nicht mehr implizit als FROM-Klausel betrachtet¶
Diese Änderung ist eine der größten konzeptionellen Änderungen in SQLAlchemy seit vielen Jahren. Es wird jedoch gehofft, dass die Auswirkungen auf den Endbenutzer relativ gering sind, da die Änderung viel genauer dem entspricht, was Datenbanken wie MySQL und PostgreSQL ohnehin verlangen.
Die unmittelbarste spürbare Auswirkung ist, dass ein select() nicht mehr direkt in ein anderes select() eingebettet werden kann, ohne das innere select() zuerst explizit in eine Unterabfrage umzuwandeln. Dies wurde historisch durch die Verwendung der Methode SelectBase.alias() durchgeführt, die weiterhin existiert, aber expliziter durch die Verwendung einer neuen Methode SelectBase.subquery() unterstützt wird; beide Methoden tun dasselbe. Das zurückgegebene Objekt ist nun Subquery, das dem Alias-Objekt sehr ähnlich ist und sich eine gemeinsame Basis AliasedReturnsRows teilt.
Das heißt, dies wird jetzt ausgelöst
stmt1 = select(user.c.id, user.c.name)
stmt2 = select(addresses, stmt1).select_from(addresses.join(stmt1))Auslösen
sqlalchemy.exc.ArgumentError: Column expression or FROM clause expected,
got <...Select object ...>. To create a FROM clause from a <class
'sqlalchemy.sql.selectable.Select'> object, use the .subquery() method.Die korrekte Aufrufform ist stattdessen (beachten Sie auch, dass Klammern für select() nicht mehr erforderlich sind)
sq1 = select(user.c.id, user.c.name).subquery()
stmt2 = select(addresses, sq1).select_from(addresses.join(sq1))Beachten Sie oben, dass die Methode SelectBase.subquery() im Wesentlichen der Verwendung der Methode SelectBase.alias() entspricht.
Die Begründung für diese Änderung basiert auf Folgendem
Um die Vereinheitlichung von
SelectmitQueryzu unterstützen, muss dasSelect-Objekt die MethodenSelect.join()undSelect.outerjoin()haben, die JOIN-Kriterien zur bestehenden FROM-Klausel hinzufügen, wie es Benutzer ohnehin immer erwartet haben. Das frühere Verhalten, das mit dem einesFromClauseübereinstimmen musste, war, dass es eine unbenannte Unterabfrage generierte und dann dazu jointe, was eine völlig nutzlose Funktion war, die nur die unglücklichen Benutzer verwirrte, die dies versuchten. Diese Änderung wird diskutiert unter select().join() und outerjoin() fügen JOIN-Kriterien zur aktuellen Abfrage hinzu, anstatt eine Unterabfrage zu erstellen.Das Einbeziehen eines SELECT in die FROM-Klausel eines anderen SELECT, ohne zuerst einen Alias oder eine Unterabfrage zu erstellen, hätte eine unbenannte Unterabfrage zur Folge. Während Standard-SQL diese Syntax unterstützt, wird sie in der Praxis von den meisten Datenbanken abgelehnt. Zum Beispiel lehnen sowohl MySQL als auch PostgreSQL die Verwendung unbenannter Unterabfragen kategorisch ab
# MySQL / MariaDB: MariaDB [(none)]> select * from (select 1); ERROR 1248 (42000): Every derived table must have its own alias # PostgreSQL: test=> select * from (select 1); ERROR: subquery in FROM must have an alias LINE 1: select * from (select 1); ^ HINT: For example, FROM (SELECT ...) [AS] foo.
Datenbanken wie SQLite akzeptieren sie, aber es ist immer noch oft der Fall, dass die aus einer solchen Unterabfrage resultierenden Namen zu mehrdeutig sind, um nützlich zu sein
sqlite> CREATE TABLE a(id integer); sqlite> CREATE TABLE b(id integer); sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=id; Error: ambiguous column name: id sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=b.id; Error: no such column: b.id # use a name sqlite> SELECT * FROM a JOIN (SELECT * FROM b) AS anon_1 ON a.id=anon_1.id;
Da SelectBase-Objekte keine FromClause-Objekte mehr sind, sind Attribute wie das .c-Attribut sowie Methoden wie .select() nun veraltet, da sie die implizite Erzeugung einer Unterabfrage implizieren. Die Methoden .join() und .outerjoin() sind nun umfunktioniert, um JOIN-Kriterien zur bestehenden Abfrage hinzuzufügen, ähnlich wie bei Query.join(), was Benutzer von diesen Methoden ohnehin immer erwartet haben.
Anstelle des .c-Attributs wird ein neues Attribut SelectBase.selected_columns hinzugefügt. Dieses Attribut löst sich zu einer Spaltensammlung auf, die das ist, was die meisten Leute von .c erwarten (aber nicht erhalten), nämlich die Referenzierung der Spalten, die sich in der Spaltenklausel der SELECT-Anweisung befinden. Ein häufiger Anfängerfehler ist Code wie der folgende
stmt = select(users)
stmt = stmt.where(stmt.c.name == "foo")Der obige Code erscheint intuitiv und würde „SELECT * FROM users WHERE name=’foo’” generieren, aber erfahrene SQLAlchemy-Benutzer erkennen, dass er tatsächlich eine nutzlose Unterabfrage generiert, die wie „SELECT * FROM (SELECT * FROM users) WHERE name=’foo’” aussieht.
Das neue Attribut SelectBase.selected_columns eignet sich jedoch für den oben genannten Anwendungsfall, da es in einem Fall wie dem obigen direkt auf die Spalten verweist, die sich in der users.c-Sammlung befinden.
stmt = select(users)
stmt = stmt.where(stmt.selected_columns.name == "foo")select().join() und outerjoin() fügen JOIN-Kriterien zur aktuellen Abfrage hinzu, anstatt eine Unterabfrage zu erstellen¶
Im Hinblick auf die Vereinheitlichung von Query und Select, insbesondere für die Verwendung von Select im 2.0-Stil, war es entscheidend, eine funktionierende Select.join()-Methode zu haben, die sich wie die Query.join()-Methode verhält und zusätzliche Einträge zur FROM-Klausel des bestehenden SELECT hinzufügt und dann das neue Select-Objekt für weitere Modifikationen zurückgibt, anstatt das Objekt in eine unbenannte Unterabfrage zu verpacken und einen JOIN von dieser Unterabfrage zurückzugeben, ein Verhalten, das schon immer praktisch nutzlos und für Benutzer völlig irreführend war.
Um dies zu ermöglichen, wurde Eine SELECT-Anweisung wird nicht mehr implizit als FROM-Klausel betrachtet zuerst implementiert, was Select davon befreite, eine FromClause sein zu müssen; dies entfernte die Anforderung, dass Select.join() ein Join-Objekt zurückgeben müsste, anstatt einer neuen Version dieses Select-Objekts, das einen neuen JOIN in seiner FROM-Klausel enthält.
Von diesem Zeitpunkt an, da Select.join() und Select.outerjoin() ein bestehendes Verhalten hatten, war der ursprüngliche Plan, dass diese Methoden veraltet würden und die neue "nützliche" Version der Methoden auf einem alternativen, "zukünftigen" Select-Objekt verfügbar wäre, das als separater Import erhältlich ist.
Nach einiger Zeit der Arbeit mit dieser spezifischen Codebasis wurde jedoch entschieden, dass zwei verschiedene Arten von Select-Objekten, die jeweils 95% des gleichen Verhaltens aufweisen, abgesehen von geringfügigen Unterschieden im Verhalten einiger Methoden, irreführender und umständlicher wären, als einfach eine harte Änderung des Verhaltens dieser beiden Methoden vorzunehmen, da das bestehende Verhalten von Select.join() und Select.outerjoin() praktisch nie verwendet wird und nur zu Verwirrung führt.
Daher wurde entschieden, angesichts des sehr geringen Nutzens des aktuellen Verhaltens und des extrem hohen Nutzens und der Wichtigkeit des neuen Verhaltens, eine harte Verhaltensänderung in diesem einen Bereich vorzunehmen, anstatt noch ein Jahr zu warten und dazwischen eine umständlichere API zu haben. SQLAlchemy-Entwickler nehmen es nicht leicht, eine solche komplett brechende Änderung vorzunehmen, aber dies ist ein sehr besonderer Fall und es ist äußerst unwahrscheinlich, dass die frühere Implementierung dieser Methoden verwendet wurde; wie bereits in Eine SELECT-Anweisung wird nicht mehr implizit als FROM-Klausel betrachtet erwähnt, erlauben große Datenbanken wie MySQL und PostgreSQL ohnehin keine unbenannten Unterabfragen, und aus syntaktischer Sicht ist es fast unmöglich, dass ein JOIN aus einer unbenannten Unterabfrage nützlich ist, da es sehr schwierig ist, die darin enthaltenen Spalten eindeutig zu referenzieren.
Mit der neuen Implementierung verhalten sich Select.join() und Select.outerjoin() sehr ähnlich wie Query.join() und fügen der bestehenden Anweisung JOIN-Kriterien hinzu, indem sie mit der linken Entität abgeglichen werden.
stmt = select(user_table).join(
addresses_table, user_table.c.id == addresses_table.c.user_id
)produziert
SELECT user.id, user.name FROM user JOIN address ON user.id=address.user_idWie bei Join wird die ON-Klausel automatisch bestimmt, wenn dies möglich ist.
stmt = select(user_table).join(addresses_table)Wenn ORM-Entitäten in der Anweisung verwendet werden, ist dies im Wesentlichen, wie ORM-Abfragen im 2.0-Stil aufgerufen werden. ORM-Entitäten weisen der Anweisung intern einen "Plugin" zu, sodass ORM-bezogene Kompilierungsregeln angewendet werden, wenn die Anweisung in einen SQL-String kompiliert wird. Direkter kann die Methode Select.join() ORM-Beziehungen berücksichtigen, ohne die strikte Trennung zwischen Core- und ORM-Interna zu verletzen.
stmt = select(User).join(User.addresses)Eine weitere neue Methode Select.join_from() wird ebenfalls hinzugefügt, die eine einfachere Angabe der linken und rechten Seite eines Joins auf einmal ermöglicht.
stmt = select(Address.email_address, User.name).join_from(User, Address)produziert
SELECT address.email_address, user.name FROM user JOIN address ON user.id == address.user_idDas URL-Objekt ist nun unveränderlich¶
Das URL-Objekt wurde formalisiert, sodass es sich nun als namedtuple mit einer festen Anzahl von Feldern präsentiert, die unveränderlich sind. Darüber hinaus ist das durch das Attribut URL.query dargestellte Dictionary ebenfalls eine unveränderliche Zuordnung. Die Mutation des URL-Objekts war kein formal unterstützter oder dokumentierter Anwendungsfall, was zu einigen offenen Anwendungsfällen führte, die es sehr schwierig machten, fehlerhafte Verwendungen abzufangen, am häufigsten die Mutation des URL.query-Dictionarys, um Nicht-String-Elemente einzuschließen. Dies führte auch zu all den üblichen Problemen der Zulassung von Änderbarkeit bei einem grundlegenden Datenobjekt, nämlich unerwünschten Mutationen an anderer Stelle, die in Code eindringen, der nicht erwartete, dass sich die URL ändert. Schließlich ist das namedtuple-Design von Pythons urllib.parse.urlparse() inspiriert, das das geparste Objekt als benanntes Tupel zurückgibt.
Die Entscheidung, die API grundlegend zu ändern, basiert auf einer Kalkulation, die die Undurchführbarkeit eines Deprecation-Pfads (der das Ändern des URL.query-Wörterbuchs in ein spezielles Wörterbuch, das Deprecation-Warnungen ausgibt, wenn irgendeine Art von Standardbibliotheks-Mutationsmethoden aufgerufen wird, sowie dass das Wörterbuch, wenn es irgendeine Art von Liste von Elementen enthalten würde, auch bei Mutationen Deprecation-Warnungen ausgeben müsste) gegen den unwahrscheinlichen Anwendungsfall von Projekten abwägt, die URL-Objekte überhaupt erst mutieren, sowie dass kleine Änderungen wie die von #5341 ohnehin Rückwärtsinkompatibilität erzeugten. Der primäre Anwendungsfall für die Mutation eines URL-Objekts ist das Parsen von Plugin-Argumenten innerhalb des CreateEnginePlugin-Erweiterungspunkts, selbst eine ziemlich neue Ergänzung, die laut Github-Code-Suche in zwei Repositorien verwendet wird, von denen keines tatsächlich das URL-Objekt mutiert.
Das URL-Objekt bietet nun eine reichhaltige Schnittstelle zur Inspektion und Generierung neuer URL-Objekte. Der bestehende Mechanismus zur Erstellung eines URL-Objekts, die Funktion make_url(), bleibt unverändert.
>>> from sqlalchemy.engine import make_url
>>> url = make_url("postgresql+psycopg2://user:pass@host/dbname")Für die programmatische Konstruktion erhalten Code, der möglicherweise den URL-Konstruktor oder die Methode __init__ direkt verwendet hat, eine Deprecation-Warnung, wenn Argumente als Schlüsselwortargumente und nicht als exaktes 7-Tupel übergeben werden. Der Konstruktor im Schlüsselwortstil ist nun über die Methode URL.create() verfügbar.
>>> from sqlalchemy.engine import URL
>>> url = URL.create("postgresql", "user", "pass", host="host", database="dbname")
>>> str(url)
'postgresql://user:pass@host/dbname'Felder können typischerweise mit der Methode URL.set() geändert werden, die ein neues URL-Objekt mit angewendeten Änderungen zurückgibt.
>>> mysql_url = url.set(drivername="mysql+pymysql")
>>> str(mysql_url)
'mysql+pymysql://user:pass@host/dbname'Um den Inhalt des URL.query-Wörterbuchs zu ändern, können Methoden wie URL.update_query_dict() verwendet werden.
>>> url.update_query_dict({"sslcert": "/path/to/crt"})
postgresql://user:***@host/dbname?sslcert=%2Fpath%2Fto%2FcrtUm Code zu aktualisieren, der diese Felder direkt mutiert, ist ein **rückwärts- und vorwärtskompatibler Ansatz** die Verwendung von Duck-Typing, wie im folgenden Stil
def set_url_drivername(some_url, some_drivername):
# check for 1.4
if hasattr(some_url, "set"):
return some_url.set(drivername=some_drivername)
else:
# SQLAlchemy 1.3 or earlier, mutate in place
some_url.drivername = some_drivername
return some_url
def set_ssl_cert(some_url, ssl_cert):
# check for 1.4
if hasattr(some_url, "update_query_dict"):
return some_url.update_query_dict({"sslcert": ssl_cert})
else:
# SQLAlchemy 1.3 or earlier, mutate in place
some_url.query["sslcert"] = ssl_cert
return some_urlDer Query-String behält sein bestehendes Format als Wörterbuch von Strings zu Strings bei, wobei Sequenzen von Strings zur Darstellung mehrerer Parameter verwendet werden. Zum Beispiel
>>> from sqlalchemy.engine import make_url
>>> url = make_url(
... "postgresql://user:pass@host/dbname?alt_host=host1&alt_host=host2&sslcert=%2Fpath%2Fto%2Fcrt"
... )
>>> url.query
immutabledict({'alt_host': ('host1', 'host2'), 'sslcert': '/path/to/crt'})Um mit dem Inhalt des URL.query-Attributs zu arbeiten, sodass alle Werte in Sequenzen normalisiert werden, verwenden Sie das Attribut URL.normalized_query.
>>> url.normalized_query
immutabledict({'alt_host': ('host1', 'host2'), 'sslcert': ('/path/to/crt',)})Der Query-String kann über Methoden wie URL.update_query_dict(), URL.update_query_pairs(), URL.update_query_string() angehängt werden.
>>> url.update_query_dict({"alt_host": "host3"}, append=True)
postgresql://user:***@host/dbname?alt_host=host1&alt_host=host2&alt_host=host3&sslcert=%2Fpath%2Fto%2FcrtSiehe auch
Änderungen an CreateEnginePlugin¶
Die CreateEnginePlugin ist ebenfalls von dieser Änderung betroffen, da die Dokumentation für benutzerdefinierte Plugins angab, dass die Methode dict.pop() verwendet werden sollte, um konsumierte Argumente aus dem URL-Objekt zu entfernen. Dies sollte nun mit der Methode CreateEnginePlugin.update_url() erfolgen. Ein rückwärtskompatibler Ansatz würde so aussehen:
from sqlalchemy.engine import CreateEnginePlugin
class MyPlugin(CreateEnginePlugin):
def __init__(self, url, kwargs):
# check for 1.4 style
if hasattr(CreateEnginePlugin, "update_url"):
self.my_argument_one = url.query["my_argument_one"]
self.my_argument_two = url.query["my_argument_two"]
else:
# legacy
self.my_argument_one = url.query.pop("my_argument_one")
self.my_argument_two = url.query.pop("my_argument_two")
self.my_argument_three = kwargs.pop("my_argument_three", None)
def update_url(self, url):
# this method runs in 1.4 only and should be used to consume
# plugin-specific arguments
return url.difference_update_query(["my_argument_one", "my_argument_two"])Siehe den Docstring bei CreateEnginePlugin für vollständige Details zur Verwendung dieser Klasse.
select(), case() akzeptieren nun Positionsausdrücke¶
Wie an anderer Stelle in diesem Dokument zu sehen ist, akzeptiert der select()-Konstrukt nun „Column Clause“-Argumente positionell, anstatt sie als Liste übergeben zu müssen.
# new way, supports 2.0
stmt = select(table.c.col1, table.c.col2, ...)Wenn die Argumente positionell übergeben werden, sind keine weiteren Schlüsselwortargumente zulässig. In SQLAlchemy 2.0 wird der oben genannte Aufrufstil der einzige unterstützte sein.
Für die Dauer von 1.4 funktioniert der vorherige Aufrufstil weiterhin, bei dem die Liste der Spalten oder anderer Ausdrücke als Liste übergeben wird.
# old way, still works in 1.4
stmt = select([table.c.col1, table.c.col2, ...])Der obige Legacy-Aufrufstil akzeptiert auch die alten Schlüsselwortargumente, die seitdem aus den meisten narrativen Dokumentationen entfernt wurden. Die Existenz dieser Schlüsselwortargumente ist der Grund, warum die Spaltenklausel überhaupt als Liste übergeben wurde.
# very much the old way, but still works in 1.4
stmt = select([table.c.col1, table.c.col2, ...], whereclause=table.c.col1 == 5)Die Erkennung zwischen den beiden Stilen basiert darauf, ob das erste Positionsargument eine Liste ist. Leider gibt es wahrscheinlich immer noch einige Verwendungen, die wie folgt aussehen, wobei das Schlüsselwort für „whereclause“ ausgelassen wird.
# very much the old way, but still works in 1.4
stmt = select([table.c.col1, table.c.col2, ...], table.c.col1 == 5)Als Teil dieser Änderung erhält das Select-Konstrukt auch die 2.0-Stil-„Future“-API, die eine aktualisierte Methode Select.join() sowie Methoden wie Select.filter_by() und Select.join_from() umfasst.
In einer verwandten Änderung wurde das case()-Konstrukt ebenfalls so modifiziert, dass es seine Liste von WHEN-Klauseln positionell akzeptiert, mit einer ähnlichen Deprecation-Spur für den alten Aufrufstil.
stmt = select(users_table).where(
case(
(users_table.c.name == "wendy", "W"),
(users_table.c.name == "jack", "J"),
else_="E",
)
)Die Konvention für SQLAlchemy-Konstrukte, die *args oder eine Liste von Werten akzeptieren, wie im letzteren Fall für ein Konstrukt wie ColumnOperators.in_(), ist, dass **Positionsargumente für die strukturelle Spezifikation verwendet werden, Listen für die Datenspezifikation**.
Alle IN-Ausdrücke rendern Parameter für jeden Wert in der Liste on-the-fly (z. B. erweiterte Parameter)¶
Das „Expanding IN“-Feature, das erstmals in Spät erweiterte IN-Parametersätze ermöglichen IN-Ausdrücke mit gecachten Statements eingeführt wurde, ist so ausgereift, dass es der bisherigen Methode zum Rendern von IN-Ausdrücken deutlich überlegen ist. Da der Ansatz zur Handhabung leerer Listen von Werten verbessert wurde, ist er nun das einzige Mittel, das Core / ORM zur Darstellung von Listen von IN-Parametern verwenden wird.
Der frühere Ansatz, der seit der ersten Veröffentlichung in SQLAlchemy vorhanden war, bestand darin, dass, wenn eine Liste von Werten an die Methode ColumnOperators.in_() übergeben wurde, die Liste zur Zeit der Statement-Erstellung in eine Reihe einzelner BindParameter-Objekte erweitert wurde. Dies hatte die Einschränkung, dass es nicht möglich war, die Parameterliste zur Zeit der Statement-Ausführung basierend auf dem Parameterwörterbuch zu variieren, was bedeutete, dass String-SQL-Statements nicht unabhängig von ihren Parametern gecacht werden konnten, noch konnte das Parameterwörterbuch vollständig für Statements verwendet werden, die im Allgemeinen IN-Ausdrücke enthielten.
Um das in Baked Queries beschriebene „baked query“-Feature zu bedienen, war eine cachebare Version von IN erforderlich, was das „Expanding IN“-Feature hervorbrachte. Im Gegensatz zum bestehenden Verhalten, bei dem die Parameterliste zur Zeit der Statement-Erstellung in einzelne BindParameter-Objekte erweitert wird, verwendet das Feature stattdessen einen einzigen BindParameter, der die Liste der Werte auf einmal speichert; wenn das Statement vom Engine ausgeführt wird, wird es on-the-fly in einzelne gebundene Parameterpositionen „erweitert“, basierend auf den Parametern, die an den Aufruf von Connection.execute() übergeben werden, und der bestehende SQL-String, der möglicherweise aus einer früheren Ausführung abgerufen wurde, wird mithilfe eines regulären Ausdrucks angepasst, um zum aktuellen Parametersatz zu passen. Dies ermöglicht es, dass dasselbe Compiled-Objekt, das den gerenderten String-Statement speichert, mehrmals gegen verschiedene Parametersätze aufgerufen werden kann, die den Inhalt der an IN-Ausdrücke übergebenen Liste ändern, während das Verhalten einzelner Skalarparameter, die an den DBAPI übergeben werden, beibehalten wird. Obwohl einige DBAPIs diese Funktionalität direkt unterstützen, ist sie nicht allgemein verfügbar; das „Expanding IN“-Feature unterstützt nun das Verhalten konsistent für alle Backends.
Da ein Hauptaugenmerk von 1.4 darin besteht, echte Statement-Caches in Core und ORM ohne die Unannehmlichkeiten des „baked“-Systems zu ermöglichen, und da das „Expanding IN“-Feature ohnehin einen einfacheren Ansatz zur Erstellung von Ausdrücken darstellt, wird es nun automatisch aufgerufen, wenn eine Liste von Werten an einen IN-Ausdruck übergeben wird.
stmt = select(A.id, A.data).where(A.id.in_([1, 2, 3]))Die String-Repräsentation vor der Ausführung ist
>>> print(stmt)
SELECT a.id, a.data
FROM a
WHERE a.id IN ([POSTCOMPILE_id_1])
Um die Werte direkt zu rendern, verwenden Sie literal_binds, wie es zuvor der Fall war.
>>> print(stmt.compile(compile_kwargs={"literal_binds": True}))
SELECT a.id, a.data
FROM a
WHERE a.id IN (1, 2, 3)
Ein neues Flag „render_postcompile“ wird als Hilfsmittel hinzugefügt, um den aktuellen gebundenen Wert so zu rendern, wie er an die Datenbank übergeben würde.
>>> print(stmt.compile(compile_kwargs={"render_postcompile": True}))
SELECT a.id, a.data
FROM a
WHERE a.id IN (:id_1_1, :id_1_2, :id_1_3)
Die Engine-Protokollausgabe zeigt auch das endgültig gerenderte Statement.
INFO sqlalchemy.engine.base.Engine SELECT a.id, a.data
FROM a
WHERE a.id IN (?, ?, ?)
INFO sqlalchemy.engine.base.Engine (1, 2, 3)Als Teil dieser Änderung wird das Verhalten von „leeren IN“-Ausdrücken, bei denen der Listenparameter leer ist, nun auf die Verwendung des IN-Operators gegen einen sogenannten „leeren Satz“ standardisiert. Da es keine standardmäßige SQL-Syntax für leere Mengen gibt, wird eine SELECT-Anweisung verwendet, die keine Zeilen zurückgibt und für jede Backend-Art spezifisch zugeschnitten ist, so dass die Datenbank sie als leeren Satz behandelt; dieses Feature wurde erstmals in Version 1.3 eingeführt und ist unter Das Expanding IN Feature unterstützt jetzt leere Listen beschrieben. Der Parameter create_engine.empty_in_strategy, der in Version 1.2 als Mittel zur Migration der Behandlung dieses Falls für das frühere IN-System eingeführt wurde, ist nun veraltet und diese Flagge hat keine Auswirkung mehr; wie in Das Verhalten von leeren Sammlungen des IN / NOT IN Operators ist nun konfigurierbar; Standardausdruck vereinfacht beschrieben, erlaubte diese Flagge einem Dialekt, zwischen dem ursprünglichen System des Vergleichs einer Spalte mit sich selbst, das sich als riesiges Performance-Problem erwies, und einem neueren System des Vergleichs von „1 != 1“, um einen „falschen“ Ausdruck zu erzeugen, zu wechseln. Die in 1.3 eingeführte Verhaltensweise, die nun in allen Fällen stattfindet, ist korrekter als beide Ansätze, da der IN-Operator weiterhin verwendet wird und nicht das Performance-Problem des ursprünglichen Systems aufweist.
Darüber hinaus wurde das „erweiterte“ Parametersystem verallgemeinert, so dass es auch andere dialektspezifische Anwendungsfälle bedient, bei denen ein Parameter vom DBAPI oder der zugrunde liegenden Datenbank nicht untergebracht werden kann; siehe Neue „Post Compile“-gebundene Parameter für LIMIT/OFFSET in Oracle, SQL Server für Details.
Integrierte FROM-Linting-Funktion warnt vor möglichen kartesischen Produkten in einer SELECT-Anweisung¶
Da die Core-Ausdruckssprache sowie die ORM auf einem Modell der „impliziten FROMs“ basieren, bei dem eine bestimmte FROM-Klausel automatisch hinzugefügt wird, wenn irgendein Teil der Abfrage darauf verweist, ist ein häufiges Problem die Situation, in der eine SELECT-Anweisung, sei es eine Top-Level-Anweisung oder eine eingebettete Subquery, FROM-Elemente enthält, die nicht mit den restlichen FROM-Elementen in der Abfrage verbunden sind, was zu einem sogenannten „kartesischen Produkt“ im Ergebnis-Set führt, d. h. jede mögliche Kombination von Zeilen aus jedem nicht anderweitig verbundenen FROM-Element. In relationalen Datenbanken ist dies fast immer ein unerwünschtes Ergebnis, da es ein riesiges Ergebnis-Set voller duplizierter, unkorrelierter Daten erzeugt.
SQLAlchemy ist mit all seinen großartigen Funktionen besonders anfällig für diese Art von Problemen, da einer SELECT-Anweisung automatisch FROM-Elemente aus jeder Tabelle hinzugefügt werden, die in den anderen Klauseln vorkommt. Ein typisches Szenario sieht wie folgt aus, wobei zwei Tabellen GEJOINt werden, jedoch ein zusätzlicher Eintrag in der WHERE-Klausel, der vielleicht unbeabsichtigt nicht mit diesen beiden Tabellen übereinstimmt, einen zusätzlichen FROM-Eintrag erstellt.
address_alias = aliased(Address)
q = (
session.query(User)
.join(address_alias, User.addresses)
.filter(Address.email_address == "foo")
)Die obige Abfrage wählt aus einem JOIN von User und address_alias, letzteres eine Alias-Entität von Address. Allerdings wird die Address-Entität direkt in der WHERE-Klausel verwendet, so dass das obige zu folgendem SQL führen würde:
SELECT
users.id AS users_id, users.name AS users_name,
users.fullname AS users_fullname,
users.nickname AS users_nickname
FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
WHERE addresses.email_address = :email_address_1Im obigen SQL sehen wir, was SQLAlchemy-Entwickler als „das gefürchtete Komma“ bezeichnen, da wir „FROM addresses, users JOIN addresses“ in der FROM-Klausel sehen, was das klassische Zeichen eines kartesischen Produkts ist; wobei eine Abfrage JOIN verwendet, um FROM-Klauseln miteinander zu verbinden, aber da eine davon nicht verbunden ist, verwendet sie ein Komma. Die obige Abfrage gibt einen vollständigen Satz von Zeilen zurück, der die Tabellen „user“ und „addresses“ über die Spalte „id / user_id“ verbindet, und wendet dann alle diese Zeilen in einem kartesischen Produkt gegen jede Zeile in der Tabelle „addresses“ direkt an. Das heißt, wenn es zehn Benutzerzeilen und 100 Zeilen in Adressen gibt, gibt die obige Abfrage ihre erwarteten Ergebniszeilen zurück, wahrscheinlich 100, da alle Adresszeilen ausgewählt würden, multipliziert mit 100, so dass die Gesamtgröße des Ergebnisses 10000 Zeilen beträgt.
Das Muster „table1, table2 JOIN table3“ tritt auch häufig in der SQLAlchemy ORM auf, entweder durch subtile Fehlverwendung von ORM-Features, insbesondere solcher, die sich auf Joined Eager Loading oder Joined Table Inheritance beziehen, oder als Ergebnis von SQLAlchemy ORM-Fehlern in denselben Systemen. Ähnliche Probleme gelten für SELECT-Anweisungen, die „implizite Joins“ verwenden, bei denen das JOIN-Schlüsselwort nicht verwendet wird und stattdessen jedes FROM-Element über die WHERE-Klausel mit einem anderen verbunden wird.
Seit einigen Jahren gibt es ein Rezept auf der Wiki, das einen Graphenalgorithmus auf ein select()-Konstrukt zur Abfragezeit anwendet und die Struktur der Abfrage auf diese nicht verknüpften FROM-Klauseln untersucht, die WHERE-Klausel und alle JOIN-Klauseln durchgeht, um zu bestimmen, wie FROM-Elemente miteinander verbunden sind, und sicherstellt, dass alle FROM-Elemente in einem einzigen Graphen verbunden sind. Dieses Rezept wurde nun in den SQLCompiler selbst integriert, wo es optional eine Warnung für eine Anweisung ausgibt, wenn dieser Zustand erkannt wird. Die Warnung wird mit dem Flag create_engine.enable_from_linting aktiviert und ist standardmäßig aktiviert. Der Rechenaufwand des Linters ist sehr gering, und er tritt zudem nur während der Statement-Kompilierung auf, was bedeutet, dass er bei einem gecachten SQL-Statement nur einmal auftritt.
Unter Verwendung dieser Funktion gibt unsere obige ORM-Abfrage eine Warnung aus.
>>> q.all()
SAWarning: SELECT statement has a cartesian product between FROM
element(s) "addresses_1", "users" and FROM element "addresses".
Apply join condition(s) between each element to resolve.Das Linter-Feature berücksichtigt nicht nur Tabellen, die über JOIN-Klauseln verbunden sind, sondern auch über die WHERE-Klausel. Oben können wir eine WHERE-Klausel hinzufügen, um die neue Address-Entität mit der vorherigen address_alias-Entität zu verbinden, und das entfernt die Warnung.
q = (
session.query(User)
.join(address_alias, User.addresses)
.filter(Address.email_address == "foo")
.filter(Address.id == address_alias.id)
) # resolve cartesian products,
# will no longer warnDie kartesische Produktwarnung betrachtet **jede** Art von Verbindung zwischen zwei FROM-Klauseln als Auflösung, auch wenn das Endergebnis immer noch verschwenderisch ist, da der Linter nur dazu dient, den häufigen Fall einer vollständig unerwarteten FROM-Klausel zu erkennen. Wenn die FROM-Klausel an anderer Stelle explizit referenziert und mit den anderen FROMs verbunden ist, wird keine Warnung ausgegeben.
q = (
session.query(User)
.join(address_alias, User.addresses)
.filter(Address.email_address == "foo")
.filter(Address.id > address_alias.id)
) # will generate a lot of rows,
# but no warningVollständige kartesische Produkte sind ebenfalls zulässig, wenn sie explizit angegeben werden; wenn wir beispielsweise das kartesische Produkt von User und Address wünschen, können wir auf true() JOINen, damit jede Zeile mit jeder anderen übereinstimmt; die folgende Abfrage gibt alle Zeilen zurück und erzeugt keine Warnungen.
from sqlalchemy import true
# intentional cartesian product
q = session.query(User).join(Address, true()) # intentional cartesian productDie Warnung wird standardmäßig nur generiert, wenn das Statement vom Connection zur Ausführung kompiliert wird; das Aufrufen der Methode ClauseElement.compile() gibt keine Warnung aus, es sei denn, das Linting-Flag wird übergeben.
>>> from sqlalchemy.sql import FROM_LINTING
>>> print(q.statement.compile(linting=FROM_LINTING))
SAWarning: SELECT statement has a cartesian product between FROM element(s) "addresses" and FROM element "users". Apply join condition(s) between each element to resolve.
SELECT users.id, users.name, users.fullname, users.nickname
FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
WHERE addresses.email_address = :email_address_1
Neues Ergebnisobjekt¶
Ein Hauptziel von SQLAlchemy 2.0 ist die Vereinheitlichung der Behandlung von „Ergebnissen“ zwischen ORM und Core. Zu diesem Zweck führt Version 1.4 neue Versionen sowohl des ResultProxy als auch des RowProxy Objekts ein, die seit Beginn von SQLAlchemy vorhanden sind.
Die neuen Objekte sind unter Result und Row dokumentiert und werden nicht nur für Core-Ergebnis-Sets, sondern auch für 2.0-Stil-Ergebnisse innerhalb der ORM verwendet.
Dieses Ergebnisobjekt ist vollständig mit ResultProxy kompatibel und enthält viele neue Funktionen, die nun sowohl auf Core- als auch auf ORM-Ergebnisse gleichermaßen angewendet werden, einschließlich Methoden wie
Result.one() - gibt genau eine Zeile zurück oder löst eine Ausnahme aus.
with engine.connect() as conn:
row = conn.execute(table.select().where(table.c.id == 5)).one()Result.one_or_none() - dasselbe, gibt aber auch None für keine Zeilen zurück.
Result.all() - gibt alle Zeilen zurück.
Result.partitions() - holt Zeilen in Chunks.
with engine.connect() as conn:
result = conn.execute(
table.select().order_by(table.c.id),
execution_options={"stream_results": True},
)
for chunk in result.partitions(500):
# process up to 500 records
...Result.columns() - ermöglicht das Slicing und Reorganisieren von Zeilen.
with engine.connect() as conn:
# requests x, y, z
result = conn.execute(select(table.c.x, table.c.y, table.c.z))
# iterate rows as y, x
for y, x in result.columns("y", "x"):
print("Y: %s X: %s" % (y, x))Result.scalars() - gibt Listen von Skalarobjekten zurück, standardmäßig aus der ersten Spalte, kann aber auch ausgewählt werden.
result = session.execute(select(User).order_by(User.id))
for user_obj in result.scalars():
...Result.mappings() - gibt anstelle von benannten Tupelzeilen Wörterbücher zurück.
with engine.connect() as conn:
result = conn.execute(select(table.c.x, table.c.y, table.c.z))
for map_ in result.mappings():
print("Y: %(y)s X: %(x)s" % map_)Bei der Verwendung von Core ist das von Connection.execute() zurückgegebene Objekt eine Instanz von CursorResult, die weiterhin dieselben API-Funktionen wie ResultProxy in Bezug auf eingefügte Primärschlüssel, Standardwerte, Zeilenanzahlen usw. bietet. Für ORM wird eine Result-Unterklasse zurückgegeben, die die Übersetzung von Core-Zeilen in ORM-Zeilen durchführt und dann alle gleichen Operationen ermöglicht.
Siehe auch
ORM Query vereinheitlicht mit Core Select - in der 2.0 Migrationsdokumentation.
RowProxy ist kein „Proxy“ mehr; es heißt jetzt Row und verhält sich wie ein erweitertes benanntes Tupel¶
Die Klasse RowProxy, die einzelne Datenbankergebniszeilen in einem Core-Ergebnis-Set darstellt, heißt nun Row und ist kein „Proxy“-Objekt mehr; das bedeutet, dass die Zeile, wenn das Row-Objekt zurückgegeben wird, ein einfaches Tupel ist, das die Daten in ihrer endgültigen Form enthält, nachdem sie bereits von den Ergebnis-Zeilenverarbeitungsfunktionen, die mit Datentypen verbunden sind, verarbeitet wurden (Beispiele hierfür sind die Umwandlung eines Datumsstrings aus der Datenbank in ein datetime-Objekt, eines JSON-Strings in ein Python json.loads()-Ergebnis usw.).
Der unmittelbare Grund dafür ist, dass die Zeile sich eher wie ein Python-benanntes Tupel als wie eine Zuordnung verhalten kann, wobei die Werte im Tupel Gegenstand des __contains__-Operators auf dem Tupel sind, anstatt der Schlüssel. Da Row sich wie ein benanntes Tupel verhält, ist es dann geeignet, als Ersatz für das KeyedTuple-Objekt der ORM verwendet zu werden, was zu einer zukünftigen API führt, bei der sowohl die ORM als auch Core Ergebnis-Sets liefern, die sich identisch verhalten. Die Vereinheitlichung wichtiger Muster innerhalb von ORM und Core ist ein Hauptziel von SQLAlchemy 2.0, und Version 1.4 zielt darauf ab, die meisten oder alle zugrunde liegenden Architekturmuster bereitzustellen, um diesen Prozess zu unterstützen. Der Hinweis in Das von Query zurückgegebene „KeyedTuple“-Objekt wird durch Row ersetzt beschreibt die Verwendung des Row-Klasse durch die ORM.
Für die Version 1.4 bietet die Row-Klasse eine zusätzliche Unterklasse LegacyRow, die von Core verwendet wird und eine rückwärtskompatible Version von RowProxy darstellt, während sie Deprecation-Warnungen für die API-Funktionen und Verhaltensweisen ausgibt, die verschoben werden. ORM Query verwendet nun direkt Row als Ersatz für KeyedTuple.
Die Klasse LegacyRow ist eine Übergangsklasse, bei der die Methode __contains__ immer noch gegen die Schlüssel und nicht gegen die Werte testet, während sie eine Deprecation-Warnung ausgibt, wenn die Operation erfolgreich ist. Darüber hinaus sind alle anderen Mapping-ähnlichen Methoden des vorherigen RowProxy veraltet, einschließlich LegacyRow.keys(), LegacyRow.items() usw. Für Mapping-ähnliche Verhaltensweisen eines Row-Objekts, einschließlich der Unterstützung für diese Methoden sowie eines Schlüssel-orientierten __contains__-Operators, ist die API zukünftig die erste Anlaufstelle für ein spezielles Attribut Row._mapping, das dann eine vollständige Mapping-Schnittstelle zum Zeilenobjekt bietet, anstatt einer Tupel-Schnittstelle.
Begründung: Verhalten als benanntes Tupel statt als Zuordnung¶
Der Unterschied zwischen einem benannten Tupel und einer Zuordnung in Bezug auf boolesche Operatoren lässt sich zusammenfassen. Angenommen, ein „benanntes Tupel“ in Pseudocode lautet
row = (id: 5, name: 'some name')Der größte inkompatible Unterschied ist das Verhalten von __contains__.
"id" in row # True for a mapping, False for a named tuple
"some name" in row # False for a mapping, True for a named tupleIn 1.4, wenn ein LegacyRow von einem Core-Ergebnis-Set zurückgegeben wird, wird der obige Vergleich "id" in row weiterhin erfolgreich sein, aber eine Deprecation-Warnung wird ausgegeben. Um den „in“-Operator als Zuordnung zu verwenden, verwenden Sie das Attribut Row._mapping.
"id" in row._mappingDas SQLAlchemy 2.0-Ergebnisobjekt wird über einen Modifier namens .mappings() verfügen, sodass diese Mappings direkt empfangen werden können.
# using sqlalchemy.future package
for row in result.mappings():
row["id"]Das Proxying-Verhalten entfällt, war auch im modernen Gebrauch unnötig.¶
Die Überarbeitung von Row, um sich wie ein Tupel zu verhalten, erfordert, dass alle Datenwerte sofort vollständig verfügbar sind. Dies ist eine interne Verhaltensänderung gegenüber RowProxy, bei dem Ergebniszeilen-Verarbeitungsfunktionen zum Zeitpunkt des Zugriffs auf ein Element der Zeile aufgerufen wurden, anstatt wenn die Zeile zum ersten Mal abgerufen wurde. Das bedeutet zum Beispiel beim Abrufen eines Datums-/Zeitwerts aus SQLite, dass die Daten für die Zeile, wie sie im RowProxy-Objekt vorhanden waren, zuvor so ausgesehen hätten:
row_proxy = (1, "2019-12-31 19:56:58.272106")und dann beim Zugriff über __getitem__ die Funktion datetime.strptime() zur Laufzeit verwendet wurde, um das obige String-Datum in ein datetime-Objekt zu konvertieren. Mit der neuen Architektur ist das datetime()-Objekt im Tupel vorhanden, wenn es zurückgegeben wird, und die Funktion datetime.strptime() wurde nur einmal im Voraus aufgerufen.
row = (1, datetime.datetime(2019, 12, 31, 19, 56, 58, 272106))Die Objekte RowProxy und Row in SQLAlchemy sind die Orte, an denen der Großteil des C-Erweiterungscodes von SQLAlchemy stattfindet. Dieser Code wurde stark überarbeitet, um das neue Verhalten effizient bereitzustellen, und die Gesamtleistung wurde verbessert, da das Design von Row nun erheblich einfacher ist.
Die Begründung für das vorherige Verhalten ging von einem Nutzungsmodell aus, bei dem eine Ergebniszeile Dutzende oder Hunderte von Spalten enthalten konnte, von denen die meisten nicht abgerufen wurden und für die die Mehrheit dieser Spalten eine Ergebniswert-Verarbeitungsfunktion benötigte. Durch den Aufruf der Verarbeitungsfunktion nur bei Bedarf war das Ziel, dass viele Ergebnisverarbeitungsfunktionen nicht notwendig wären und somit die Leistung gesteigert würde.
Es gibt viele Gründe, warum die obigen Annahmen nicht zutreffen.
Die überwiegende Mehrheit der aufgerufenen Zeilenverarbeitungsfunktionen diente dazu, einen Bytestring unter Python 2 in einen Python-Unicode-String zu dekodieren. Dies war richtig, als Unicode in Python zu nutzen begann und bevor Python 3 existierte. Mit der Einführung von Python 3 nahmen innerhalb weniger Jahre alle Python-DBAPIs die richtige Rolle der Unterstützung der direkten Bereitstellung von Python-Unicode-Objekten sowohl unter Python 2 (als Option) als auch unter Python 3 (als einzigem Weg) an. Schließlich wurde es in den meisten Fällen auch für Python 2 zum Standard. Die Python 2-Unterstützung von SQLAlchemy ermöglicht weiterhin die explizite String-zu-Unicode-Konvertierung für einige DBAPIs wie cx_Oracle, jedoch wird diese nun auf der DBAPI-Ebene durchgeführt und nicht mehr als standardmäßige SQLAlchemy-Ergebniszeilen-Verarbeitungsfunktion.
Die obige String-Konvertierung wurde, wenn sie verwendet wird, durch die C-Erweiterungen extrem performant gemacht, so sehr, dass selbst in 1.4 der Byte-zu-Unicode-Codec-Hook von SQLAlchemy bei cx_Oracle eingehängt ist, wo er als performanter als der eigene Hook von cx_Oracle beobachtet wurde. Das bedeutete, dass der Overhead für die Konvertierung aller Strings in einer Zeile ohnehin nicht so signifikant war, wie er ursprünglich war.
Zeilenverarbeitungsfunktionen werden in den meisten anderen Fällen nicht verwendet; die Ausnahmen sind die Datums-/Zeitunterstützung von SQLite, die JSON-Unterstützung für einige Backends, einige numerische Handler wie String zu
Decimal. Im Fall vonDecimalhat Python 3 auch die hochperformantecdecimal-Implementierung standardisiert, was bei Python 2 nicht der Fall ist, das weiterhin die weitaus weniger performante reine Python-Version verwendet.Das Abrufen vollständiger Zeilen, bei denen nur wenige Spalten benötigt werden, ist in realen Anwendungsfällen nicht üblich. In den frühen Tagen von SQLAlchemy war der Datenbankcode aus anderen Sprachen der Form „row = fetch(‘SELECT * FROM table’)“ üblich; die Verwendung der Ausdruckssprache von SQLAlchemy führt jedoch dazu, dass beobachteter Code typischerweise die spezifisch benötigten Spalten verwendet.
SELECT-Objekte und abgeleitete FROM-Klauseln erlauben doppelte Spalten und Spaltenbezeichnungen.¶
Diese Änderung ermöglicht es der select()-Konstruktion, nun doppelte Spaltenbezeichnungen sowie doppelte Spaltenobjekte selbst zuzulassen, sodass Ergebnis-Tupel auf die gleiche Weise organisiert und geordnet werden, wie die Spalten ausgewählt wurden. Die ORM Query funktioniert bereits so, daher ermöglicht diese Änderung eine größere Kreuzkompatibilität zwischen den beiden, was ein Kernziel des 2.0-Übergangs ist.
>>> from sqlalchemy import column, select
>>> c1, c2, c3, c4 = column("c1"), column("c2"), column("c3"), column("c4")
>>> stmt = select(c1, c2, c3.label("c2"), c2, c4)
>>> print(stmt)
SELECT c1, c2, c3 AS c2, c2, c4
Zur Unterstützung dieser Änderung unterstützt die ColumnCollection, die von SelectBase verwendet wird, sowie für abgeleitete FROM-Klauseln wie Unterabfragen, ebenfalls doppelte Spalten; dies schließt das neue Attribut SelectBase.selected_columns, das veraltete Attribut SelectBase.c sowie das Attribut FromClause.c ein, das auf Konstrukten wie Subquery und Alias zu finden ist.
>>> list(stmt.selected_columns)
[
<sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcca20; c1>,
<sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
<sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>,
<sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
<sqlalchemy.sql.elements.ColumnClause at 0x7fa540897048; c4>
]
>>> print(stmt.subquery().select())
SELECT anon_1.c1, anon_1.c2, anon_1.c2, anon_1.c2, anon_1.c4
FROM (SELECT c1, c2, c3 AS c2, c2, c4) AS anon_1
ColumnCollection ermöglicht auch den Zugriff über den ganzzahligen Index, um Fälle zu unterstützen, in denen der String-„Schlüssel“ mehrdeutig ist.
>>> stmt.selected_columns[2]
<sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>Um der Verwendung von ColumnCollection in Objekten wie Table und PrimaryKeyConstraint Rechnung zu tragen, wird das alte „deduplizierende“ Verhalten, das für diese Objekte kritischer ist, in einer neuen Klasse DedupeColumnCollection beibehalten.
Die Änderung beinhaltet, dass die vertraute Warnung "Column %r on table %r being replaced by %r, which has the same key. Consider use_labels for select() statements." **entfernt** wird; die Methode Select.apply_labels() ist weiterhin verfügbar und wird vom ORM für alle SELECT-Operationen verwendet, impliziert jedoch keine Deduplizierung von Spaltenobjekten, obwohl sie die Deduplizierung von implizit generierten Bezeichnungen impliziert.
>>> from sqlalchemy import table
>>> user = table("user", column("id"), column("name"))
>>> stmt = select(user.c.id, user.c.name, user.c.id).apply_labels()
>>> print(stmt)
SELECT "user".id AS user_id, "user".name AS user_name, "user".id AS id_1
FROM "user"Schließlich erleichtert die Änderung die Erstellung von UNION und anderen _selectable.CompoundSelect-Objekten, indem sie sicherstellt, dass die Anzahl und Position der Spalten in einer SELECT-Anweisung das widerspiegelt, was angegeben wurde, in einem Anwendungsfall wie
>>> s1 = select(user, user.c.id)
>>> s2 = select(c1, c2, c3)
>>> from sqlalchemy import union
>>> u = union(s1, s2)
>>> print(u)
SELECT "user".id, "user".name, "user".id
FROM "user" UNION SELECT c1, c2, c3
Verbesserte Spaltenbezeichnung für einfache Spaltenausdrücke mit CAST oder Ähnlichem.¶
Ein Benutzer wies darauf hin, dass die PostgreSQL-Datenbank beim Verwenden von Funktionen wie CAST auf eine benannte Spalte ein praktisches Verhalten hat, nämlich dass der Ergebnisspaltenname derselbe ist wie der innere Ausdruck.
test=> SELECT CAST(data AS VARCHAR) FROM foo;
data
------
5
(1 row)Dies ermöglicht es, CAST auf Tabellenspalten anzuwenden, ohne den Spaltennamen (oben mit dem Namen "data") in der Ergebniszeile zu verlieren. Vergleichen Sie dies mit Datenbanken wie MySQL/MariaDB sowie den meisten anderen, bei denen der Spaltenname aus dem vollständigen SQL-Ausdruck stammt und nicht sehr portabel ist.
MariaDB [test]> SELECT CAST(data AS CHAR) FROM foo;
+--------------------+
| CAST(data AS CHAR) |
+--------------------+
| 5 |
+--------------------+
1 row in set (0.003 sec)In SQLAlchemy Core-Ausdrücken arbeiten wir nie mit einem rohen generierten Namen wie dem oben genannten, da SQLAlchemy automatische Bezeichnungen für solche Ausdrücke anwendet, die bis jetzt immer sogenannte „anonyme“ Ausdrücke waren.
>>> print(select(cast(foo.c.data, String)))
SELECT CAST(foo.data AS VARCHAR) AS anon_1 # old behavior
FROM foo
Diese anonymen Ausdrücke waren notwendig, da SQLAlchemy’s ResultProxy stark auf Ergebnisspaltennamen angewiesen war, um Datentypen abzugleichen, wie z. B. den Datentyp String, der früher ein Ergebniszeilen-Verarbeitungsverhalten hatte, der korrekten Spalte zugeordnet wird, sodass die Namen vor allem auf eine datenbankunabhängige Weise leicht zu ermitteln und in allen Fällen eindeutig sein mussten. In SQLAlchemy 1.0 wurde im Rahmen von #918 diese Abhängigkeit von benannten Spalten in Ergebniszeilen (insbesondere dem cursor.description-Element des PEP-249-Cursors) so weit reduziert, dass sie für die meisten Core-SELECT-Konstrukte nicht mehr erforderlich war. In der Version 1.4 wird das System insgesamt besser mit SELECT-Anweisungen umgehen, die doppelte Spalten- oder Bezeichnungsnamen haben, wie in SELECT-Objekte und abgeleitete FROM-Klauseln erlauben doppelte Spalten und Spaltenbezeichnungen. Wir emulieren nun das sinnvolle Verhalten von PostgreSQL für einfache Modifikationen an einer einzelnen Spalte, insbesondere mit CAST.
>>> print(select(cast(foo.c.data, String)))
SELECT CAST(foo.data AS VARCHAR) AS data
FROM foo
Für CAST auf Ausdrücke, die keinen Namen haben, wird die vorherige Logik verwendet, um die üblichen „anonymen“ Bezeichnungen zu generieren.
>>> print(select(cast("hi there," + foo.c.data, String)))
SELECT CAST(:data_1 + foo.data AS VARCHAR) AS anon_1
FROM foo
Ein cast() auf ein Label, obwohl die Bezeichnungs-Ausdrücke weggelassen werden müssen, da diese nicht innerhalb eines CAST gerendert werden, wird dennoch den gegebenen Namen verwenden.
>>> print(select(cast(("hi there," + foo.c.data).label("hello_data"), String)))
SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
FROM foo
Und natürlich kann, wie immer, ein Label außerhalb des Ausdrucks angewendet werden, um direkt eine „AS <name>“-Bezeichnung anzuwenden.
>>> print(select(cast(("hi there," + foo.c.data), String).label("hello_data")))
SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
FROM foo
Neue „Post-Compile“-gebundene Parameter für LIMIT/OFFSET in Oracle, SQL Server.¶
Ein Hauptziel der Serie 1.4 ist die Etablierung, dass alle Core SQL-Konstrukte vollständig cachebar sind, was bedeutet, dass eine bestimmte Compiled-Struktur unabhängig von verwendeten SQL-Parametern eine identische SQL-Zeichenkette erzeugt, was insbesondere die zur Angabe von LIMIT- und OFFSET-Werten verwendeten Parameter einschließt, die typischerweise für Paginierung und „Top N“-Ergebnisse verwendet werden.
Obwohl SQLAlchemy seit vielen Jahren gebundene Parameter für LIMIT/OFFSET-Schemata verwendet, blieben einige Ausnahmen bestehen, bei denen solche Parameter nicht erlaubt waren, einschließlich einer SQL Server „TOP N“-Anweisung wie
SELECT TOP 5 mytable.id, mytable.data FROM mytablesowie bei Oracle, wo der FIRST_ROWS()-Hinweis (den SQLAlchemy verwendet, wenn der Parameter optimize_limits=True an create_engine() mit einer Oracle-URL übergeben wird) diese nicht zulässt, aber auch, dass die Verwendung von gebundenen Parametern mit ROWNUM-Vergleichen zu langsameren Abfrageplänen geführt hat.
SELECT anon_1.id, anon_1.data FROM (
SELECT /*+ FIRST_ROWS(5) */
anon_2.id AS id,
anon_2.data AS data,
ROWNUM AS ora_rn FROM (
SELECT mytable.id, mytable.data FROM mytable
) anon_2
WHERE ROWNUM <= :param_1
) anon_1 WHERE ora_rn > :param_2Um alle Anweisungen auf Kompilierungsebene bedingungslos cachebar zu machen, wurde eine neue Form gebundener Parameter namens „Post-Compile“-Parameter hinzugefügt, die denselben Mechanismus wie „Expanding IN Parameters“ verwendet. Dies ist ein bindparam(), das sich identisch zu jedem anderen gebundenen Parameter verhält, außer dass der Parameterwert buchstäblich in die SQL-Zeichenkette gerendert wird, bevor er an die cursor.execute()-Methode des DBAPI gesendet wird. Der neue Parameter wird intern von den SQL Server- und Oracle-Dialekten verwendet, sodass die Treiber den buchstäblich gerenderten Wert erhalten, aber der Rest von SQLAlchemy ihn weiterhin als gebundenen Parameter betrachten kann. Die oben genannten beiden Anweisungen sehen nun, wenn sie mit str(statement.compile(dialect=<dialect>)) stringifiziert werden, so aus:
SELECT TOP [POSTCOMPILE_param_1] mytable.id, mytable.data FROM mytableund
SELECT anon_1.id, anon_1.data FROM (
SELECT /*+ FIRST_ROWS([POSTCOMPILE__ora_frow_1]) */
anon_2.id AS id,
anon_2.data AS data,
ROWNUM AS ora_rn FROM (
SELECT mytable.id, mytable.data FROM mytable
) anon_2
WHERE ROWNUM <= [POSTCOMPILE_param_1]
) anon_1 WHERE ora_rn > [POSTCOMPILE_param_2]Das Format [POSTCOMPILE_<param>] ist auch das, was gesehen wird, wenn ein „expanding IN“ verwendet wird.
Beim Betrachten der SQL-Protokollausgabe wird die endgültige Form der Anweisung angezeigt.
SELECT anon_1.id, anon_1.data FROM (
SELECT /*+ FIRST_ROWS(5) */
anon_2.id AS id,
anon_2.data AS data,
ROWNUM AS ora_rn FROM (
SELECT mytable.id AS id, mytable.data AS data FROM mytable
) anon_2
WHERE ROWNUM <= 8
) anon_1 WHERE ora_rn > 3Die Funktion „Post-Compile-Parameter“ ist als öffentliche API über den Parameter bindparam.literal_execute verfügbar, ist jedoch derzeit nicht für den allgemeinen Gebrauch bestimmt. Die Literalwerte werden mit dem TypeEngine.literal_processor() des zugrunde liegenden Datentyps gerendert, was in SQLAlchemy einen **extrem begrenzten** Umfang hat und nur Ganzzahlen und einfache Zeichenfolgenwerte unterstützt.
Verbindungsebene-Transaktionen können nun basierend auf Untertransaktionen inaktiv sein.¶
Eine Connection enthält nun das Verhalten, dass eine Transaction aufgrund eines Rollbacks einer inneren Transaktion inaktiv werden kann, jedoch wird die Transaction erst bereinigt, wenn sie selbst zurückgerollt wurde.
Dies ist im Wesentlichen eine neue Fehlerbedingung, die die Ausführung von Anweisungen auf einer Connection verhindert, wenn eine innere „Unter“-Transaktion zurückgerollt wurde. Das Verhalten ähnelt dem einer ORM Session, wo, wenn eine äußere Transaktion begonnen wurde, diese zurückgerollt werden muss, um die ungültige Transaktion zu bereinigen. Dieses Verhalten wird unter „This Session’s transaction has been rolled back due to a previous exception during flush.“ (oder ähnlich) beschrieben.
Während die Connection ein weniger strenges Verhaltensmuster als die Session hatte, wurde diese Änderung vorgenommen, da sie dazu beiträgt, zu erkennen, wann eine Untertransaktion die DBAPI-Transaktion zurückgerollt hat, die externe Code jedoch nicht darauf aufmerksam ist und versucht, fortzufahren, was tatsächlich Operationen auf einer neuen Transaktion ausführt. Das unter Joining a Session into an External Transaction (such as for test suites) beschriebene „Test-Harness“-Muster ist der häufigste Ort dafür.
Die „Subtransaction“-Funktion von Core und ORM ist selbst veraltet und wird in Version 2.0 nicht mehr vorhanden sein. Daher ist diese neue Fehlerbedingung selbst temporär, da sie nicht mehr gilt, sobald Untertransaktionen entfernt wurden.
Um mit dem 2.0-Stil-Verhalten zu arbeiten, das keine Untertransaktionen enthält, verwenden Sie den Parameter create_engine.future auf create_engine().
Die Fehlermeldung wird auf der Fehlerseite unter This connection is on an inactive transaction. Please rollback() fully before proceeding beschrieben.
Enum- und Boolean-Datentypen erstellen standardmäßig keine Constraints mehr.¶
Die Parameter Enum.create_constraint und Boolean.create_constraint haben nun standardmäßig False, was bedeutet, dass bei Erstellung einer sogenannten „nicht-nativen“ Version dieser beiden Datentypen standardmäßig **keine** CHECK-Constraint generiert wird. Diese CHECK-Constraints stellen Wartungskomplexitäten für das Schemamanagement dar, die opt-in sein sollten und nicht standardmäßig aktiviert sind.
Um sicherzustellen, dass eine CREATE CONSTRAINT-Anweisung für diese Typen ausgegeben wird, setzen Sie diese Flags auf True.
class Spam(Base):
__tablename__ = "spam"
id = Column(Integer, primary_key=True)
boolean = Column(Boolean(create_constraint=True))
enum = Column(Enum("a", "b", "c", create_constraint=True))Neue Features - ORM¶
Raiseload für Spalten¶
Die „raiseload“-Funktion, die InvalidRequestError auslöst, wenn auf ein nicht geladenes Attribut zugegriffen wird, ist nun für spaltenorientierte Attribute verfügbar über den Parameter defer.raiseload von defer(). Dies funktioniert genauso wie die raiseload()-Option, die für die Beziehungsladung verwendet wird.
book = session.query(Book).options(defer(Book.summary, raiseload=True)).first()
# would raise an exception
book.summaryUm spaltenbezogenes raiseload auf einer Abbildung zu konfigurieren, kann der Parameter deferred.raiseload von deferred() verwendet werden. Die Option undefer() kann dann zur Abfragezeit verwendet werden, um das Attribut eager zu laden.
class Book(Base):
__tablename__ = "book"
book_id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
summary = deferred(Column(String(2000)), raiseload=True)
excerpt = deferred(Column(Text), raiseload=True)
book_w_excerpt = session.query(Book).options(undefer(Book.excerpt)).first()Es wurde ursprünglich erwogen, die bestehende Option raiseload(), die für relationship()-Attribute funktioniert, auf spaltenorientierte Attribute auszuweiten. Dies würde jedoch das „Wildcard“-Verhalten von raiseload() brechen, das dokumentiert ist und es ermöglicht, alle Beziehungen nicht zu laden.
session.query(Order).options(joinedload(Order.items), raiseload("*"))Wie oben erwähnt, würde die Ausweitung von raiseload() auf Spalten auch Spalten vom Laden ausschließen und somit eine abwärtsinkompatible Änderung darstellen; außerdem ist unklar, wie man mit raiseload(), das sowohl Spaltenausdrücke als auch Beziehungen abdeckt, den oben genannten Effekt erzielen könnte, nur Beziehungen zu blockieren, ohne neue API hinzuzufügen. Um die Dinge einfach zu halten, bleibt die Option für Spalten bei defer().
raiseload()– Abfrageoption zum Auslösen von Beziehungsabrufen.
defer.raiseload– Abfrageoption zum Auslösen von Spaltenausdrucksabrufen.
Als Teil dieser Änderung hat sich das Verhalten von „deferred“ in Verbindung mit der Attributablaufung geändert. Zuvor, wenn ein Objekt als abgelaufen markiert und dann durch den Zugriff auf eines der abgelaufenen Attribute wieder abgelaufen wurde, wurden auch Attribute, die auf Mapper-Ebene als „deferred“ abgebildet waren, geladen. Dies wurde geändert, sodass ein Attribut, das in der Abbildung als deferred abgebildet ist, niemals „wieder abläuft“, sondern nur geladen wird, wenn es als Teil des Deferred-Ladeprogramms abgerufen wird.
Ein Attribut, das nicht als „deferred“ abgebildet ist, aber zur Abfragezeit über die Option defer() verzögert wurde, wird zurückgesetzt, wenn das Objekt oder Attribut abläuft; d. h., die Verzögerungsoption wird entfernt. Dies ist das gleiche Verhalten wie zuvor.
ORM Batch-Inserts mit psycopg2 führen nun in den meisten Fällen Batches mit RETURNING durch.¶
Die Änderung in psycopg2-Dialekt unterstützt „execute_values“ mit RETURNING für INSERT-Anweisungen standardmäßig fügt Unterstützung für „executemany“ + „RETURNING“ gleichzeitig in Core hinzu, was nun standardmäßig für den psycopg2-Dialekt unter Verwendung der psycopg2-Erweiterung execute_values() aktiviert ist. Der ORM-Flush-Prozess nutzt nun diese Funktion, sodass der Abruf neu generierter Primärschlüsselwerte und Server-Standardwerte erreicht werden kann, ohne die Leistungsvorteile des Batching von INSERT-Anweisungen zu verlieren. Darüber hinaus bietet die execute_values()-Erweiterung von psycopg2 selbst eine fünffache Leistungssteigerung gegenüber der Standard-„executemany“-Implementierung von psycopg2, indem eine INSERT-Anweisung umgeschrieben wird, um viele „VALUES“-Ausdrücke in einer einzigen Anweisung einzuschließen, anstatt dieselbe Anweisung wiederholt aufzurufen, da psycopg2 nicht die Möglichkeit hat, die Anweisung im Voraus vorzubereiten, wie es für diesen Ansatz normalerweise erwartet wird, um performant zu sein.
SQLAlchemy enthält eine Performance-Suite innerhalb seiner Beispiele, wo wir die Zeiten für den „batch_inserts“-Runner im Vergleich zu 1.3 und 1.4 vergleichen können, was eine 3- bis 5-fache Beschleunigung für die meisten Batch-Insert-Varianten zeigt.
# 1.3
$ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
test_flush_no_pk : (100000 iterations); total time 14.051527 sec
test_bulk_save_return_pks : (100000 iterations); total time 15.002470 sec
test_flush_pk_given : (100000 iterations); total time 7.863680 sec
test_bulk_save : (100000 iterations); total time 6.780378 sec
test_bulk_insert_mappings : (100000 iterations); total time 5.363070 sec
test_core_insert : (100000 iterations); total time 5.362647 sec
# 1.4 with enhancement
$ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
test_flush_no_pk : (100000 iterations); total time 3.820807 sec
test_bulk_save_return_pks : (100000 iterations); total time 3.176378 sec
test_flush_pk_given : (100000 iterations); total time 4.037789 sec
test_bulk_save : (100000 iterations); total time 2.604446 sec
test_bulk_insert_mappings : (100000 iterations); total time 1.204897 sec
test_core_insert : (100000 iterations); total time 0.958976 secBeachten Sie, dass die Erweiterung execute_values() die INSERT-Anweisung in der psycopg2-Schicht modifiziert, **nachdem** sie von SQLAlchemy protokolliert wurde. Bei der SQL-Protokollierung sehen Sie also, dass die Parametersätze zusammengefasst werden, aber die Zusammenführung mehrerer „Werte“ ist auf der Anwendungsseite nicht sichtbar.
2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (%(data)s) RETURNING a.id
2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine [generated in 0.00698s] ({'data': 'data 1'}, {'data': 'data 2'}, {'data': 'data 3'}, {'data': 'data 4'}, {'data': 'data 5'}, {'data': 'data 6'}, {'data': 'data 7'}, {'data': 'data 8'} ... displaying 10 of 4999 total bound parameter sets ... {'data': 'data 4998'}, {'data': 'data 4999'})
2020-06-27 19:08:18,254 INFO sqlalchemy.engine.Engine COMMITDie endgültige INSERT-Anweisung kann durch Aktivieren der Anweisungsprotokollierung auf der PostgreSQL-Seite eingesehen werden.
2020-06-27 19:08:18.169 EDT [26960] LOG: statement: INSERT INTO a (data)
VALUES ('data 1'),('data 2'),('data 3'),('data 4'),('data 5'),('data 6'),('data
7'),('data 8'),('data 9'),('data 10'),('data 11'),('data 12'),
... ('data 999'),('data 1000') RETURNING a.id
2020-06-27 19:08:18.175 EDT
[26960] LOG: statement: INSERT INTO a (data) VALUES ('data 1001'),('data
1002'),('data 1003'),('data 1004'),('data 1005 '),('data 1006'),('data
1007'),('data 1008'),('data 1009'),('data 1010'),('data 1011'), ...Die Funktion fasst Zeilen standardmäßig in Gruppen von 1000 zusammen, was über das Argument executemany_values_page_size gesteuert werden kann, das unter Psycopg2 Fast Execution Helpers dokumentiert ist.
ORM Bulk Update und Delete verwenden RETURNING für die „Fetch“-Strategie, wenn verfügbar.¶
Ein ORM Bulk Update oder Delete, das die „Fetch“-Strategie verwendet
sess.query(User).filter(User.age > 29).update(
{"age": User.age - 10}, synchronize_session="fetch"
)Wird nun RETURNING verwenden, wenn die Backend-Datenbank dies unterstützt; dies umfasst derzeit PostgreSQL und SQL Server (der Oracle-Dialekt unterstützt keine Rückgabe mehrerer Zeilen).
UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s RETURNING users.id
[generated in 0.00060s] {'age_int_1': 10, 'age_int_2': 29}
Col ('id',)
Row (2,)
Row (4,)Für Backends, die keine Rückgabe mehrerer Zeilen unterstützen, wird der bisherige Ansatz, zuvor SELECT für die Primärschlüssel auszugeben, weiterhin verwendet.
SELECT users.id FROM users WHERE users.age_int > %(age_int_1)s
[generated in 0.00043s] {'age_int_1': 29}
Col ('id',)
Row (2,)
Row (4,)
UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s
[generated in 0.00102s] {'age_int_1': 10, 'age_int_2': 29}Eine der komplexen Herausforderungen dieser Änderung ist die Unterstützung von Fällen wie der Horizontal Sharding Extension, bei der ein einzelnes Bulk-Update oder -Delete über Backends multiplexiert werden kann, von denen einige RETURNING unterstützen und andere nicht. Die neue 1.4-Ausführungsarchitektur unterstützt diesen Fall, sodass die „Fetch“-Strategie beibehalten werden kann, mit einer graceful Degradation zur Verwendung eines SELECT, anstatt eine neue „Returning“-Strategie hinzufügen zu müssen, die nicht backend-agnostisch wäre.
Als Teil dieser Änderung wird die „Fetch“-Strategie auch wesentlich effizienter, da sie die Objekte, die mit den Zeilen übereinstimmen, nicht mehr ablaufen lässt, für Python-Ausdrücke, die in der SET-Klausel verwendet werden und in Python ausgewertet werden können. Diese werden stattdessen direkt auf das Objekt angewendet, ähnlich wie bei der „Evaluate“-Strategie. Nur für SQL-Ausdrücke, die nicht ausgewertet werden können, fällt sie auf das Ablaufen der Attribute zurück. Die „Evaluate“-Strategie wurde ebenfalls verbessert, um für einen Wert, der nicht ausgewertet werden kann, auf „Expire“ zurückzufallen.
Verhaltensänderungen - ORM¶
Das von Query zurückgegebene „KeyedTuple“-Objekt wird durch Row ersetzt.¶
Wie unter RowProxy ist kein „Proxy“ mehr; heißt jetzt Row und verhält sich wie ein erweiterter benannter Tupel diskutiert, wird das Core RowProxy-Objekt nun durch eine Klasse namens Row ersetzt. Das Basis-Row-Objekt verhält sich nun vollständiger wie ein benanntes Tupel, und als solches wird es nun als Grundlage für Tupel-ähnliche Ergebnisse verwendet, die von dem Query-Objekt zurückgegeben werden, anstelle der bisherigen „KeyedTuple“-Klasse.
Die Begründung ist, dass bis SQLAlchemy 2.0 sowohl Core- als auch ORM-SELECT-Anweisungen Ergebniszeilen mit demselben Row-Objekt zurückgeben, das sich wie ein benanntes Tupel verhält. Wörterbuchähnliche Funktionalität ist von Row über das Attribut Row._mapping verfügbar. In der Zwischenzeit werden Core-Ergebnisdatensätze eine Row-Unterklasse LegacyRow verwenden, die das vorherige Wörterbuch/Tupel-Hybridverhalten zur Abwärtskompatibilität beibehält, während die Row-Klasse direkt für ORM-Tupelergebnisse verwendet wird, die vom Query-Objekt zurückgegeben werden.
Es wurde Anstrengung unternommen, die meisten Funktionen von Row im ORM verfügbar zu machen, was bedeutet, dass der Zugriff nach Zeichenkettennamen sowie nach Entität / Spalte funktionieren sollte
row = s.query(User, Address).join(User.addresses).first()
row._mapping[User] # same as row[0]
row._mapping[Address] # same as row[1]
row._mapping["User"] # same as row[0]
row._mapping["Address"] # same as row[1]
u1 = aliased(User)
row = s.query(u1).only_return_tuples(True).first()
row._mapping[u1] # same as row[0]
row = s.query(User.id, Address.email_address).join(User.addresses).first()
row._mapping[User.id] # same as row[0]
row._mapping["id"] # same as row[0]
row._mapping[users.c.id] # same as row[0]Session-Funktionen mit neuem „autobegin“-Verhalten¶
Zuvor würde die Session in ihrem Standardmodus von autocommit=False intern sofort nach der Konstruktion ein SessionTransaction-Objekt starten und zusätzlich nach jedem Aufruf von Session.rollback() oder Session.commit() ein neues erstellen.
Das neue Verhalten besteht darin, dass dieses SessionTransaction-Objekt nun bedarfsgesteuert erstellt wird, nur wenn Methoden wie Session.add() oder Session.execute() aufgerufen werden. Es ist jedoch nun auch möglich, Session.begin() explizit aufzurufen, um die Transaktion zu starten, auch im Modus autocommit=False, was das Verhalten der zukünftigen _base.Connection nachahmt.
Die Verhaltensänderungen, die dies anzeigt, sind
Die
Sessionkann sich nun in einem Zustand befinden, in dem keine Transaktion begonnen wurde, auch nicht im Modusautocommit=False. Zuvor war dieser Zustand nur im Modus „autocommit“ verfügbar.Innerhalb dieses Zustands sind die Methoden
Session.commit()undSession.rollback()No-Ops. Code, der sich auf diese Methoden verlässt, um alle Objekte zu ablaufen, sollte explizit entwederSession.begin()oderSession.expire_all()verwenden, um ihren Anwendungsfall zu erfüllen.Der Event-Hook
SessionEvents.after_transaction_create()wird nicht sofort ausgelöst, wenn dieSessionerstellt wird, oder nach Abschluss eines Aufrufs vonSession.rollback()oderSession.commit().Die Methode
Session.close()impliziert ebenfalls kein implizites Beginnen einer neuenSessionTransaction.
Siehe auch
Begründung¶
Das Standardverhalten der Session mit autocommit=False bedeutete historisch, dass immer ein SessionTransaction-Objekt über das Attribut Session.transaction mit der Session verbunden war. Wenn die gegebene SessionTransaction durch ein Commit, Rollback oder Schließen abgeschlossen wurde, wurde sie sofort durch eine neue ersetzt. Die SessionTransaction selbst impliziert nicht die Nutzung von verbindungsbezogenen Ressourcen, daher hat dieses langjährige Verhalten eine besondere Eleganz, da der Zustand von Session.transaction immer als nicht-None vorhersehbar ist.
Im Rahmen der Initiative in #5056 zur erheblichen Reduzierung von Referenzzyklen bedeutet diese Annahme jedoch, dass der Aufruf von Session.close() zu einem Session-Objekt führt, das immer noch Referenzzyklen aufweist und teurer zu bereinigen ist, ganz zu schweigen von einem geringen Overhead beim Erstellen des SessionTransaction-Objekts, was bedeutete, dass für eine Session, die beispielsweise Session.commit() und dann Session.close() aufrief, unnötiger Overhead erzeugt wurde.
Daher wurde beschlossen, dass Session.close() den internen Zustand von self.transaction, nun intern als self._transaction bezeichnet, als None hinterlassen sollte und dass eine neue SessionTransaction nur dann erstellt werden sollte, wenn sie benötigt wird. Zur Konsistenz und Codeabdeckung wurde dieses Verhalten auch auf alle Punkte ausgedehnt, an denen „autobegin“ erwartet wird, nicht nur wenn Session.close() aufgerufen wurde.
Insbesondere führt dies zu einer Verhaltensänderung für Anwendungen, die den Event-Hook SessionEvents.after_transaction_create() abonnieren; zuvor wurde dieses Ereignis beim ersten Erstellen der Session sowie bei den meisten Aktionen ausgelöst, die die vorherige Transaktion schlossen und SessionEvents.after_transaction_end() auslösten. Das neue Verhalten ist, dass SessionEvents.after_transaction_create() bedarfsgesteuert ausgelöst wird, wenn die Session noch kein neues SessionTransaction-Objekt erstellt hat und zugeordnete Objekte über Methoden wie Session.add() und Session.delete() mit der Session verknüpft sind, wenn auf das Attribut Session.transaction zugegriffen wird, wenn die Methode Session.flush() Aufgaben zu erledigen hat, usw.
Zusätzlich können Code, der sich auf die Methode Session.commit() oder Session.rollback() verlässt, um bedingungslos alle Objekte ablaufen zu lassen, dies nicht mehr tun. Code, der alle Objekte ablaufen lassen muss, wenn keine Änderungen aufgetreten sind, sollte für diesen Fall Session.expire_all() aufrufen.
Abgesehen von der Änderung, wann das Ereignis SessionEvents.after_transaction_create() ausgelöst wird, sowie der No-Op-Natur von Session.commit() oder Session.rollback(), sollte die Änderung keine weiteren sichtbaren Auswirkungen auf das Verhalten des Session-Objekts haben; die Session wird weiterhin das Verhalten aufweisen, dass sie für neue Operationen nach dem Aufruf von Session.close() nutzbar bleibt, und die Sequenzierung, wie die Session mit der Engine und der Datenbank selbst interagiert, sollte ebenfalls unberührt bleiben, da diese Operationen bereits bedarfsgesteuert abliefen.
Nur-Ansicht-Beziehungen synchronisieren keine Rückverweise¶
In #5149 in 1.3.14 begann SQLAlchemy mit der Ausgabe einer Warnung, wenn die Schlüssel relationship.backref oder relationship.back_populates gleichzeitig mit dem Flag relationship.viewonly für die Zielbeziehung verwendet würden. Dies liegt daran, dass eine „Nur-Ansicht“-Beziehung Änderungen, die daran vorgenommen werden, nicht tatsächlich speichert, was zu einigen irreführenden Verhaltensweisen führen könnte. In #5237 versuchten wir jedoch, dieses Verhalten zu verfeinern, da es legitime Anwendungsfälle gibt, Rückverweise auf Nur-Ansicht-Beziehungen einzurichten, einschließlich der Tatsache, dass Rückverweisattribute in einigen Fällen von den Relationship-Lazy-Loadern verwendet werden, um festzustellen, dass eine zusätzliche Eager-Load-Anforderung in die andere Richtung nicht notwendig ist, sowie dass Rückverweise für die Mapper-Introspektion verwendet werden können und dass backref() eine bequeme Möglichkeit sein kann, bidirektionale Beziehungen einzurichten.
Die Lösung bestand dann darin, die „Mutation“, die von einem Rückverweis herrührt, optional zu gestalten, indem das Flag relationship.sync_backref verwendet wurde. In 1.4 ist der Wert von relationship.sync_backref standardmäßig False für ein Beziehungsziel, das auch relationship.viewonly setzt. Dies zeigt an, dass alle Änderungen an einer Nur-Ansicht-Beziehung den Zustand der anderen Seite oder der Session in keiner Weise beeinflussen
class User(Base):
# ...
addresses = relationship(Address, backref=backref("user", viewonly=True))
class Address(Base): ...
u1 = session.query(User).filter_by(name="x").first()
a1 = Address()
a1.user = u1Oben wird das a1-Objekt **nicht** zur u1.addresses-Sammlung hinzugefügt, noch wird das a1-Objekt zur Session hinzugefügt. Zuvor wären beides wahr. Die Warnung, dass relationship.sync_backref auf False gesetzt werden sollte, wenn relationship.viewonly False ist, wird nicht mehr ausgegeben, da dies nun das Standardverhalten ist.
cascade_backrefs-Verhalten zur Entfernung in 2.0 veraltet¶
SQLAlchemy hatte lange ein Verhalten, Objekte basierend auf Rückverweiszuweisung in die Session zu kaskadieren. Angenommen, User unten ist bereits in einer Session, die Zuweisung zu dem Attribut Address.user eines Address-Objekts, unter der Annahme einer bidirektionalen Beziehung, würde bedeuten, dass die Address zu diesem Zeitpunkt auch in die Session gestellt wird.
u1 = User()
session.add(u1)
a1 = Address()
a1.user = u1 # <--- adds "a1" to the SessionDas obige Verhalten war ein unbeabsichtigter Nebeneffekt des Rückverweisverhaltens, da a1.user u1.addresses.append(a1) impliziert, würde a1 in die Session kaskadiert werden. Dies bleibt das Standardverhalten während 1.4. Zu einem späteren Zeitpunkt wurde eine neue Flagge relationship.cascade_backrefs hinzugefügt, um das obige Verhalten zu deaktivieren, zusammen mit backref.cascade_backrefs, um dies festzulegen, wenn die Beziehung durch relationship.backref spezifiziert wird, da es überraschend sein kann und auch einige Operationen behindert, bei denen das Objekt zu früh in die Session gestellt wird und vorzeitig geleert wird.
In 2.0 ist das Standardverhalten, dass „cascade_backrefs“ False ist, und zusätzlich wird kein „True“-Verhalten existieren, da dies im Allgemeinen kein wünschenswertes Verhalten ist. Wenn 2.0-Deprecation-Warnungen aktiviert sind, wird eine Warnung ausgegeben, wenn eine „backref cascade“ tatsächlich stattfindet. Um das neue Verhalten zu erhalten, setzen Sie entweder relationship.cascade_backrefs und backref.cascade_backrefs auf False für alle Zielbeziehungen, wie es bereits in 1.3 und früher unterstützt wird, oder verwenden Sie alternativ die Flagge Session.future für den 2.0-Stil-Modus
Session = sessionmaker(engine, future=True)
with Session() as session:
u1 = User()
session.add(u1)
a1 = Address()
a1.user = u1 # <--- will not add "a1" to the SessionEager-Loader werden bei Unexpire-Operationen ausgelöst¶
Ein lang erwartetes Verhalten war, dass beim Zugriff auf ein abgelaufenes Objekt konfigurierte Eager-Loader ausgeführt werden, um Beziehungen auf dem abgelaufenen Objekt eifrig zu laden, wenn das Objekt aktualisiert oder anderweitig abgelaufen wird. Dieses Verhalten wurde nun hinzugefügt, sodass Joinedloader wie üblich Inline-JOINs hinzufügen und Selectin/Subquery-Loader eine „Immediateload“-Operation für eine gegebene Beziehung ausführen, wenn ein abgelaufenes Objekt wieder abgelaufen wird oder ein Objekt aktualisiert wird.
>>> a1 = session.query(A).options(joinedload(A.bs)).first()
>>> a1.data = "new data"
>>> session.commit()Oben wurde das A-Objekt mit einer joinedload()-Option geladen, um die bs-Sammlung eifrig zu laden. Nach dem session.commit() ist der Zustand des Objekts abgelaufen. Beim Zugriff auf das Spaltenattribut .data wird das Objekt aktualisiert, und dies beinhaltet nun auch die Joinedload-Operation.
>>> a1.data
SELECT a.id AS a_id, a.data AS a_data, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
WHERE a.id = ?
Das Verhalten gilt sowohl für Loader-Strategien, die direkt auf die relationship() angewendet werden, als auch für Optionen, die mit Query.options() verwendet werden, vorausgesetzt, das Objekt wurde ursprünglich von dieser Abfrage geladen.
Für die „sekundären“ Eager-Loader „selectinload“ und „subqueryload“ ist die SQL-Strategie für diese Loader nicht erforderlich, um Attribute auf einem einzelnen Objekt eifrig zu laden; daher rufen sie im Aktualisierungsfall die „immediateload“-Strategie auf, die der Abfrage ähnelt, die von „lazyload“ ausgegeben wird und als zusätzliche Abfrage ausgegeben wird.
>>> a1 = session.query(A).options(selectinload(A.bs)).first()
>>> a1.data = "new data"
>>> session.commit()
>>> a1.data
SELECT a.id AS a_id, a.data AS a_data
FROM a
WHERE a.id = ?
(1,)
SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE ? = b.a_id
(1,)
Beachten Sie, dass eine Loader-Option nicht für ein Objekt gilt, das auf eine andere Weise in die Session eingeführt wurde. Das heißt, wenn das a1-Objekt gerade in dieser Session gespeichert wurde oder mit einer anderen Abfrage geladen wurde, bevor die Eager-Option angewendet wurde, hat das Objekt keine zugeordnete Eager-Load-Option. Dies ist kein neues Konzept, aber Benutzer, die die Eagerload-on-Refresh-Funktion suchen, werden dies möglicherweise stärker bemerken.
Spalten-Loader wie deferred(), with_expression() wirken nur dann, wenn sie auf der äußersten, vollständigen Entitätsabfrage angegeben sind¶
Hinweis
Diese Änderungsnotiz war in früheren Versionen dieses Dokuments nicht vorhanden, ist aber für alle SQLAlchemy 1.4-Versionen relevant.
Ein Verhalten, das in 1.3 und früheren Versionen nie unterstützt wurde und dennoch eine besondere Auswirkung hatte, war die Wiederverwendung von Spalten-Loader-Optionen wie defer() und with_expression() in Subabfragen, um zu steuern, welche SQL-Ausdrücke in der Spaltenklausel jeder Subabfrage enthalten sein würden. Ein typisches Beispiel wäre die Konstruktion von UNION-Abfragen, wie zum Beispiel
q1 = session.query(User).options(with_expression(User.expr, literal("u1")))
q2 = session.query(User).options(with_expression(User.expr, literal("u2")))
q1.union_all(q2).all()In Version 1.3 wirkte sich die Option with_expression() auf jedes Element der UNION aus, wie zum Beispiel
SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id,
anon_1.user_account_name AS anon_1_user_account_name
FROM (
SELECT ? AS anon_2, user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
UNION ALL
SELECT ? AS anon_3, user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
) AS anon_1
('u1', 'u2')Die Vorstellung von Loader-Optionen in SQLAlchemy 1.4 wurde strenger gestaltet und wird daher **nur auf den äußersten Teil der Abfrage** angewendet, nämlich auf die SELECT-Anweisung, die die eigentlichen ORM-Entitäten zum Zurückgeben füllen soll. Die obige Abfrage in 1.4 erzeugt
SELECT ? AS anon_1, anon_2.user_account_id AS anon_2_user_account_id,
anon_2.user_account_name AS anon_2_user_account_name
FROM (
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
UNION ALL
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
) AS anon_2
('u1',)Das heißt, die Optionen für die Query wurden aus dem ersten Element der UNION übernommen, da alle Loader-Optionen nur auf der obersten Ebene liegen sollen. Die Option aus der zweiten Abfrage wurde ignoriert.
Begründung¶
Dieses Verhalten entspricht nun enger dem anderer Arten von Loader-Optionen wie Relationship-Loader-Optionen wie joinedload() in allen SQLAlchemy-Versionen, einschließlich 1.3 und früher, die in einer UNION-Situation bereits in die oberste Ebene der Abfrage kopiert und nur aus dem ersten Element der UNION übernommen wurden, wobei alle Optionen in anderen Teilen der Abfrage verworfen wurden.
Dieses implizite Kopieren und selektive Ignorieren von Optionen, das oben als ziemlich willkürlich dargestellt wird, ist ein Legacy-Verhalten, das nur Teil von Query ist, und ein besonderes Beispiel dafür, wo Query und seine Mittel zur Anwendung von Query.union_all() zu kurz greifen, da es unklar ist, wie eine einzelne SELECT in eine UNION davon und einer anderen Abfrage umgewandelt wird und wie Loader-Optionen auf diese neue Anweisung angewendet werden sollen.
Das Verhalten von SQLAlchemy 1.4 kann als generell besser als das von 1.3 für einen gebräuchlicheren Fall der Verwendung von defer() demonstriert werden. Die folgende Abfrage
q1 = session.query(User).options(defer(User.name))
q2 = session.query(User).options(defer(User.name))
q1.union_all(q2).all()In 1.3 würde NULL unbeholfen zu den inneren Abfragen hinzugefügt und dann selektiert werden.
SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id
FROM (
SELECT NULL AS anon_2, user_account.id AS user_account_id
FROM user_account
UNION ALL
SELECT NULL AS anon_2, user_account.id AS user_account_id
FROM user_account
) AS anon_1Wenn nicht alle Abfragen identische Optionen hatten, würde das obige Szenario einen Fehler auslösen, da keine ordnungsgemäße UNION gebildet werden konnte.
Während in 1.4 die Option nur auf der obersten Ebene angewendet wird und das Abrufen von User.name weggelassen wird, wird diese Komplexität vermieden.
SELECT anon_1.user_account_id AS anon_1_user_account_id
FROM (
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
UNION ALL
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
) AS anon_1Richtiger Ansatz¶
Bei Verwendung von 2.0-Stil-Abfragen wird derzeit keine Warnung ausgegeben, jedoch werden verschachtelte with_expression()-Optionen konsistent ignoriert, da sie nicht auf eine geladene Entität angewendet werden und nicht implizit kopiert werden. Die folgende Abfrage erzeugt keine Ausgabe für die Aufrufe von with_expression()
s1 = select(User).options(with_expression(User.expr, literal("u1")))
s2 = select(User).options(with_expression(User.expr, literal("u2")))
stmt = union_all(s1, s2)
session.scalars(select(User).from_statement(stmt)).all()erzeugt die SQL
SELECT user_account.id, user_account.name
FROM user_account
UNION ALL
SELECT user_account.id, user_account.name
FROM user_accountUm with_expression() korrekt auf die User-Entität anzuwenden, muss sie auf der äußersten Ebene der Abfrage angewendet werden, indem ein gewöhnlicher SQL-Ausdruck in der Spaltenklausel jedes SELECT verwendet wird.
s1 = select(User, literal("u1").label("some_literal"))
s2 = select(User, literal("u2").label("some_literal"))
stmt = union_all(s1, s2)
session.scalars(
select(User)
.from_statement(stmt)
.options(with_expression(User.expr, stmt.selected_columns.some_literal))
).all()Was die erwartete SQL erzeugt
SELECT user_account.id, user_account.name, ? AS some_literal
FROM user_account
UNION ALL
SELECT user_account.id, user_account.name, ? AS some_literal
FROM user_accountDie User-Objekte selbst werden diesen Ausdruck in ihren Inhalten unter User.expr enthalten.
Der Zugriff auf ein nicht initialisiertes Sammlungsattribut auf einem transienten Objekt mutiert das __dict__ nicht mehr¶
Es war schon immer das Verhalten von SQLAlchemy, dass der Zugriff auf zugeordnete Attribute eines neu erstellten Objekts einen implizit generierten Wert zurückgibt, anstatt einen AttributeError auszulösen, wie z. B. None für Skalarattribute oder [] für eine Listen-haltige Beziehung.
>>> u1 = User()
>>> u1.name
None
>>> u1.addresses
[]Die Begründung für das obige Verhalten bestand ursprünglich darin, die Arbeit mit ORM-Objekten zu erleichtern. Da ein ORM-Objekt eine leere Zeile darstellt, wenn es zum ersten Mal ohne Zustand erstellt wird, ist es intuitiv, dass seine nicht aufgerufenen Attribute zu None (oder SQL NULL) für Skalare und zu leeren Sammlungen für Beziehungen aufgelöst werden. Insbesondere ermöglicht es ein äußerst gängiges Muster, die neue Sammlung zu ändern, ohne manuell eine leere Sammlung zu erstellen und zuzuweisen.
>>> u1 = User()
>>> u1.addresses.append(Address()) # no need to assign u1.addresses = []Bis zur Version 1.0 von SQLAlchemy verhielt sich dieses Initialisierungssystem sowohl für skalare Attribute als auch für Sammlungen so, dass der Wert None oder die leere Sammlung in den Zustand des Objekts, z. B. __dict__, *populiert* wurde. Dies bedeutete, dass die folgenden beiden Operationen äquivalent waren
>>> u1 = User()
>>> u1.name = None # explicit assignment
>>> u2 = User()
>>> u2.name # implicit assignment just by accessing it
NoneWo oben sowohl u1 als auch u2 den Wert None im Wert des Attributs name gesetzt hatten. Da dies ein SQL NULL ist, übersprang die ORM das Einfügen dieser Werte in ein INSERT, damit SQL-seitige Standardwerte, falls vorhanden, greifen, andernfalls wird der Wert auf der Datenbankseite auf NULL gesetzt.
In Version 1.0 wurde als Teil von Änderungen an Attributereignissen und anderen Operationen bezüglich Attribute, die keinen vorbestehenden Wert haben dieses Verhalten verfeinert, sodass der Wert None nicht mehr in __dict__ populiert wurde, sondern nur zurückgegeben wurde. Neben der Entfernung des mutierenden Nebeneffekts einer Getter-Operation ermöglichte diese Änderung auch das Setzen von Spalten, die Server-Standardwerte hatten, auf den Wert NULL, indem tatsächlich None zugewiesen wurde, was nun von der bloßen Lektüre unterschieden wurde.
Die Änderung berücksichtigte jedoch keine Sammlungen, bei denen die Rückgabe einer leeren Sammlung, die nicht zugewiesen ist, bedeutete, dass diese veränderliche Sammlung jedes Mal unterschiedlich wäre und auch keine mutierenden Operationen (z. B. append, add usw.) korrekt aufnehmen könnte. Während das Verhalten im Allgemeinen niemanden störte, wurde schließlich ein Grenzfall in #4519 identifiziert, bei dem diese leere Sammlung schädlich sein könnte, nämlich wenn das Objekt in eine Sitzung gemergt wird
>>> u1 = User(id=1) # create an empty User to merge with id=1 in the database
>>> merged1 = session.merge(
... u1
... ) # value of merged1.addresses is unchanged from that of the DB
>>> u2 = User(id=2) # create an empty User to merge with id=2 in the database
>>> u2.addresses
[]
>>> merged2 = session.merge(u2) # value of merged2.addresses has been emptied in the DBOben enthält die Sammlung .addresses auf merged1 alle Address() Objekte, die bereits in der Datenbank waren. merged2 wird dies nicht tun; da ihm implizit eine leere Liste zugewiesen ist, wird die Sammlung .addresses gelöscht. Dies ist ein Beispiel dafür, wie dieser mutierende Nebeneffekt die Datenbank selbst mutieren kann.
Während erwogen wurde, ob das Attributsystem nicht ein strenges „Plain Python“-Verhalten annehmen sollte, das in allen Fällen für nicht existierende Attribute auf nicht persistenten Objekten einen AttributeError auslöst und explizite Zuweisung aller Sammlungen erfordert, wäre eine solche Änderung für die große Anzahl von Anwendungen, die sich seit vielen Jahren auf dieses Verhalten verlassen haben, wahrscheinlich zu extrem, was zu einer komplexen Einführung / Rückwärtskompatibilitätsproblemen sowie der Wahrscheinlichkeit führen würde, dass Workarounds zur Wiederherstellung des alten Verhaltens verbreitet würden und die gesamte Änderung somit unwirksam wäre.
Die Änderung besteht also darin, das standardmäßig produzierende Verhalten beizubehalten, aber das nicht-mutierende Verhalten von Skalaren auch für Sammlungen durch die Hinzufügung zusätzlicher Mechanismen im Sammelsystem zu realisieren. Beim Zugriff auf das leere Attribut wird die neue Sammlung erstellt und mit dem Zustand verknüpft, jedoch erst dann zu __dict__ hinzugefügt, wenn sie tatsächlich mutiert wird.
>>> u1 = User()
>>> l1 = u1.addresses # new list is created, associated with the state
>>> assert u1.addresses is l1 # you get the same list each time you access it
>>> assert (
... "addresses" not in u1.__dict__
... ) # but it won't go into __dict__ until it's mutated
>>> from sqlalchemy import inspect
>>> inspect(u1).attrs.addresses.history
History(added=None, unchanged=None, deleted=None)Wenn die Liste geändert wird, wird sie zu den verfolgten Änderungen, die in die Datenbank geschrieben werden sollen
>>> l1.append(Address())
>>> assert "addresses" in u1.__dict__
>>> inspect(u1).attrs.addresses.history
History(added=[<__main__.Address object at 0x7f49b725eda0>], unchanged=[], deleted=[])Diese Änderung wird *nahezu* keine Auswirkungen auf bestehende Anwendungen haben, außer dass beobachtet wurde, dass einige Anwendungen sich auf die implizite Zuweisung dieser Sammlung verlassen könnten, z. B. um zu behaupten, dass das Objekt bestimmte Werte basierend auf seinem __dict__ enthält.
>>> u1 = User()
>>> u1.addresses
[]
# this will now fail, would pass before
>>> assert {k: v for k, v in u1.__dict__.items() if not k.startswith("_")} == {
... "addresses": []
... }oder um sicherzustellen, dass die Sammlung für das Fortfahren keine Lazy-Load benötigt. Der (zugegebenermaßen umständliche) Code unten wird nun ebenfalls fehlschlagen.
>>> u1 = User()
>>> u1.addresses
[]
>>> s.add(u1)
>>> s.flush()
>>> s.close()
>>> u1.addresses # <-- will fail, .addresses is not loaded and object is detachedAnwendungen, die sich auf das implizite mutierende Verhalten von Sammlungen verlassen, müssen geändert werden, damit sie die gewünschte Sammlung explizit zuweisen.
>>> u1.addresses = []Die Fehlermeldung „New instance conflicts with existing identity“ ist jetzt eine Warnung¶
SQLAlchemy hatte schon immer eine Logik, um zu erkennen, wenn ein Objekt in der Session, das eingefügt werden soll, denselben Primärschlüssel hat wie ein bereits vorhandenes Objekt.
class Product(Base):
__tablename__ = "product"
id = Column(Integer, primary_key=True)
session = Session(engine)
# add Product with primary key 1
session.add(Product(id=1))
session.flush()
# add another Product with same primary key
session.add(Product(id=1))
s.commit() # <-- will raise FlushErrorDie Änderung besteht darin, dass der FlushError zu einer Warnung geändert wird.
sqlalchemy/orm/persistence.py:408: SAWarning: New instance <Product at 0x7f1ff65e0ba8> with identity key (<class '__main__.Product'>, (1,), None) conflicts with persistent instance <Product at 0x7f1ff60a4550>Anschließend versucht die Bedingung, die Zeile in die Datenbank einzufügen, was einen IntegrityError auslöst, was derselbe Fehler ist, der ausgelöst würde, wenn die Primärschlüssel-Identität nicht bereits in der Session vorhanden wäre.
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: product.idDer Grundgedanke ist, Code, der IntegrityError zum Abfangen von Duplikaten verwendet, unabhängig vom vorhandenen Zustand der Session funktionieren zu lassen, wie es oft mit Savepoints geschieht.
# add another Product with same primary key
try:
with session.begin_nested():
session.add(Product(id=1))
except exc.IntegrityError:
print("row already exists")Die obige Logik war früher nicht vollständig machbar, da im Fall, dass das Product-Objekt mit der vorhandenen Identität bereits in der Session war, der Code auch FlushError abfangen müsste, was zusätzlich nicht auf die spezifische Bedingung von Integritätsproblemen gefiltert ist. Mit der Änderung verhält sich der obige Block konsistent, mit der Ausnahme, dass die Warnung ebenfalls ausgegeben wird.
Da die fragliche Logik sich auf den Primärschlüssel bezieht, lösen alle Datenbanken im Falle von Primärschlüsselkonflikten bei INSERT einen Integritätsfehler aus. Der Fall, in dem kein Fehler ausgelöst würde, der früher ausgelöst worden wäre, ist das extrem ungewöhnliche Szenario einer Zuordnung, die einen Primärschlüssel auf dem zugeordneten wählbaren Element definiert, der restriktiver ist als das, was tatsächlich im Datenbankschema konfiguriert ist, z. B. beim Zuordnen zu Joins von Tabellen oder beim Definieren zusätzlicher Spalten als Teil eines zusammengesetzten Primärschlüssels, der nicht tatsächlich im Datenbankschema eingeschränkt ist. Diese Situationen funktionieren jedoch auch konsistenter, da der INSERT theoretisch fortgesetzt würde, unabhängig davon, ob die vorhandene Identität noch in der Datenbank vorhanden ist. Die Warnung kann auch mit dem Python-Warnungsfilter so konfiguriert werden, dass sie eine Ausnahme auslöst.
Auf die Persistenz bezogene Cascade-Operationen sind mit viewonly=True nicht zulässig¶
Wenn ein relationship() mit dem Flag viewonly=True über den relationship.viewonly gesetzt wird, zeigt dies an, dass diese Beziehung nur zum Laden von Daten aus der Datenbank verwendet werden soll und nicht mutiert oder in eine Persistenzoperation einbezogen werden darf. Um sicherzustellen, dass dieser Vertrag erfolgreich funktioniert, darf die Beziehung keine relationship.cascade-Einstellungen mehr angeben, die im Hinblick auf „viewonly“ keinen Sinn ergeben.
Die wichtigsten Ziele hier sind die „delete, delete-orphan“-Cascades, die bis 1.3 weiterhin die Persistenz beeinflussten, auch wenn viewonly True war, was ein Fehler ist; selbst wenn viewonly True war, würde ein Objekt diese beiden Operationen immer noch auf das zugehörige Objekt kaskadieren, wenn das Elternobjekt gelöscht oder das Objekt abgetrennt wurde. Anstatt die Cascade-Operationen zu ändern, um viewonly zu überprüfen, ist die Konfiguration beider zusammen einfach nicht zulässig.
class User(Base):
# ...
# this is now an error
addresses = relationship("Address", viewonly=True, cascade="all, delete-orphan")Das Obige löst Folgendes aus
sqlalchemy.exc.ArgumentError: Cascade settings
"delete, delete-orphan, merge, save-update" apply to persistence
operations and should not be combined with a viewonly=True relationship.Anwendungen, die dieses Problem haben, sollten seit SQLAlchemy 1.3.12 eine Warnung ausgeben, und für den obigen Fehler besteht die Lösung darin, die Cascade-Einstellungen für eine viewonly-Beziehung zu entfernen.
Strengeres Verhalten beim Abfragen von Vererbungszuordnungen mit benutzerdefinierten Abfragen¶
Diese Änderung betrifft das Szenario, in dem eine Unterklasse-Entität für Joined- oder Single-Table-Vererbung abgefragt wird, wobei eine abgeschlossene SELECT-Unterabfrage als Grundlage dient. Wenn die angegebene Unterabfrage Zeilen zurückgibt, die nicht mit der angeforderten polymorphen Identität oder Identitäten übereinstimmen, wird ein Fehler ausgelöst. Zuvor wäre diese Bedingung bei Joined-Table-Vererbung lautlos durchgelaufen, wobei eine ungültige Unterklasse zurückgegeben wurde, und bei Single-Table-Vererbung würde die Query zusätzliche Kriterien gegen die Unterabfrage hinzufügen, um die Ergebnisse zu begrenzen, was die Absicht der Abfrage unangemessen beeinträchtigen könnte.
Unter der Beispielzuordnung von Employee, Engineer(Employee), Manager(Employee) würde in der 1.3-Serie, wenn wir die folgende Abfrage gegen eine Joined-Inheritance-Zuordnung ausgeben
s = Session(e)
s.add_all([Engineer(), Manager()])
s.commit()
print(s.query(Manager).select_entity_from(s.query(Employee).subquery()).all())Die Unterabfrage wählt sowohl die Zeilen von Engineer als auch von Manager aus, und obwohl die äußere Abfrage gegen Manager läuft, erhalten wir ein Objekt zurück, das kein Manager ist.
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
2020-01-29 18:04:13,524 INFO sqlalchemy.engine.base.Engine ()
[<__main__.Engineer object at 0x7f7f5b9a9810>, <__main__.Manager object at 0x7f7f5b9a9750>]Das neue Verhalten ist, dass diese Bedingung einen Fehler auslöst.
sqlalchemy.exc.InvalidRequestError: Row with identity key
(<class '__main__.Employee'>, (1,), None) can't be loaded into an object;
the polymorphic discriminator column '%(140205120401296 anon)s.type'
refers to mapped class Engineer->engineer, which is not a sub-mapper of
the requested mapped class Manager->managerDer obige Fehler wird nur ausgelöst, wenn die Primärschlüsselspalten der Entität nicht NULL sind. Wenn für eine gegebene Entität in einer Zeile kein Primärschlüssel vorhanden ist, wird kein Versuch unternommen, eine Entität zu konstruieren.
Bei der Single-Table-Inheritance-Zuordnung ist die Verhaltensänderung etwas komplexer; wenn Engineer und Manager oben mit Single-Table-Inheritance zugeordnet sind, würde in 1.3 die folgende Abfrage ausgegeben und nur ein Manager-Objekt zurückgegeben.
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
WHERE anon_1.type IN (?)
2020-01-29 18:08:32,975 INFO sqlalchemy.engine.base.Engine ('manager',)
[<__main__.Manager object at 0x7ff1b0200d50>]Die Query fügte die Kriterien für „single table inheritance“ zur Unterabfrage hinzu und interpretierte die ursprüngliche Absicht, die von ihr festgelegt wurde. Dieses Verhalten wurde in Version 1.0 in #3891 hinzugefügt und schafft eine Verhaltensinkonsistenz zwischen „joined“ und „single“ table inheritance und modifiziert zudem die Absicht der gegebenen Abfrage, die möglicherweise zusätzliche Zeilen zurückgeben soll, bei denen die den ererbenden Entitäten entsprechenden Spalten NULL sind, was ein gültiger Anwendungsfall ist. Das Verhalten ist nun äquivalent zu dem der Joined-Table-Vererbung, bei der angenommen wird, dass die Unterabfrage die korrekten Zeilen zurückgibt und ein Fehler ausgelöst wird, wenn eine unerwartete polymorphe Identität angetroffen wird.
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
2020-01-29 18:13:10,554 INFO sqlalchemy.engine.base.Engine ()
Traceback (most recent call last):
# ...
sqlalchemy.exc.InvalidRequestError: Row with identity key
(<class '__main__.Employee'>, (1,), None) can't be loaded into an object;
the polymorphic discriminator column '%(140700085268432 anon)s.type'
refers to mapped class Engineer->employee, which is not a sub-mapper of
the requested mapped class Manager->employeeDie korrekte Anpassung der oben dargestellten Situation, die unter 1.3 funktionierte, besteht darin, die gegebene Unterabfrage anzupassen, um die Zeilen basierend auf der Diskriminatorspalte korrekt zu filtern.
print(
s.query(Manager)
.select_entity_from(
s.query(Employee).filter(Employee.discriminator == "manager").subquery()
)
.all()
)SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee
WHERE employee.type = ?) AS anon_1
2020-01-29 18:14:49,770 INFO sqlalchemy.engine.base.Engine ('manager',)
[<__main__.Manager object at 0x7f70e13fca90>]Dialektänderungen¶
pg8000 Mindestversion ist 1.16.6, unterstützt nur Python 3¶
Die Unterstützung für den pg8000-Dialekt wurde mit Hilfe des Projektverwalters dramatisch verbessert.
Aufgrund von API-Änderungen erfordert der pg8000-Dialekt nun die Version 1.16.6 oder höher. Die pg8000-Serie hat die Python-2-Unterstützung ab der 1.13-Serie eingestellt. Python-2-Benutzer, die pg8000 benötigen, sollten sicherstellen, dass ihre Anforderungen auf SQLAlchemy<1.4 beschränkt sind.
Für den PostgreSQL psycopg2-Dialekt ist Version 2.7 oder höher erforderlich¶
Der psycopg2-Dialekt stützt sich auf viele Funktionen von psycopg2, die in den letzten Jahren veröffentlicht wurden. Um den Dialekt zu vereinfachen, ist nun die am 2017. März veröffentlichte Version 2.7 die Mindestversion.
Der psycopg2-Dialekt hat keine Einschränkungen mehr bezüglich der Namen von gebundenen Parametern¶
SQLAlchemy 1.3 konnte im psycopg2-Dialekt keine gebundenen Parameternamen verarbeiten, die Prozentzeichen oder Klammern enthielten. Dies bedeutete wiederum, dass Spaltennamen, die diese Zeichen enthielten, ebenfalls problematisch waren, da INSERT und andere DML-Anweisungen Parameternamen generierten, die dem Spaltennamen entsprachen, was dann zu Fehlern führte. Die Umgehung bestand darin, den Column.key-Parameter zu verwenden, um einen alternativen Namen zu verwenden, der zur Generierung des Parameters verwendet würde, oder andernfalls der Parameterstil des Dialekts auf der Ebene von create_engine() geändert werden musste. Seit SQLAlchemy 1.4.0beta3 sind alle Namensbeschränkungen aufgehoben und die Parameter werden in allen Szenarien vollständig maskiert, sodass diese Umgehungen nicht mehr erforderlich sind.
Der psycopg2-Dialekt verwendet standardmäßig „execute_values“ mit RETURNING für INSERT-Anweisungen¶
Die erste Hälfte einer signifikanten Leistungsverbesserung für PostgreSQL bei der Verwendung von sowohl Core als auch ORM: Der psycopg2-Dialekt verwendet nun standardmäßig psycopg2.extras.execute_values() für kompilierte INSERT-Anweisungen und implementiert auch RETURNING-Unterstützung in diesem Modus. Die andere Hälfte dieser Änderung ist ORM Batch-Inserts mit psycopg2 schlagen nun Statements mit RETURNING in den meisten Fällen zusammen, was es der ORM ermöglicht, RETURNING mit executemany (d. h. Batching von INSERT-Anweisungen) zu nutzen, sodass ORM-Bulk-Inserts mit psycopg2 je nach Spezifikationen bis zu 400 % schneller sind.
Diese Erweiterungsmethode ermöglicht das Einfügen vieler Zeilen innerhalb einer einzigen Anweisung unter Verwendung einer erweiterten VALUES-Klausel für die Anweisung. Während die insert()-Konstruktion von SQLAlchemy diese Syntax bereits über die Methode Insert.values() unterstützt, ermöglicht die Erweiterungsmethode die dynamische Konstruktion der VALUES-Klausel, wenn die Anweisung als „executemany“-Ausführung ausgeführt wird, was passiert, wenn man eine Liste von Parameterwörterbüchern an Connection.execute() übergibt. Sie tritt auch über die Cache-Grenze hinaus auf, sodass die INSERT-Anweisung vor dem Rendern der VALUES gecached werden kann.
Ein schneller Test des execute_values()-Ansatzes unter Verwendung des Skripts bulk_inserts.py in der Leistung-Beispielsuite (Performance) zeigt eine ungefähre **fünffache Leistungssteigerung**.
$ python -m examples.performance bulk_inserts --test test_core_insert --num 100000 --dburl postgresql://scott:tiger@localhost/test
# 1.3
test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 5.229326 sec
# 1.4
test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 0.944007 secDie Unterstützung für die „batch“-Erweiterung wurde in Version 1.2 in Unterstützung für Batch-Modus / Fast Execution Helpers hinzugefügt und in 1.3 in #4623 um die Unterstützung für die execute_values-Erweiterung erweitert. In 1.4 wird die execute_values-Erweiterung nun standardmäßig für INSERT-Anweisungen aktiviert; die „batch“-Erweiterung für UPDATE und DELETE bleibt standardmäßig deaktiviert.
Darüber hinaus unterstützt die Erweiterungsfunktion execute_values die Rückgabe der von RETURNING generierten Zeilen als aggregierte Liste. Der psycopg2-Dialekt ruft nun diese Liste ab, wenn die gegebene insert()-Konstruktion über die Methode Insert.returning() oder ähnliche Methoden, die zur Rückgabe generierter Standardwerte dienen, eine Rückgabe anfordert; die Zeilen werden dann in das Ergebnis installiert, sodass sie so abgerufen werden, als kämen sie direkt vom Cursor. Dies ermöglicht es Tools wie der ORM, Batched Inserts in allen Fällen zu verwenden, was voraussichtlich eine dramatische Leistungsverbesserung bringt.
Das Feature executemany_mode des psycopg2-Dialekts wurde mit folgenden Änderungen überarbeitet
Ein neuer Modus
"values_only"wurde hinzugefügt. Dieser Modus verwendet die sehr performantepsycopg2.extras.execute_values()-Erweiterungsmethode für kompilierte INSERT-Anweisungen, die mit executemany() ausgeführt werden, aber nichtexecute_batch()für UPDATE- und DELETE-Anweisungen verwendet. Dieser neue Modus ist nun die Standardeinstellung für den psycopg2-Dialekt.Der bestehende Modus
"values"heißt jetzt"values_plus_batch". Dieser Modus verwendetexecute_valuesfür INSERT-Anweisungen undexecute_batchfür UPDATE- und DELETE-Anweisungen. Der Modus ist nicht standardmäßig aktiviert, da er die korrekte Funktion voncursor.rowcountbei UPDATE- und DELETE-Anweisungen, die mitexecutemany()ausgeführt werden, deaktiviert.RETURNING-Unterstützung ist für
"values_only"und"values"für INSERT-Anweisungen aktiviert. Der psycopg2-Dialekt ruft die Zeilen von psycopg2 mit dem Flag fetch=True ab und installiert sie im Ergebnissatz, als kämen sie direkt vom Cursor (was sie letztendlich taten, jedoch hat die Erweiterungsfunktion von psycopg2 mehrere Batches zu einer Liste aggregiert).Die Standardeinstellung für
page_sizefürexecute_valueswurde von 100 auf 1000 erhöht. Die Standardeinstellung für die Funktionexecute_batchbleibt 100. Diese Parameter können beide wie bisher modifiziert werden.Das Flag
use_batch_mode, das Teil der Version 1.2 des Features war, wurde entfernt; das Verhalten ist weiterhin über das Flagexecutemany_modesteuerbar, das in 1.3 hinzugefügt wurde.Die Core Engine und der Dialekt wurden erweitert, um den executemany Plus returning Modus zu unterstützen, der derzeit nur mit psycopg2 verfügbar ist, indem neue Accessoren
CursorResult.inserted_primary_key_rowsundCursorResult.returned_default_rowsbereitgestellt werden.
Siehe auch
„join rewriting“-Logik aus dem SQLite-Dialekt entfernt; Imports aktualisiert¶
Die Unterstützung für die Umformulierung von Right-Nested-Joins wurde fallen gelassen, um alte SQLite-Versionen vor 3.7.16 (veröffentlicht 2013) zu unterstützen. Es wird nicht erwartet, dass moderne Python-Versionen von dieser Einschränkung abhängen.
Das Verhalten wurde erstmals in 0.9 eingeführt und war Teil der größeren Änderung, die Right-Nested-Joins ermöglichte, wie unter Viele JOIN- und LEFT OUTER JOIN-Ausdrücke werden nicht mehr in (SELECT * FROM ..) AS ANON_1 eingeschlossen beschrieben. Die SQLite-Umgehung führte jedoch in den Jahren 2013-2014 zu vielen Regressionen aufgrund ihrer Komplexität. Im Jahr 2016 wurde der Dialekt so modifiziert, dass die Join-Rewriting-Logik nur für SQLite-Versionen vor 3.7.16 auftrat, nachdem Bisektion verwendet wurde, um zu identifizieren, wo SQLite seine Unterstützung für diese Konstruktion behoben hatte, und es wurden keine weiteren Probleme mit dem Verhalten gemeldet (obwohl einige Fehler intern gefunden wurden). Es wird nun davon ausgegangen, dass es nur wenige bis keine Python-Builds für Python 2.7 oder 3.5 und höher (die unterstützten Python-Versionen) gibt, die eine SQLite-Version vor 3.7.17 enthalten würden, und das Verhalten ist nur in komplexeren ORM-Join-Szenarien erforderlich. Es wird nun eine Warnung ausgegeben, wenn die installierte SQLite-Version älter als 3.7.16 ist.
In verwandten Änderungen versuchen die Modulimporte für SQLite unter Python 3 nicht mehr, den Treiber „pysqlite2“ zu importieren, da dieser Treiber unter Python 3 nicht existiert; eine sehr alte Warnung für alte pysqlite2-Versionen wird ebenfalls entfernt.
Sequenzunterstützung für MariaDB 10.3 hinzugefügt¶
Die MariaDB-Datenbank unterstützt ab 10.3 Sequenzen. Der MySQL-Dialekt von SQLAlchemy implementiert nun die Unterstützung für das Sequence-Objekt für diese Datenbank, was bedeutet, dass „CREATE SEQUENCE“-DDL für eine Sequence ausgegeben wird, die in einer Table oder MetaData-Sammlung vorhanden ist, auf die gleiche Weise wie bei Backends wie PostgreSQL oder Oracle, wenn die Serverseitenversionsprüfung des Dialekts bestätigt hat, dass die Datenbank MariaDB 10.3 oder neuer ist. Zusätzlich fungiert die Sequence als Spaltendefault und Primärschlüssel-Generierungsobjekt, wenn sie auf diese Weise verwendet wird.
Da diese Änderung die Annahmen sowohl für DDL als auch für das Verhalten von INSERT-Anweisungen für eine Anwendung beeinflusst, die derzeit gegen MariaDB 10.3 bereitgestellt wird und auch explizit die Sequence-Konstruktion in ihren Tabellendefinitionen verwendet, ist es wichtig zu beachten, dass Sequence ein Flag Sequence.optional unterstützt, das verwendet wird, um die Szenarien einzuschränken, in denen die Sequence wirksam wird. Wenn „optional“ auf eine Sequence angewendet wird, die sich in der ganzzahligen Primärschlüsselspalte einer Tabelle befindet.
Table(
"some_table",
metadata,
Column(
"id", Integer, Sequence("some_seq", start=1, optional=True), primary_key=True
),
)Die obige Sequence wird nur für DDL- und INSERT-Anweisungen verwendet, wenn die Ziel-Datenbank keine andere Möglichkeit zur Generierung von ganzzahligen Primärschlüsselwerten für die Spalte unterstützt. Das heißt, die obige Oracle-Datenbank würde die Sequenz verwenden, jedoch würden die PostgreSQL- und MariaDB 10.3-Datenbanken dies nicht tun. Dies kann für eine bestehende Anwendung wichtig sein, die auf SQLAlchemy 1.4 aktualisiert und möglicherweise noch keine DDL für diese Sequence gegen ihre zugrunde liegende Datenbank ausgegeben hat, da eine INSERT-Anweisung fehlschlägt, wenn sie versucht, eine nicht erstellte Sequenz zu verwenden.
Siehe auch
Sequenzunterstützung, getrennt von IDENTITY, für SQL Server hinzugefügt¶
Die Sequence-Konstruktion ist nun vollständig funktionsfähig mit Microsoft SQL Server. Wenn sie auf eine Column angewendet wird, enthält die DDL für die Tabelle keine IDENTITY-Schlüsselwörter mehr und verlässt sich stattdessen auf „CREATE SEQUENCE“, um sicherzustellen, dass eine Sequenz vorhanden ist, die dann für INSERT-Anweisungen auf der Tabelle verwendet wird.
Die Sequence vor Version 1.3 wurde verwendet, um Parameter für die IDENTITY-Spalte in SQL Server zu steuern; diese Verwendung löste während 1.3 Deprecation-Warnungen aus und wurde nun in 1.4 entfernt. Für die Steuerung von Parametern einer IDENTITY-Spalte sollten die Parameter mssql_identity_start und mssql_identity_increment verwendet werden; siehe die unten verlinkte Dokumentation des MSSQL-Dialekts.
Siehe auch
Die Designs von flambé! dem Drachen und Der Alchemist wurden von Rotem Yaari erstellt und großzügig gespendet.
Erstellt mit Sphinx 7.2.6. Dokumentation zuletzt generiert: Di 11 Mär 2025 14:40:17 EDT