SQLAlchemy 2.0 - Haupt-Migrationsanleitung

Hinweis für Leser

Die Übergangsdokumente für SQLAlchemy 2.0 sind in zwei Dokumente aufgeteilt – eines, das wichtige API-Änderungen von der 1.x zur 2.x Serie beschreibt, und das andere, das neue Funktionen und Verhaltensweisen im Vergleich zu SQLAlchemy 1.4 beschreibt.

Leser, die ihre 1.4 Anwendung bereits an die SQLAlchemy 2.0 Engine- und ORM-Konventionen angepasst haben, können zu Was ist neu in SQLAlchemy 2.0? navigieren, um einen Überblick über neue Funktionen und Möglichkeiten zu erhalten.

Über dieses Dokument

Dieses Dokument beschreibt die Änderungen zwischen SQLAlchemy Version 1.4 und SQLAlchemy Version 2.0.

SQLAlchemy 2.0 stellt eine große Veränderung für eine Vielzahl von wichtigen SQLAlchemy-Nutzungsmustern sowohl in den Core- als auch in den ORM-Komponenten dar. Das Ziel dieser Veröffentlichung ist es, einige der grundlegendsten Annahmen von SQLAlchemy seit seinen Anfängen leicht anzupassen und ein neu gestrafftes Nutzungsmodell zu liefern, das hoffentlich deutlich minimalistischer und konsistenter zwischen den Core- und ORM-Komponenten sowie leistungsfähiger ist. Der Übergang zu Python nur noch Python 3 sowie das Aufkommen von schrittweisen Typsystemen für Python 3 sind die anfänglichen Inspirationen für diese Veränderung, ebenso wie die sich ändernde Natur der Python-Community, die heute nicht nur hartgesottene Datenbankprogrammierer, sondern auch eine riesige neue Gemeinschaft von Datenwissenschaftlern und Studenten aus vielen verschiedenen Disziplinen umfasst.

SQLAlchemy wurde mit Python 2.3 gestartet, das keine Kontextmanager, keine Funktionsdekore, Unicode als Nebenfunktion und eine Vielzahl anderer Mängel hatte, die heute unbekannt wären. Die größten Änderungen in SQLAlchemy 2.0 zielen auf die verbliebenen Annahmen aus dieser frühen Phase der SQLAlchemy-Entwicklung ab, sowie auf die Überbleibsel, die aus der inkrementellen Einführung wichtiger API-Funktionen wie Query und Declarative resultieren. Es hofft auch, neuere Funktionen zu standardisieren, die sich als sehr effektiv erwiesen haben.

Der 1.4->2.0 Migrationspfad

Die auffälligsten architektonischen Merkmale und API-Änderungen, die als „SQLAlchemy 2.0“ gelten, wurden tatsächlich als vollständig verfügbar in der 1.4-Serie veröffentlicht, um einen sauberen Upgrade-Pfad von der 1.x- zur 2.x-Serie zu ermöglichen und als Beta-Plattform für die Funktionen selbst zu dienen. Diese Änderungen umfassen

Die obigen Punkte verlinken zur Beschreibung dieser neuen Paradigmen, wie sie in SQLAlchemy 1.4 eingeführt wurden, im Dokument Was ist neu in SQLAlchemy 1.4?.

Für SQLAlchemy 2.0 sind alle API-Funktionen und Verhaltensweisen, die als für 2.0 veraltet markiert waren, nun endgültig; insbesondere sind wichtige APIs, die nicht mehr vorhanden sind

Die obigen Punkte beziehen sich auf die auffälligsten vollständig rückwärtskompatiblen Änderungen, die in der 2.0-Veröffentlichung finalisiert sind. Der Migrationspfad für Anwendungen, um diese und andere Änderungen zu berücksichtigen, ist als Übergangspfad zunächst in die 1.4-Serie von SQLAlchemy gestaltet, wo die „future“-APIs verfügbar sind, um die „2.0“-Arbeitsweise zu ermöglichen, und dann in die 2.0-Serie, wo die oben genannten und andere nicht mehr verwendete APIs entfernt werden.

Die vollständigen Schritte für diesen Migrationspfad finden Sie später in diesem Dokument unter 1.x -> 2.x Migrationsübersicht.

1.x -> 2.x Migrationsübersicht

Der Übergang zu SQLAlchemy 2.0 präsentiert sich in der SQLAlchemy 1.4-Veröffentlichung als eine Reihe von Schritten, die es ermöglichen, eine Anwendung jeder Größe und Komplexität schrittweise und iterativ zu SQLAlchemy 2.0 zu migrieren. Lektionen aus dem Übergang von Python 2 zu Python 3 haben ein System inspiriert, das in größtmöglichem Umfang keine „breaking“-Änderungen erfordert, oder keine Änderung, die universell oder gar nicht vorgenommen werden muss.

Als Mittel zur Überprüfung der 2.0-Architektur und zur Ermöglichung einer vollständig iterativen Übergangsumgebung sind der gesamte Umfang der neuen APIs und Funktionen von 2.0 in der 1.4-Serie vorhanden und verfügbar; dazu gehören wichtige neue Funktionsbereiche wie das SQL-Caching-System, das neue ORM-Statement-Ausführungsmodell, neue transaktionale Paradigmen für ORM und Core, ein neues ORM-Deklarationssystem, das klassisches und deklaratives Mapping vereinheitlicht, Unterstützung für Python-Datenklassen und asyncio-Unterstützung für Core und ORM.

Die Schritte zur Erreichung der 2.0-Migration sind in den folgenden Unterabschnitten aufgeführt; insgesamt ist die allgemeine Strategie, dass eine Anwendung, die auf 1.4 mit allen aktivierten Warnungs-Flags läuft und keine 2.0-Deprecationswarnungen ausgibt, nun weitgehend mit SQLAlchemy 2.0 kompatibel ist. Bitte beachten Sie, dass es zusätzliche API- und Verhaltensänderungen geben kann, die sich beim Ausführen gegen SQLAlchemy 2.0 anders verhalten können; testen Sie Code immer gegen eine tatsächliche SQLAlchemy 2.0-Veröffentlichung als letzten Schritt der Migration.

Erste Voraussetzung, Schritt eins - Eine funktionierende 1.3 Anwendung

Der erste Schritt besteht darin, eine bestehende Anwendung auf 1.4 zu bringen. Im Falle einer typischen, nicht trivialen Anwendung ist es zunächst wichtig, sicherzustellen, dass sie unter SQLAlchemy 1.3 ohne Deprecationswarnungen läuft. Release 1.4 enthält einige Änderungen, die sich auf Bedingungen beziehen, die in früheren Versionen gewarnt haben, darunter einige Warnungen, die in 1.3 eingeführt wurden, insbesondere einige Änderungen am Verhalten der Flags relationship.viewonly und relationship.sync_backref.

Für beste Ergebnisse sollte die Anwendung mit der neuesten SQLAlchemy 1.3-Version fehlerfrei laufen oder alle ihre Tests bestehen, ohne SQLAlchemy Deprecationswarnungen; dies sind Warnungen, die für die Klasse SADeprecationWarning ausgegeben werden.

Erste Voraussetzung, Schritt zwei - Eine funktionierende 1.4 Anwendung

Sobald die Anwendung unter SQLAlchemy 1.3 einwandfrei funktioniert, besteht der nächste Schritt darin, sie unter SQLAlchemy 1.4 zum Laufen zu bringen. In den allermeisten Fällen sollten Anwendungen ohne Probleme von SQLAlchemy 1.3 auf 1.4 laufen. Wie jedoch bei jeder 1.x-zu-1.y-Veröffentlichung gibt es immer wieder subtile oder in einigen Fällen etwas weniger subtile Änderungen an APIs und Verhaltensweisen, und das SQLAlchemy-Projekt erhält in den ersten Monaten immer eine gute Anzahl von Regressionsberichten.

Der 1.x->1.y Release-Prozess hat normalerweise einige eher dramatische Änderungen am Rande, die auf Anwendungsfällen basieren, die sehr selten oder gar nicht genutzt werden. Für 1.4 sind die als in diesem Bereich identifizierten Änderungen wie folgt:

Für den vollständigen Überblick über die Änderungen in SQLAlchemy 1.4 siehe das Dokument Was ist neu in SQLAlchemy 1.4?.

Migration zu 2.0 Schritt Eins - Nur Python 3 (Python 3.7 Minimum für 2.0 Kompatibilität)

SQLAlchemy 2.0 wurde zuerst durch die Tatsache inspiriert, dass das EOL von Python 2 im Jahr 2020 war. SQLAlchemy nimmt sich mehr Zeit als andere wichtige Projekte, um die Unterstützung für Python 2.7 einzustellen. Um jedoch SQLAlchemy 2.0 verwenden zu können, muss die Anwendung mindestens unter Python 3.7 lauffähig sein. SQLAlchemy 1.4 unterstützt Python 3.6 oder neuer innerhalb der Python 3-Serie; während der gesamten 1.4-Serie kann die Anwendung unter Python 2.7 oder mindestens Python 3.6 laufen. Version 2.0 startet jedoch bei Python 3.7.

Migration zu 2.0 Schritt Zwei - Aktivieren von RemovedIn20Warnings

SQLAlchemy 1.4 verfügt über ein bedingtes Warnsystem für Deprecations, inspiriert vom Python „-3“-Flag, das Legacy-Muster in einer laufenden Anwendung anzeigt. Für SQLAlchemy 1.4 wird die Deprecationsklasse RemovedIn20Warning nur ausgegeben, wenn eine Umgebungsvariable SQLALCHEMY_WARN_20 auf entweder true oder 1 gesetzt ist.

Angesichts des folgenden Beispielprogramms

from sqlalchemy import column
from sqlalchemy import create_engine
from sqlalchemy import select
from sqlalchemy import table


engine = create_engine("sqlite://")

engine.execute("CREATE TABLE foo (id integer)")
engine.execute("INSERT INTO foo (id) VALUES (1)")


foo = table("foo", column("id"))
result = engine.execute(select([foo.c.id]))

print(result.fetchall())

Das obige Programm verwendet mehrere Muster, die viele Benutzer bereits als „Legacy“ erkennen werden, nämlich die Verwendung der Methode Engine.execute(), die Teil der API für „verbindungsloses Ausführen“ ist. Wenn wir das obige Programm gegen 1.4 ausführen, gibt es eine einzige Zeile aus:

$ python test3.py
[(1,)]

Um „2.0 Deprecations Modus“ zu aktivieren, aktivieren wir die Variable SQLALCHEMY_WARN_20=1 und stellen außerdem sicher, dass ein Warnungsfilter ausgewählt ist, der keine Warnungen unterdrückt.

SQLALCHEMY_WARN_20=1 python -W always::DeprecationWarning test3.py

Da der gemeldete Warnungsort nicht immer an der richtigen Stelle liegt, kann die Identifizierung des fehlerhaften Codes ohne den vollständigen Stacktrace schwierig sein. Dies kann erreicht werden, indem die Warnungen in Ausnahmen umgewandelt werden, indem der Warnungsfilter error angegeben wird, wobei die Python-Option -W error::DeprecationWarning verwendet wird.

Mit aktivierten Warnungen hat unser Programm nun viel zu sagen

$ SQLALCHEMY_WARN_20=1 python -W always::DeprecationWarning test3.py
test3.py:9: RemovedIn20Warning: The Engine.execute() function/method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  engine.execute("CREATE TABLE foo (id integer)")
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:2856: RemovedIn20Warning: Passing a string to Connection.execute() is deprecated and will be removed in version 2.0.  Use the text() construct, or the Connection.exec_driver_sql() method to invoke a driver-level SQL string. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  return connection.execute(statement, *multiparams, **params)
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:1639: RemovedIn20Warning: The current statement is being autocommitted using implicit autocommit.Implicit autocommit will be removed in SQLAlchemy 2.0.   Use the .begin() method of Engine or Connection in order to use an explicit transaction for DML and DDL statements. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  self._commit_impl(autocommit=True)
test3.py:10: RemovedIn20Warning: The Engine.execute() function/method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  engine.execute("INSERT INTO foo (id) VALUES (1)")
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:2856: RemovedIn20Warning: Passing a string to Connection.execute() is deprecated and will be removed in version 2.0.  Use the text() construct, or the Connection.exec_driver_sql() method to invoke a driver-level SQL string. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  return connection.execute(statement, *multiparams, **params)
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:1639: RemovedIn20Warning: The current statement is being autocommitted using implicit autocommit.Implicit autocommit will be removed in SQLAlchemy 2.0.   Use the .begin() method of Engine or Connection in order to use an explicit transaction for DML and DDL statements. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  self._commit_impl(autocommit=True)
/home/classic/dev/sqlalchemy/lib/sqlalchemy/sql/selectable.py:4271: RemovedIn20Warning: The legacy calling style of select() is deprecated and will be removed in SQLAlchemy 2.0.  Please use the new calling style described at select(). (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  return cls.create_legacy_select(*args, **kw)
test3.py:14: RemovedIn20Warning: The Engine.execute() function/method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  result = engine.execute(select([foo.c.id]))
[(1,)]

Mit der obigen Anleitung können wir unser Programm auf 2.0-Stile migrieren, und als Bonus ist unser Programm viel klarer

from sqlalchemy import column
from sqlalchemy import create_engine
from sqlalchemy import select
from sqlalchemy import table
from sqlalchemy import text


engine = create_engine("sqlite://")

# don't rely on autocommit for DML and DDL
with engine.begin() as connection:
    # use connection.execute(), not engine.execute()
    # use the text() construct to execute textual SQL
    connection.execute(text("CREATE TABLE foo (id integer)"))
    connection.execute(text("INSERT INTO foo (id) VALUES (1)"))


foo = table("foo", column("id"))

with engine.connect() as connection:
    # use connection.execute(), not engine.execute()
    # select() now accepts column / table expressions positionally
    result = connection.execute(select(foo.c.id))

    print(result.fetchall())

Das Ziel des „2.0 Deprecations Modus“ ist, dass ein Programm, das ohne RemovedIn20Warning-Warnungen mit aktiviertem „2.0 Deprecations Modus“ läuft, dann bereit ist, in SQLAlchemy 2.0 ausgeführt zu werden.

Migration zu 2.0 Schritt Drei - Auflösen aller RemovedIn20Warnings

Der Code kann iterativ entwickelt werden, um diese Warnungen zu beheben. Innerhalb des SQLAlchemy-Projekts wird der folgende Ansatz verfolgt:

  1. aktivieren Sie die Umgebungsvariable SQLALCHEMY_WARN_20=1 in der Testsuite; für SQLAlchemy ist dies in der Datei tox.ini

  2. Konfigurieren Sie im Setup für die Testsuite eine Reihe von Warnungsfiltern, die bestimmte Teilmengen von Warnungen auswählen, um entweder eine Ausnahme auszulösen oder ignoriert (oder protokolliert) zu werden. Arbeiten Sie nur mit einer Untergruppe von Warnungen gleichzeitig. Im Folgenden ist ein Warnungsfilter für eine Anwendung konfiguriert, bei der die Änderung an den .execute()-Aufrufen auf Core-Ebene erforderlich sein wird, damit alle Tests erfolgreich sind, während alle anderen 2.0-Stil-Warnungen unterdrückt werden.

    import warnings
    from sqlalchemy import exc
    
    # for warnings not included in regex-based filter below, just log
    warnings.filterwarnings("always", category=exc.RemovedIn20Warning)
    
    # for warnings related to execute() / scalar(), raise
    for msg in [
        r"The (?:Executable|Engine)\.(?:execute|scalar)\(\) function",
        r"The current statement is being autocommitted using implicit autocommit,",
        r"The connection.execute\(\) method in SQLAlchemy 2.0 will accept "
        "parameters as a single dictionary or a single sequence of "
        "dictionaries only.",
        r"The Connection.connect\(\) function/method is considered legacy",
        r".*DefaultGenerator.execute\(\)",
    ]:
        warnings.filterwarnings(
            "error",
            message=msg,
            category=exc.RemovedIn20Warning,
        )
  3. Nachdem jede Unterkategorie von Warnungen in der Anwendung behoben wurde, können neue Warnungen, die vom „immer“-Filter abgefangen werden, zur Liste der zu behebenden „Fehler“ hinzugefügt werden.

  4. Sobald keine Warnungen mehr ausgegeben werden, kann der Filter entfernt werden.

Migration zu 2.0 Schritt Vier - Verwenden Sie das future Flag auf Engine

Das Engine-Objekt verfügt in Version 2.0 über eine aktualisierte API auf Transaktionsebene. In 1.4 ist diese neue API verfügbar, indem das Flag future=True an die Funktion create_engine() übergeben wird.

Wenn das Flag create_engine.future verwendet wird, unterstützen die Objekte Engine und Connection die 2.0 API vollständig und keine Legacy-Funktionen mehr, einschließlich des neuen Argumentformats für Connection.execute(), der Entfernung von „implizitem Autocommit“, Zeichenfolgenanweisungen erfordern das text()-Konstrukt, es sei denn, die Methode Connection.exec_driver_sql() wird verwendet, und verbindungsloses Ausführen von der Engine ist entfernt.

Wenn alle RemovedIn20Warning-Warnungen bezüglich der Verwendung von Engine und Connection behoben wurden, dann kann das Flag create_engine.future aktiviert werden und es sollten keine Fehler ausgelöst werden.

Die neue Engine wird unter Engine beschrieben, die ein neues Connection-Objekt liefert. Zusätzlich zu den oben genannten Änderungen verfügt das Connection-Objekt über die Methoden Connection.commit() und Connection.rollback() zur Unterstützung des neuen Betriebsmodus „commit-as-you-go“.

from sqlalchemy import create_engine

engine = create_engine("postgresql+psycopg2:///")

with engine.connect() as conn:
    conn.execute(text("insert into table (x) values (:some_x)"), {"some_x": 10})

    conn.commit()  # commit as you go

Migration zu 2.0 Schritt Fünf - Verwenden Sie das future Flag auf Session

Das Session-Objekt verfügt ebenfalls über eine aktualisierte API auf Transaktions-/Verbindungsebene in Version 2.0. Diese API ist in 1.4 über das Flag Session.future auf Session oder auf sessionmaker verfügbar.

Das Session-Objekt unterstützt den „future“-Modus und beinhaltet diese Änderungen:

  1. Die Session unterstützt keine „gebundenen Metadaten“ mehr, wenn sie die zu verwendende Engine für die Konnektivität auflöst. Dies bedeutet, dass ein Engine-Objekt muss dem Konstruktor übergeben werden (dies kann entweder ein Legacy- oder ein Future-Stil-Objekt sein).

  2. Das Flag Session.begin.subtransactions wird nicht mehr unterstützt.

  3. Die Methode Session.commit() sendet immer ein COMMIT an die Datenbank, anstatt zu versuchen, „Subtransaktionen“ abzustimmen.

  4. Die Methode Session.rollback() macht immer den gesamten Transaktionsstapel auf einmal rückgängig, anstatt zu versuchen, „Subtransaktionen“ beizubehalten.

Die Session unterstützt auch flexiblere Erstellungsmuster in 1.4, die nun eng mit den Mustern übereinstimmen, die vom Connection-Objekt verwendet werden. Highlights sind, dass die Session als Kontextmanager verwendet werden kann

from sqlalchemy.orm import Session

with Session(engine) as session:
    session.add(MyObject())
    session.commit()

Darüber hinaus unterstützt das Objekt sessionmaker einen Kontextmanager sessionmaker.begin(), der eine Session erstellt und eine Transaktion in einem Block beginnt/committet.

from sqlalchemy.orm import sessionmaker

Session = sessionmaker(engine)

with Session.begin() as session:
    session.add(MyObject())

Siehe den Abschnitt Session-Level vs. Engine-Level Transaktionssteuerung für einen Vergleich der Erstellungsmuster von Session mit denen von Connection.

Sobald die Anwendung alle Tests besteht / mit SQLALCHEMY_WARN_20=1 und allen exc.RemovedIn20Warning-Vorkommen, die auf Fehler gesetzt sind, läuft, ist die Anwendung bereit!.

Die folgenden Abschnitte beschreiben die spezifischen Änderungen für alle wichtigen API-Modifikationen.

Migration zu 2.0 Schritt Sechs - Hinzufügen von __allow_unmapped__ zu explizit typisierten ORM-Modellen

SQLAlchemy 2.0 hat neue Unterstützung für die Laufzeitinterpretation von PEP 484-Typanmerkungen für ORM-Modelle. Eine Voraussetzung für diese Anmerkungen ist, dass sie das generische Container Mapped verwenden müssen. Anmerkungen, die Mapped nicht verwenden und sich auf Konstrukte wie relationship() beziehen, lösen Fehler in Python aus, da sie auf Fehlkonfigurationen hindeuten.

SQLAlchemy-Anwendungen, die das Mypy-Plugin mit expliziten Anmerkungen verwenden, die Mapped nicht in ihren Anmerkungen verwenden, unterliegen diesen Fehlern, wie im folgenden Beispiel gezeigt:

Base = declarative_base()


class Foo(Base):
    __tablename__ = "foo"

    id: int = Column(Integer, primary_key=True)

    # will raise
    bars: List["Bar"] = relationship("Bar", back_populates="foo")


class Bar(Base):
    __tablename__ = "bar"

    id: int = Column(Integer, primary_key=True)
    foo_id = Column(ForeignKey("foo.id"))

    # will raise
    foo: Foo = relationship(Foo, back_populates="bars", cascade="all")

Oben lösen die relationship()-Deklarationen Foo.bars und Bar.foo beim Erstellen der Klasse einen Fehler aus, da sie Mapped nicht verwenden (im Gegensatz dazu werden die Anmerkungen, die Column verwenden, von 2.0 ignoriert, da diese als Legacy-Konfigurationsstil erkannt werden können). Um alle Anmerkungen, die Mapped nicht verwenden, fehlerfrei durchlaufen zu lassen, kann das Attribut __allow_unmapped__ auf der Klasse oder einer Unterklasse verwendet werden, was dazu führt, dass die Anmerkungen in diesen Fällen vom neuen deklarativen System vollständig ignoriert werden.

Hinweis

Die Direktive __allow_unmapped__ gilt nur für das Laufzeitverhalten des ORM. Sie hat keinen Einfluss auf das Verhalten von Mypy, und die obige Zuordnung erfordert weiterhin die Installation des Mypy-Plugins. Für vollständig 2.0-konforme ORM-Modelle, die unter Mypy ohne Plugin korrekt typisiert werden, folgen Sie den Migrationsschritten unter Migration einer bestehenden Zuordnung.

Das folgende Beispiel veranschaulicht die Anwendung von __allow_unmapped__ auf die deklarative Klasse Base, wo sie für alle von Base abstammenden Klassen wirksam wird.

# qualify the base with __allow_unmapped__.  Can also be
# applied to classes directly if preferred
class Base:
    __allow_unmapped__ = True


Base = declarative_base(cls=Base)


# existing mapping proceeds, Declarative will ignore any annotations
# which don't include ``Mapped[]``
class Foo(Base):
    __tablename__ = "foo"

    id: int = Column(Integer, primary_key=True)

    bars: List["Bar"] = relationship("Bar", back_populates="foo")


class Bar(Base):
    __tablename__ = "bar"

    id: int = Column(Integer, primary_key=True)
    foo_id = Column(ForeignKey("foo.id"))

    foo: Foo = relationship(Foo, back_populates="bars", cascade="all")

Geändert in Version 2.0.0beta3: - verbesserte Unterstützung für das Attribut __allow_unmapped__, um explizite, annotierte Beziehungen im 1.4-Stil, die Mapped nicht verwenden, weiterhin nutzbar zu machen.

Migration zu 2.0 Schritt Sieben - Testen gegen eine SQLAlchemy 2.0 Veröffentlichung

Wie bereits erwähnt, gibt es in SQLAlchemy 2.0 zusätzliche API- und Verhaltensänderungen, die abwärtskompatibel sein sollen, aber dennoch einige Inkompatibilitäten einführen können. Daher ist nach Abschluss des gesamten Portierungsprozesses der letzte Schritt, gegen die neueste Veröffentlichung von SQLAlchemy 2.0 zu testen, um verbleibende Probleme zu beheben.

Der Leitfaden unter Was ist neu in SQLAlchemy 2.0? bietet einen Überblick über neue Funktionen und Verhaltensweisen für SQLAlchemy 2.0, die über die Basissatz der 1.4->2.0 API-Änderungen hinausgehen.

2.0 Migration - Core Connection / Transaktion

Bibliotheksweites (aber nicht treiberweites) „Autocommit“ aus Core und ORM entfernt

Synopsis

In SQLAlchemy 1.x committen die folgenden Anweisungen automatisch die zugrunde liegende DBAPI-Transaktion, in SQLAlchemy 2.0 geschieht dies jedoch nicht.

conn = engine.connect()

# won't autocommit in 2.0
conn.execute(some_table.insert().values(foo="bar"))

Auch dies wird kein Autocommit ausführen.

conn = engine.connect()

# won't autocommit in 2.0
conn.execute(text("INSERT INTO table (foo) VALUES ('bar')"))

Die gängige Umgehungslösung für benutzerdefinierte DML, die ein Commit erfordert, die „autocommit“-Ausführungsoption, wird entfernt.

conn = engine.connect()

# won't autocommit in 2.0
conn.execute(text("EXEC my_procedural_thing()").execution_options(autocommit=True))

Migration zu 2.0

Die Methode, die mit 1.x-Stil und 2.0-Stil-Ausführung kreuzkompatibel ist, ist die Verwendung der Methode Connection.begin() oder des Kontextmanagers Engine.begin().

with engine.begin() as conn:
    conn.execute(some_table.insert().values(foo="bar"))
    conn.execute(some_other_table.insert().values(bat="hoho"))

with engine.connect() as conn:
    with conn.begin():
        conn.execute(some_table.insert().values(foo="bar"))
        conn.execute(some_other_table.insert().values(bat="hoho"))

with engine.begin() as conn:
    conn.execute(text("EXEC my_procedural_thing()"))

Bei Verwendung des 2.0-Stils mit dem Flag create_engine.future kann auch der "Commit as you go"-Stil verwendet werden, da die Connection das **autobegin**-Verhalten aufweist, das auftritt, wenn eine Anweisung zum ersten Mal ohne expliziten Aufruf von Connection.begin() aufgerufen wird.

with engine.connect() as conn:
    conn.execute(some_table.insert().values(foo="bar"))
    conn.execute(some_other_table.insert().values(bat="hoho"))

    conn.commit()

Wenn der 2.0 Deprecations-Modus aktiviert ist, wird eine Warnung ausgegeben, wenn das veraltete "autocommit"-Feature auftritt, und weist auf Stellen hin, an denen eine explizite Transaktion vermerkt werden sollte.

Diskussion

Die ersten Veröffentlichungen von SQLAlchemy standen im Widerspruch zum Geist der Python DBAPI (PEP 249), da sie versuchte, die Betonung von PEP 249 auf "implizites Beginnen" und "explizites Committen" von Transaktionen zu verbergen. Fünfzehn Jahre später sehen wir nun, dass dies im Wesentlichen ein Fehler war, da die vielen Muster von SQLAlchemy, die versuchen, die Anwesenheit einer Transaktion zu "verbergen", zu einer komplexeren API führen, die inkonsistent funktioniert und besonders für Benutzer, die neu in relationalen Datenbanken und ACID-Transaktionen im Allgemeinen sind, äußerst verwirrend ist. SQLAlchemy 2.0 wird alle Versuche, Transaktionen implizit zu committen, abschaffen, und die Nutzungsmuster werden immer erfordern, dass der Benutzer den "Beginn" und das "Ende" einer Transaktion auf irgendeine Weise abgrenzt, so wie das Lesen oder Schreiben in eine Datei in Python einen "Anfang" und ein "Ende" hat.

Im Falle von Autocommit für eine reine Textanweisung gibt es tatsächlich einen regulären Ausdruck, der jede Anweisung parst, um Autocommit zu erkennen! Wenig überraschend ist dieser Regex ständig gescheitert, verschiedene Arten von Anweisungen und gespeicherten Prozeduren zu berücksichtigen, die einen "Schreibvorgang" auf der Datenbank implizieren, was zu ständiger Verwirrung führt, da einige Anweisungen Ergebnisse in der Datenbank produzieren und andere nicht. Indem der Benutzer vom transaktionalen Konzept ferngehalten wird, erhalten wir viele Fehlermeldungen hierzu, da Benutzer nicht verstehen, dass Datenbanken immer eine Transaktion verwenden, unabhängig davon, ob eine Schicht sie automatisch committet.

SQLAlchemy 2.0 wird erfordern, dass alle Datenbankaktionen auf jeder Ebene explizit sind, wie die Transaktion verwendet werden soll. Für die überwiegende Mehrheit der Core-Anwendungsfälle ist dies das bereits empfohlene Muster.

with engine.begin() as conn:
    conn.execute(some_table.insert().values(foo="bar"))

Für die Verwendung "Commit as you go, or rollback instead", die der Art und Weise ähnelt, wie die Session heute normalerweise verwendet wird, enthält die "future"-Version von Connection, die von einer mit dem Flag create_engine.future erstellten Engine zurückgegeben wird, neue Methoden Connection.commit() und Connection.rollback(), die auf eine Transaktion wirken, die nun automatisch begonnen wird, wenn eine Anweisung zum ersten Mal aufgerufen wird.

# 1.4 / 2.0 code

from sqlalchemy import create_engine

engine = create_engine(..., future=True)

with engine.connect() as conn:
    conn.execute(some_table.insert().values(foo="bar"))
    conn.commit()

    conn.execute(text("some other SQL"))
    conn.rollback()

Oben gibt die Methode engine.connect() eine Connection zurück, die **autobegin** aufweist. Das bedeutet, dass das Ereignis begin() ausgelöst wird, wenn die execute-Methode zum ersten Mal verwendet wird (beachten Sie jedoch, dass es kein tatsächliches "BEGIN" in der Python DBAPI gibt). "Autobegin" ist ein neues Muster in SQLAlchemy 1.4, das sowohl von der Connection als auch vom ORM Session-Objekt bereitgestellt wird. Autobegin ermöglicht es, dass die Methode Connection.begin() explizit aufgerufen werden kann, wenn das Objekt zum ersten Mal abgerufen wird, für Schemata, die den Beginn der Transaktion abgrenzen möchten, aber wenn die Methode nicht aufgerufen wird, tritt sie implizit auf, wenn zum ersten Mal Arbeit mit dem Objekt geleistet wird.

Die Entfernung von "autocommit" hängt eng mit der Entfernung der "verbindungslosen" Ausführung zusammen, die unter „Implizite“ und „verbindunglose“ Ausführung, „gebundene Metadaten“ entfernt diskutiert wird. All diese älteren Muster entstanden aus der Tatsache, dass Python keine Context Manager oder Decorators hatte, als SQLAlchemy erstmals erstellt wurde, sodass es keine praktischen idiomatischen Muster für die Abgrenzung der Nutzung einer Ressource gab.

Treiberseitiges Autocommit bleibt verfügbar

Echtes "Autocommit"-Verhalten ist nun mit den meisten DBAPI-Implementierungen weit verbreitet und wird von SQLAlchemy über den Parameter Connection.execution_options.isolation_level unterstützt, wie unter Einstellen von Transaktionsisolationsstufen, einschließlich DBAPI Autocommit erläutert. Echtes Autocommit wird als "Isolationsstufe" behandelt, sodass sich die Struktur des Anwendungscodes nicht ändert, wenn Autocommit verwendet wird. Der Connection.begin() Context Manager sowie Methoden wie Connection.commit() können weiterhin verwendet werden; sie sind einfach No-Ops auf der Treiberseite, wenn DBAPI-Autocommit aktiviert ist.

„Implizite“ und „verbindunglose“ Ausführung, „gebundene Metadaten“ entfernt

Synopsis

Die Möglichkeit, eine Engine mit einem MetaData-Objekt zu verknüpfen, das dann eine Reihe sogenannter "verbindungsloser" Ausführungsmuster verfügbar macht, wird entfernt.

from sqlalchemy import MetaData

metadata_obj = MetaData(bind=engine)  # no longer supported

metadata_obj.create_all()  # requires Engine or Connection

metadata_obj.reflect()  # requires Engine or Connection

t = Table("t", metadata_obj, autoload=True)  # use autoload_with=engine

result = engine.execute(t.select())  # no longer supported

result = t.select().execute()  # no longer supported

Migration zu 2.0

Für Schemabereiche ist die explizite Verwendung einer Engine oder Connection erforderlich. Die Engine kann immer noch direkt als Quelle für die Konnektivität für einen MetaData.create_all()-Vorgang oder einen Ladevorgang verwendet werden. Zum Ausführen von Anweisungen verfügt nur das Connection-Objekt über eine Methode Connection.execute() (zusätzlich zur ORM-Methode Session.execute()).

from sqlalchemy import MetaData

metadata_obj = MetaData()

# engine level:

# create tables
metadata_obj.create_all(engine)

# reflect all tables
metadata_obj.reflect(engine)

# reflect individual table
t = Table("t", metadata_obj, autoload_with=engine)


# connection level:


with engine.connect() as connection:
    # create tables, requires explicit begin and/or commit:
    with connection.begin():
        metadata_obj.create_all(connection)

    # reflect all tables
    metadata_obj.reflect(connection)

    # reflect individual table
    t = Table("t", metadata_obj, autoload_with=connection)

    # execute SQL statements
    result = connection.execute(t.select())

Diskussion

Die Core-Dokumentation hat sich bereits auf das gewünschte Muster hier standardisiert, sodass es wahrscheinlich ist, dass die meisten modernen Anwendungen ohnehin nicht viel ändern müssen. Es gibt jedoch wahrscheinlich viele Anwendungen, die immer noch auf Aufrufe von engine.execute() angewiesen sind, die angepasst werden müssen.

„Verbindungsloser“ Aufruf bezieht sich auf das immer noch recht beliebte Muster, .execute() von der Engine aufzurufen.

result = engine.execute(some_statement)

Der obige Vorgang ruft implizit ein Connection-Objekt ab und führt darauf die Methode .execute() aus. Obwohl dies wie eine einfache Komfortfunktion erscheint, hat es sich gezeigt, dass es zu mehreren Problemen führt.

  • Programme mit langen Zeichenketten von engine.execute()-Aufrufen sind weit verbreitet geworden und nutzen eine Funktion übermäßig, die für seltene Anwendungsfälle gedacht war, und führen zu ineffizienten nicht-transaktionalen Anwendungen. Neue Benutzer sind verwirrt über den Unterschied zwischen engine.execute() und connection.execute(), und die Nuance zwischen diesen beiden Ansätzen wird oft nicht verstanden.

  • Die Funktion basiert auf dem "Autocommit auf Anwendungsebene", um Sinn zu ergeben, das ebenfalls entfernt wird, da es auch ineffizient und irreführend ist.

  • Um Ergebnismengen zu verarbeiten, gibt Engine.execute ein Ergebnisobjekt mit nicht konsumierten Cursorergebnissen zurück. Dieser Cursor verknüpft sich notwendigerweise immer noch mit der DBAPI-Verbindung, die in einer offenen Transaktion verbleibt, und all dies wird freigegeben, sobald der Ergebnissatz die im Cursor wartenden Zeilen vollständig verbraucht hat. Das bedeutet, dass Engine.execute die von ihm verwalteten Verbindungsressourcen bei Abschluss des Aufrufs nicht tatsächlich schließt. Das "Autoclose"-Verhalten von SQLAlchemy ist gut abgestimmt, sodass Benutzer im Allgemeinen keine negativen Auswirkungen dieses Systems berichten. Dennoch bleibt es ein übermäßig implizites und ineffizientes System, das aus den frühesten SQLAlchemy-Versionen stammt.

Die Entfernung der "verbindungslosen" Ausführung führt dann zur Entfernung eines noch älteren Musters, nämlich der "impliziten, verbindungslosen" Ausführung.

result = some_statement.execute()

Das obige Muster weist alle Probleme der "verbindungslosen" Ausführung auf, zusätzlich beruht es auf dem "gebundene Metadaten"-Muster, das SQLAlchemy seit vielen Jahren zu de-emphasieren versucht. Dies war das allererste beworbene Nutzungsmodell von SQLAlchemy in Version 0.1, das fast sofort obsolet wurde, als das Connection-Objekt eingeführt wurde und später Python Context Manager ein besseres Muster für die Nutzung von Ressourcen innerhalb eines festen Geltungsbereichs boten.

Nachdem die implizite Ausführung entfernt wurde, hat auch "gebundene Metadaten" selbst keinen Zweck mehr in diesem System. In der modernen Nutzung sind "gebundene Metadaten" immer noch etwas praktisch für die Arbeit innerhalb von MetaData.create_all()-Aufrufen sowie mit Session-Objekten. Wenn diese Funktionen jedoch explizit eine Engine erhalten, ermöglicht dies ein klareres Anwendungsdesign.

Viele Wahlmöglichkeiten werden zu einer Wahl

Insgesamt wurden die oben genannten Ausführungsmuster in SQLAlchemys allererster 0.1-Version eingeführt, bevor das Connection-Objekt überhaupt existierte. Nach vielen Jahren der De-Emphasierung dieser Muster sind "implizite, verbindungslose" Ausführung und "gebundene Metadaten" nicht mehr so weit verbreitet, daher streben wir in 2.0 danach, die Anzahl der Optionen für die Ausführung einer Anweisung in Core von "vielen Wahlmöglichkeiten" zu reduzieren.

# many choices

# bound metadata?
metadata_obj = MetaData(engine)

# or not?
metadata_obj = MetaData()

# execute from engine?
result = engine.execute(stmt)

# or execute the statement itself (but only if you did
# "bound metadata" above, which means you can't get rid of "bound" if any
# part of your program uses this form)
result = stmt.execute()

# execute from connection, but it autocommits?
conn = engine.connect()
conn.execute(stmt)

# execute from connection, but autocommit isn't working, so use the special
# option?
conn.execution_options(autocommit=True).execute(stmt)

# or on the statement ?!
conn.execute(stmt.execution_options(autocommit=True))

# or execute from connection, and we use explicit transaction?
with conn.begin():
    conn.execute(stmt)

zu "einer Wahl", wobei mit "einer Wahl" "explizite Verbindung mit expliziter Transaktion" gemeint ist; es gibt immer noch ein paar Möglichkeiten, Transaktionsblöcke je nach Bedarf abzugrenzen. Die "eine Wahl" besteht darin, eine Connection zu beschaffen und dann die Transaktion explizit abzugrenzen, falls der Vorgang ein Schreibvorgang ist.

# one choice - work with explicit connection, explicit transaction
# (there remain a few variants on how to demarcate the transaction)

# "begin once" - one transaction only per checkout
with engine.begin() as conn:
    result = conn.execute(stmt)

# "commit as you go" - zero or more commits per checkout
with engine.connect() as conn:
    result = conn.execute(stmt)
    conn.commit()

# "commit as you go" but with a transaction block instead of autobegin
with engine.connect() as conn:
    with conn.begin():
        result = conn.execute(stmt)

execute()-Methode strenger, Execution-Optionen prominenter

Synopsis

Die Argumentmuster, die mit der Methode sqlalchemy.engine.Connection() execute in SQLAlchemy 2.0 verwendet werden können, sind stark vereinfacht und entfernen viele zuvor verfügbare Argumentmuster. Die neue API in der 1.4er-Serie wird unter sqlalchemy.engine.Connection() beschrieben. Die folgenden Beispiele veranschaulichen die Muster, die eine Änderung erfordern.

connection = engine.connect()

# direct string SQL not supported; use text() or exec_driver_sql() method
result = connection.execute("select * from table")

# positional parameters no longer supported, only named
# unless using exec_driver_sql()
result = connection.execute(table.insert(), ("x", "y", "z"))

# **kwargs no longer accepted, pass a single dictionary
result = connection.execute(table.insert(), x=10, y=5)

# multiple *args no longer accepted, pass a list
result = connection.execute(
    table.insert(), {"x": 10, "y": 5}, {"x": 15, "y": 12}, {"x": 9, "y": 8}
)

Migration zu 2.0

Die neue Methode Connection.execute() akzeptiert nun eine Teilmenge der Argumentstile, die von der 1.x Connection.execute()-Methode akzeptiert werden. Daher ist der folgende Code zwischen 1.x und 2.0 kompatibel.

connection = engine.connect()

from sqlalchemy import text

result = connection.execute(text("select * from table"))

# pass a single dictionary for single statement execution
result = connection.execute(table.insert(), {"x": 10, "y": 5})

# pass a list of dictionaries for executemany
result = connection.execute(
    table.insert(), [{"x": 10, "y": 5}, {"x": 15, "y": 12}, {"x": 9, "y": 8}]
)

Diskussion

Die Verwendung von *args und **kwargs wurde entfernt, sowohl um die Komplexität des Vermutens, welche Art von Argumenten an die Methode übergeben wurden, zu beseitigen, als auch um Platz für andere Optionen zu schaffen, nämlich das Connection.execute.execution_options-Dictionary, das nun für die Bereitstellung von Optionen pro Anweisung verfügbar ist. Die Methode wird auch so geändert, dass ihr Nutzungsmuster mit dem der Methode Session.execute() übereinstimmt, was in Version 2.0 eine deutlich prominentere API ist.

Die Entfernung von direktem String-SQL soll eine Inkonsistenz zwischen Connection.execute() und Session.execute() beheben, wobei im ersteren Fall der String roh an den Treiber übergeben wird und im letzteren Fall zuerst in ein text()-Konstrukt konvertiert wird. Durch die Zulassung nur von text() wird auch das akzeptierte Parameterformat auf "benannte" und nicht auf "positionelle" Parameter beschränkt. Schließlich wird die Verwendung von String-SQL aus Sicherheitsperspektive immer stärker unter die Lupe genommen, und das text()-Konstrukt stellt nun eine explizite Grenze zum Bereich des Text-SQL dar, wo auf unvertrauenswürdige Benutzereingaben geachtet werden muss.

Ergebniszeilen verhalten sich wie benannte Tupel

Synopsis

Version 1.4 führt ein völlig neues Ergebnisobjekt ein, das wiederum Row-Objekte zurückgibt, die sich bei Verwendung des "future"-Modus wie benannte Tupel verhalten.

engine = create_engine(..., future=True)  # using future mode

with engine.connect() as conn:
    result = conn.execute(text("select x, y from table"))

    row = result.first()  # suppose the row is (1, 2)

    "x" in row  # evaluates to False, in 1.x / future=False, this would be True

    1 in row  # evaluates to True, in 1.x / future=False, this would be False

Migration zu 2.0

Anwendungscode oder Test-Suiten, die auf die Anwesenheit eines bestimmten Schlüssels in einer Zeile prüfen, müssten stattdessen die Sammlung row.keys() prüfen. Dies ist jedoch ein ungewöhnlicher Anwendungsfall, da eine Ergebniszeile typischerweise von Code verwendet wird, der bereits weiß, welche Spalten darin enthalten sind.

Diskussion

Die bereits in 1.4 vorhandene Klasse KeyedTuple, die beim Auswählen von Zeilen aus dem Query-Objekt verwendet wurde, wurde durch die Klasse Row ersetzt. Dies ist die Basis derselben Row, die mit Core-Anweisungsergebnissen zurückgegeben wird, wenn das Flag create_engine.future mit Engine verwendet wird (wenn das Flag create_engine.future nicht gesetzt ist, verwenden Core-Ergebnissätze die Unterklasse LegacyRow, die rückwärtskompatible Verhaltensweisen für die Methode __contains__() beibehält; ORM verwendet ausschließlich die Klasse Row direkt).

Diese Row verhält sich wie ein benanntes Tupel, insofern sie als Sequenz fungiert, aber auch Attributnamenzugriff unterstützt, z. B. row.some_column. Sie bietet jedoch auch das vorherige "Mapping"-Verhalten über das spezielle Attribut row._mapping, das ein Python-Mapping erzeugt, sodass schlüsselbasierter Zugriff wie row["some_column"] verwendet werden kann.

Um Ergebnisse direkt als Mappings zu erhalten, kann der Modifier mappings() des Ergebnisses verwendet werden.

from sqlalchemy.future.orm import Session

session = Session(some_engine)

result = session.execute(stmt)
for row in result.mappings():
    print("the user is: %s" % row["User"])

Die von der ORM verwendete Klasse Row unterstützt auch den Zugriff über Entität oder Attribut.

from sqlalchemy.future import select

stmt = select(User, Address).join(User.addresses)

for row in session.execute(stmt).mappings():
    print("the user is: %s  the address is: %s" % (row[User], row[Address]))

Migration 2.0 - Core-Nutzung

select() akzeptiert keine variablen Konstruktorargumente mehr, Spalten werden positionell übergeben

Synopsis

Das Konstrukt select() sowie die zugehörige Methode FromClause.select() akzeptieren keine Schlüsselwortargumente mehr, um Elemente wie die WHERE-Klausel, die FROM-Liste und ORDER BY zu erstellen. Die Liste der Spalten kann nun positionell anstelle einer Liste übergeben werden. Zusätzlich akzeptiert das Konstrukt case() seine WHEN-Kriterien nun positionell anstelle einer Liste.

# select_from / order_by keywords no longer supported
stmt = select([1], select_from=table, order_by=table.c.id)

# whereclause parameter no longer supported
stmt = select([table.c.x], table.c.id == 5)

# whereclause parameter no longer supported
stmt = table.select(table.c.id == 5)

# list emits a deprecation warning
stmt = select([table.c.x, table.c.y])

# list emits a deprecation warning
case_clause = case(
    [(table.c.x == 5, "five"), (table.c.x == 7, "seven")],
    else_="neither five nor seven",
)

Migration zu 2.0

Nur der "generative" Stil von select() wird unterstützt. Die Liste der Spalten/Tabellen, aus denen ausgewählt werden soll, sollte positionell übergeben werden. Das Konstrukt select() in SQLAlchemy 1.4 akzeptiert sowohl die älteren Stile als auch die neuen Stile mithilfe eines automatischen Erkennungsschemas, sodass der unten stehende Code mit 1.4 und 2.0 kompatibel ist.

# use generative methods
stmt = select(1).select_from(table).order_by(table.c.id)

# use generative methods
stmt = select(table).where(table.c.id == 5)

# use generative methods
stmt = table.select().where(table.c.id == 5)

# pass columns clause expressions positionally
stmt = select(table.c.x, table.c.y)

# case conditions passed positionally
case_clause = case(
    (table.c.x == 5, "five"), (table.c.x == 7, "seven"), else_="neither five nor seven"
)

Diskussion

SQLAlchemy hat seit vielen Jahren eine Konvention für SQL-Konstrukte entwickelt, die ein Argument entweder als Liste oder als positionelle Argumente akzeptieren. Diese Konvention besagt, dass **strukturelle** Elemente, d. h. solche, die die Struktur einer SQL-Anweisung bilden, **positionell** übergeben werden sollten. Umgekehrt sollten **Datenelemente**, d. h. solche, die die parametrisierten Daten einer SQL-Anweisung bilden, **als Listen** übergeben werden. Viele Jahre lang konnte das Konstrukt select() nicht reibungslos an dieser Konvention teilnehmen, da die alte Aufrufweise die "WHERE"-Klausel positionell übergab. SQLAlchemy 2.0 löst dies schließlich, indem das Konstrukt select() so geändert wird, dass nur der "generative" Stil akzeptiert wird, der seit vielen Jahren der einzige dokumentierte Stil im Core-Tutorial ist.

Beispiele für "strukturelle" vs. "Daten"-Elemente sind wie folgt:

# table columns for CREATE TABLE - structural
table = Table("table", metadata_obj, Column("x", Integer), Column("y", Integer))

# columns in a SELECT statement - structural
stmt = select(table.c.x, table.c.y)

# literal elements in an IN clause - data
stmt = stmt.where(table.c.y.in_([1, 2, 3]))

insert/update/delete DML akzeptieren keine Schlüsselwort-Konstruktorargumente mehr

Synopsis

Ähnlich wie bei der vorherigen Änderung von select() werden die Konstruktorargumente für insert(), update() und delete(), abgesehen vom Tabellenargument, im Wesentlichen entfernt.

# no longer supported
stmt = insert(table, values={"x": 10, "y": 15}, inline=True)

# no longer supported
stmt = insert(table, values={"x": 10, "y": 15}, returning=[table.c.x])

# no longer supported
stmt = table.delete(table.c.x > 15)

# no longer supported
stmt = table.update(table.c.x < 15, preserve_parameter_order=True).values(
    [(table.c.y, 20), (table.c.x, table.c.y + 10)]
)

Migration zu 2.0

Die folgenden Beispiele veranschaulichen die generative Methodennutzung für die obigen Beispiele.

# use generative methods, **kwargs OK for values()
stmt = insert(table).values(x=10, y=15).inline()

# use generative methods, dictionary also still  OK for values()
stmt = insert(table).values({"x": 10, "y": 15}).returning(table.c.x)

# use generative methods
stmt = table.delete().where(table.c.x > 15)

# use generative methods, ordered_values() replaces preserve_parameter_order
stmt = (
    table.update()
    .where(
        table.c.x < 15,
    )
    .ordered_values((table.c.y, 20), (table.c.x, table.c.y + 10))
)

Diskussion

Die API und die Interna werden für die DML-Konstrukte auf ähnliche Weise wie bei select() vereinfacht.

Migration 2.0 - ORM-Konfiguration

Deklarativ wird zu einer First-Class-API

Synopsis

Das Paket sqlalchemy.ext.declarative wird bis auf wenige Ausnahmen in das Paket sqlalchemy.orm verschoben. Die Funktionen declarative_base() und declared_attr() sind ohne Verhaltensänderungen vorhanden. Eine neue Superimplementierung von declarative_base(), bekannt als registry, dient nun als oberstes ORM-Konfigurationsobjekt, das auch dekoratorbasierte Deklarationen und neue Unterstützung für klassische Mappings, die sich in die deklarative Registry integrieren, bietet.

Migration zu 2.0

Importe ändern

from sqlalchemy.ext import declarative_base, declared_attr

Zu

from sqlalchemy.orm import declarative_base, declared_attr

Diskussion

Nach etwa zehn Jahren Popularität ist das Paket sqlalchemy.ext.declarative nun in den sqlalchemy.orm-Namespace integriert, mit Ausnahme der deklarativen "Extension"-Klassen, die als deklarative Erweiterungen verbleiben. Die Änderung wird im Migrationsleitfaden 1.4 unter Deklarativ ist jetzt in die ORM integriert mit neuen Funktionen näher erläutert.

Siehe auch

Übersicht über ORM-Mapped-Klassen - alle neuen, vereinheitlichten Dokumentationen für Deklarativ, klassische Mappings, Datenklassen, attrs, etc.

Deklarativ ist jetzt in die ORM integriert mit neuen Funktionen

Die ursprüngliche Funktion "mapper()" ist nun ein Kernelement von Deklarativ und wurde umbenannt

Synopsis

Die eigenständige Funktion sqlalchemy.orm.mapper() rückt hinter die Kulissen und wird von höherrangigen APIs aufgerufen. Die neue Version dieser Funktion ist die Methode registry.map_imperatively(), die von einem registry-Objekt abgeleitet ist.

Migration zu 2.0

Code, der mit klassischen Mappings arbeitet, sollte Importe und Code ändern von

from sqlalchemy.orm import mapper


mapper(SomeClass, some_table, properties={"related": relationship(SomeRelatedClass)})

Um von einem zentralen registry-Objekt aus zu arbeiten.

from sqlalchemy.orm import registry

mapper_reg = registry()

mapper_reg.map_imperatively(
    SomeClass, some_table, properties={"related": relationship(SomeRelatedClass)}
)

Das obige registry ist auch die Quelle für deklarative Mappings, und klassische Mappings haben nun Zugriff auf dieses Registry, einschließlich der stringbasierten Konfiguration in relationship().

from sqlalchemy.orm import registry

mapper_reg = registry()

Base = mapper_reg.generate_base()


class SomeRelatedClass(Base):
    __tablename__ = "related"

    # ...


mapper_reg.map_imperatively(
    SomeClass,
    some_table,
    properties={
        "related": relationship(
            "SomeRelatedClass",
            primaryjoin="SomeRelatedClass.related_id == SomeClass.id",
        )
    },
)

Diskussion

Aufgrund der großen Nachfrage bleibt das "klassische Mapping" erhalten, die neue Form davon basiert jedoch auf dem registry-Objekt und ist als registry.map_imperatively() verfügbar.

Darüber hinaus ist der Hauptgrund für "klassisches Mapping", die Table-Einrichtung von der Klasse zu trennen. Deklarativ hat diesen Stil schon immer mit sogenannten hybriden Deklarationen erlaubt. Um die Basisklassenanforderung zu entfernen, wurde jedoch eine First-Class-Form mit **Decorators** hinzugefügt.

Als weitere separate, aber verwandte Verbesserung wird die Unterstützung für Python-Datenklassen sowohl für deklarative Decorators als auch für klassische Mapping-Formen hinzugefügt.

Siehe auch

Übersicht über ORM-Mapped-Klassen - alle neuen, vereinheitlichten Dokumentationen für Deklarativ, klassische Mappings, Datenklassen, attrs, etc.

Migration 2.0 - ORM-Nutzung

Die größte sichtbare Änderung in SQLAlchemy 2.0 ist die Verwendung von Session.execute() in Verbindung mit select() zum Ausführen von ORM-Abfragen anstelle der Verwendung von Session.query(). Wie anderswo erwähnt, gibt es keine Pläne, die API Session.query() selbst zu entfernen, da sie nun intern mithilfe der neuen API implementiert ist, wird sie als Legacy-API bestehen bleiben, und beide APIs können frei verwendet werden.

Die folgende Tabelle bietet eine Einführung in die allgemeine Änderung der Aufrufweise mit Links zur Dokumentation für jede vorgestellte Technik. Die einzelnen Migrationshinweise befinden sich in den nachfolgenden eingebetteten Abschnitten und können zusätzliche, hier nicht zusammengefasste Hinweise enthalten.

Übersicht über die wichtigsten ORM-Abfragemuster

1.x-Stil Form

2.0-Stil Form

Siehe auch

session.query(User).get(42)
session.get(User, 42)

ORM Query - get() Methode wird zu Session verschoben

session.query(User).all()
session.execute(
  select(User)
).scalars().all()

# or

session.scalars(
  select(User)
).all()

ORM-Abfrage vereinheitlicht mit Core Select

Session.scalars() Result.scalars()

session.query(User).\
  filter_by(name="some user").\
  one()
session.execute(
  select(User).
  filter_by(name="some user")
).scalar_one()

ORM-Abfrage vereinheitlicht mit Core Select

Result.scalar_one()

session.query(User).\
  filter_by(name="some user").\
  first()
session.scalars(
  select(User).
  filter_by(name="some user").
  limit(1)
).first()

ORM-Abfrage vereinheitlicht mit Core Select

Result.first()

session.query(User).options(
  joinedload(User.addresses)
).all()
session.scalars(
  select(User).
  options(
    joinedload(User.addresses)
  )
).unique().all()

ORM Zeilen werden standardmäßig nicht eindeutig identifiziert

session.query(User).\
  join(Address).\
  filter(
    Address.email == "e@sa.us"
  ).\
  all()
session.execute(
  select(User).
  join(Address).
  where(
    Address.email == "e@sa.us"
  )
).scalars().all()

ORM-Abfrage vereinheitlicht mit Core Select

Joins

session.query(User).\
  from_statement(
    text("select * from users")
  ).\
  all()
session.scalars(
  select(User).
  from_statement(
    text("select * from users")
  )
).all()

Abrufen von ORM-Ergebnissen aus textuellen Anweisungen

session.query(User).\
  join(User.addresses).\
  options(
    contains_eager(User.addresses)
  ).\
  populate_existing().all()
session.execute(
  select(User)
  .join(User.addresses)
  .options(
    contains_eager(User.addresses)
  )
  .execution_options(
      populate_existing=True
  )
).scalars().all()

ORM Ausführungsoptionen

Bestehendes auffüllen

session.query(User).\
  filter(User.name == "foo").\
  update(
    {"fullname": "Foo Bar"},
    synchronize_session="evaluate"
  )
session.execute(
  update(User)
  .where(User.name == "foo")
  .values(fullname="Foo Bar")
  .execution_options(
    synchronize_session="evaluate"
  )
)

ORM-aktivierte INSERT-, UPDATE- und DELETE-Anweisungen

session.query(User).count()
session.scalar(
  select(func.count()).
  select_from(User)
)

# or

session.scalar(
  select(func.count(User.id))
)

Session.scalar()

ORM-Abfrage vereinheitlicht mit Core Select

Synopsis

Das Objekt Query (sowie die Erweiterungen BakedQuery und ShardedQuery) wird zu einem langjährigen Legacy-Objekt, das durch die direkte Verwendung des Konstrukts select() in Verbindung mit der Methode Session.execute() ersetzt wird. Ergebnisse, die von Query in Form von Listen von Objekten oder Tupeln oder als skalare ORM-Objekte zurückgegeben werden, werden von Session.execute() einheitlich als Result-Objekte zurückgegeben, die eine Schnittstelle aufweisen, die mit der Core-Ausführung konsistent ist.

Legacy code examples are illustrated below

session = Session(engine)

# becomes legacy use case
user = session.query(User).filter_by(name="some user").one()

# becomes legacy use case
user = session.query(User).filter_by(name="some user").first()

# becomes legacy use case
user = session.query(User).get(5)

# becomes legacy use case
for user in (
    session.query(User).join(User.addresses).filter(Address.email == "some@email.com")
):
    ...

# becomes legacy use case
users = session.query(User).options(joinedload(User.addresses)).order_by(User.id).all()

# becomes legacy use case
users = session.query(User).from_statement(text("select * from users")).all()

# etc

Migration zu 2.0

Da erwartet wird, dass die überwiegende Mehrheit einer ORM-Anwendung Query-Objekte verwendet und die Verfügbarkeit der Query-Schnittstelle die neue Schnittstelle nicht beeinträchtigt, wird das Objekt in 2.0 weiterhin vorhanden sein, aber nicht mehr Teil der Dokumentation sein und größtenteils nicht mehr unterstützt werden. Das Konstrukt select() eignet sich nun sowohl für Core- als auch für ORM-Anwendungsfälle. Wenn es über die Methode Session.execute() aufgerufen wird, gibt es ORM-orientierte Ergebnisse zurück, d. h. ORM-Objekte, wenn dies angefordert wurde.

Das Konstrukt Select() fügt viele neue Methoden hinzu, um die Kompatibilität mit Query zu gewährleisten, darunter Select.filter(), Select.filter_by(), neu überarbeitete Methoden Select.join() und Select.outerjoin(), Select.options() usw. Andere ergänzende Methoden von Query wie Query.populate_existing() werden über Ausführungsoptionen implementiert.

Rückgabewerte erfolgen in Form eines Result-Objekts, der neuen Version des SQLAlchemy ResultProxy-Objekts, das ebenfalls viele neue Methoden zur Kompatibilität mit Query hinzufügt, darunter Result.one(), Result.all(), Result.first(), Result.one_or_none() usw.

Das Result-Objekt erfordert jedoch einige andere Aufrufmodelle, da es beim ersten Rückgabezeitpunkt immer Tupel zurückgibt und Ergebnisse nicht im Speicher dedupliziert. Um einzelne ORM-Objekte zurückzugeben, wie es Query tut, muss zuerst der Modifikator Result.scalars() aufgerufen werden. Um eindeutige Objekte zurückzugeben, wie sie bei der Joined Eager Loading benötigt werden, muss zuerst der Modifikator Result.unique() aufgerufen werden.

Dokumentation aller neuen Funktionen von select(), einschließlich Ausführungsoptionen usw., finden Sie im Handbuch zur ORM-Abfrage.

Unten finden Sie einige Beispiele, wie Sie zu select() migrieren können

session = Session(engine)

user = session.execute(select(User).filter_by(name="some user")).scalar_one()

# for first(), no LIMIT is applied automatically; add limit(1) if LIMIT
# is desired on the query
user = (
    session.execute(select(User).filter_by(name="some user").limit(1)).scalars().first()
)

# get() moves to the Session directly
user = session.get(User, 5)

for user in session.execute(
    select(User).join(User.addresses).filter(Address.email == "some@email.case")
).scalars():
    ...

# when using joinedload() against collections, use unique() on the result
users = (
    session.execute(select(User).options(joinedload(User.addresses)).order_by(User.id))
    .unique()
    .all()
)

# select() has ORM-ish methods like from_statement() that only work
# if the statement is against ORM entities
users = (
    session.execute(select(User).from_statement(text("select * from users")))
    .scalars()
    .all()
)

Diskussion

Die Tatsache, dass SQLAlchemy sowohl ein select()-Konstrukt als auch ein separates Query-Objekt mit einer extrem ähnlichen, aber grundlegend inkompatiblen Schnittstelle aufweist, ist wahrscheinlich die größte Inkonsistenz in SQLAlchemy, die aus kleinen inkrementellen Ergänzungen im Laufe der Zeit resultierte und zu zwei divergierenden Haupt-APIs führte.

In den ersten Versionen von SQLAlchemy gab es das Query-Objekt überhaupt nicht. Die ursprüngliche Idee war, dass das Mapper-Konstrukt selbst Zeilen auswählen könnte und dass Table-Objekte anstelle von Klassen zur Erstellung der verschiedenen Kriterien in einem Core-Stil-Ansatz verwendet würden. Das Query kam einige Monate/Jahre nach der Entstehung von SQLAlchemy als Benutzeranfrage für ein neues, "konstruierbares" Abfrageobjekt namens ursprünglich SelectResults wurde akzeptiert. Konzepte wie eine .where()-Methode, die SelectResults .filter() nannte, gab es zuvor nicht in SQLAlchemy, und das select()-Konstrukt verwendete nur den "auf einmal"-Konstruktionsstil, der jetzt unter select() akzeptiert keine variablen Konstruktorargumente mehr, Spalten werden positionsabhängig übergeben als veraltet gilt.

Als sich der neue Ansatz durchsetzte, entwickelte sich das Objekt zum Query-Objekt, da neue Funktionen wie die Möglichkeit, einzelne Spalten auszuwählen, mehrere Entitäten gleichzeitig auszuwählen, Unterabfragen aus einem Query-Objekt anstelle eines select-Objekts zu erstellen, hinzugefügt wurden. Das Ziel wurde, dass Query die volle Funktionalität von select haben sollte, d.h. es konnte so zusammengestellt werden, dass SELECT-Anweisungen vollständig ohne explizite Verwendung von select() erstellt werden konnten. Gleichzeitig hatte sich select() ebenfalls "generative" Methoden wie Select.where() und Select.order_by() entwickelt.

Im modernen SQLAlchemy wurde dieses Ziel erreicht und die beiden Objekte überschneiden sich nun vollständig in ihrer Funktionalität. Die größte Herausforderung bei der Vereinheitlichung dieser Objekte bestand darin, dass das select()-Objekt vollständig vom ORM unabhängig bleiben musste. Um dies zu erreichen, wurde der Großteil der Logik von Query in die SQL-Kompilierungsphase verschoben, wo ORM-spezifische Compiler-Plugins das Select-Konstrukt empfangen und dessen Inhalt im Sinne einer ORM-Abfrage interpretieren, bevor sie an den Compiler auf Core-Ebene weitergegeben werden, um eine SQL-Zeichenkette zu erstellen. Mit der Einführung des neuen SQL-Kompilierungs-Caching-Systems wird ein Großteil dieser ORM-Logik ebenfalls zwischengespeichert.

ORM Query - get()-Methode wird zu Session verschoben

Synopsis

Die Methode Query.get() bleibt für Legacy-Zwecke erhalten, aber die primäre Schnittstelle ist nun die Methode Session.get()

# legacy usage
user_obj = session.query(User).get(5)

Migration zu 2.0

In 1.4 / 2.0 fügt das Session-Objekt eine neue Methode Session.get() hinzu

# 1.4 / 2.0 cross-compatible use
user_obj = session.get(User, 5)

Diskussion

Das Objekt Query wird in 2.0 ein Legacy-Objekt, da ORM-Abfragen nun mit dem Objekt select() verfügbar sind. Da die Methode Query.get() eine spezielle Interaktion mit der Session definiert und nicht unbedingt eine Abfrage auslöst, ist es angemessener, dass sie Teil von Session ist, wo sie anderen "Identitäts"-Methoden wie refresh und merge ähnelt.

SQLAlchemy enthielt ursprünglich "get()", um der Hibernate Session.load()-Methode zu ähneln. Wie so oft haben wir es ein wenig falsch gemacht, da diese Methode eigentlich mehr mit der Session zu tun hat als mit dem Schreiben einer SQL-Abfrage.

ORM Query - Joining / Loading auf Beziehungen verwendet Attribute, nicht Strings

Synopsis

Dies bezieht sich auf Muster wie Query.join() sowie auf Abfrageoptionen wie joinedload(), die derzeit eine Mischung aus Zeichenketten-Attributnamen oder tatsächlichen Klassenattributen akzeptieren. Die Zeichenkettenformen werden in 2.0 entfernt

# string use removed
q = session.query(User).join("addresses")

# string use removed
q = session.query(User).options(joinedload("addresses"))

# string use removed
q = session.query(Address).filter(with_parent(u1, "addresses"))

Migration zu 2.0

Moderne SQLAlchemy 1.x-Versionen unterstützen die empfohlene Technik, nämlich die Verwendung von gemappten Attributen

# compatible with all modern SQLAlchemy versions

q = session.query(User).join(User.addresses)

q = session.query(User).options(joinedload(User.addresses))

q = session.query(Address).filter(with_parent(u1, User.addresses))

Die gleichen Techniken gelten für die Verwendung im 2.0-Stil

# SQLAlchemy 1.4 / 2.0 cross compatible use

stmt = select(User).join(User.addresses)
result = session.execute(stmt)

stmt = select(User).options(joinedload(User.addresses))
result = session.execute(stmt)

stmt = select(Address).where(with_parent(u1, User.addresses))
result = session.execute(stmt)

Diskussion

Die Zeichenketten-Aufrufform ist mehrdeutig und erfordert, dass die Interna zusätzliche Arbeit leisten, um den geeigneten Pfad zu bestimmen und die korrekte gemappte Eigenschaft abzurufen. Durch die direkte Übergabe des ORM-gemappten Attributs werden nicht nur die notwendigen Informationen vorab übergeben, sondern das Attribut ist auch typisiert und potenziell besser mit IDEs und PEP-484-Integrationen kompatibel.

ORM Query - Verkettung mit Attributlisten, anstatt einzelner Aufrufe, entfernt

Synopsis

"Verkettete" Formen von Join- und Loader-Optionen, die mehrere gemappte Attribute in einer Liste akzeptieren, werden entfernt

# chaining removed
q = session.query(User).join("orders", "items", "keywords")

Migration zu 2.0

Verwenden Sie einzelne Aufrufe an Query.join() für 1.x / 2.0-kompatible Verwendung

q = session.query(User).join(User.orders).join(Order.items).join(Item.keywords)

Für die Verwendung im 2.0-Stil hat Select das gleiche Verhalten wie Select.join() und bietet außerdem eine neue Methode Select.join_from(), die eine explizite linke Seite ermöglicht

# 1.4 / 2.0 cross compatible

stmt = select(User).join(User.orders).join(Order.items).join(Item.keywords)
result = session.execute(stmt)

# join_from can also be helpful
stmt = select(User).join_from(User, Order).join_from(Order, Item, Order.items)
result = session.execute(stmt)

Diskussion

Das Entfernen der Attributverkettung steht im Einklang mit der Vereinfachung der Aufruf-Schnittstelle von Methoden wie Select.join().

ORM Query - join(…, aliased=True), from_joinpoint entfernt

Synopsis

Die Option aliased=True bei Query.join() wird entfernt, ebenso wie das Flag from_joinpoint

# no longer supported
q = (
    session.query(Node)
    .join("children", aliased=True)
    .filter(Node.name == "some sub child")
    .join("children", from_joinpoint=True, aliased=True)
    .filter(Node.name == "some sub sub child")
)

Migration zu 2.0

Verwenden Sie stattdessen explizite Aliase

n1 = aliased(Node)
n2 = aliased(Node)

q = (
    select(Node)
    .join(Node.children.of_type(n1))
    .where(n1.name == "some sub child")
    .join(n1.children.of_type(n2))
    .where(n2.name == "some sub child")
)

Diskussion

Die Option aliased=True bei Query.join() ist eine weitere Funktion, die fast nie verwendet zu werden scheint, basierend auf umfangreichen Codesuchen, um tatsächliche Verwendungen dieser Funktion zu finden. Die interne Komplexität, die das Flag aliased=True erfordert, ist enorm und wird in 2.0 entfallen.

Die meisten Benutzer sind sich dieses Flags nicht bewusst, es ermöglicht jedoch eine automatische Aliasbildung von Elementen entlang eines Joins, was dann eine automatische Aliasbildung auf Filterbedingungen anwendet. Der ursprüngliche Anwendungsfall war die Unterstützung langer Ketten von selbstreferenziellen Joins, wie im obigen Beispiel gezeigt. Die automatische Anpassung der Filterkriterien ist jedoch intern enorm kompliziert und wird in realen Anwendungen fast nie verwendet. Das Muster führt auch zu Problemen, z. B. wenn Filterkriterien an jedem Glied der Kette hinzugefügt werden müssen; das Muster muss dann das Flag from_joinpoint verwenden, für das SQLAlchemy-Entwickler absolut keine Vorkommen dieses Parameters in realen Anwendungen finden konnten.

Die Parameter aliased=True und from_joinpoint wurden zu einer Zeit entwickelt, als das Query-Objekt noch keine guten Fähigkeiten für Joins entlang von Beziehungsattributen hatte, Funktionen wie PropComparator.of_type() noch nicht existierten und das aliased()-Konstrukt selbst noch nicht frühzeitig existierte.

Verwendung von DISTINCT mit zusätzlichen Spalten, aber nur Auswahl der Entität

Synopsis

Query fügt automatisch Spalten in der ORDER BY hinzu, wenn DISTINCT verwendet wird. Die folgende Abfrage wählt aus allen User-Spalten sowie "address.email_address" aus, gibt aber nur User-Objekte zurück

# 1.xx code

result = (
    session.query(User)
    .join(User.addresses)
    .distinct()
    .order_by(Address.email_address)
    .all()
)

In Version 2.0 wird die Spalte "email_address" nicht automatisch zur Spaltenklausel hinzugefügt, und die obige Abfrage wird fehlschlagen, da relationale Datenbanken die ORDER BY "address.email_address" bei Verwendung von DISTINCT nicht zulassen, wenn sie nicht auch in der Spaltenklausel enthalten ist.

Migration zu 2.0

In 2.0 muss die Spalte explizit hinzugefügt werden. Um das Problem der nur Rückgabe der Hauptentität und nicht der zusätzlichen Spalte zu lösen, verwenden Sie die Methode Result.columns()

# 1.4 / 2.0 code

stmt = (
    select(User, Address.email_address)
    .join(User.addresses)
    .distinct()
    .order_by(Address.email_address)
)

result = session.execute(stmt).columns(User).all()

Diskussion

Dieser Fall ist ein Beispiel für die begrenzte Flexibilität von Query, die dazu führt, dass implizites, "magisches" Verhalten hinzugefügt werden muss; die Spalte "email_address" wird implizit zur Spaltenklausel hinzugefügt, und zusätzliche interne Logik würde diese Spalte aus den tatsächlich zurückgegebenen Ergebnissen weglassen.

Der neue Ansatz vereinfacht die Interaktion und macht das Geschehen explizit, während er gleichzeitig die Erfüllung des ursprünglichen Anwendungsfalls ohne Unannehmlichkeiten ermöglicht.

Aus der Abfrage selbst als Unterabfrage auswählen, z. B. "from_self()"

Synopsis

Die Methode Query.from_self() wird aus Query entfernt

# from_self is removed
q = (
    session.query(User, Address.email_address)
    .join(User.addresses)
    .from_self(User)
    .order_by(Address.email_address)
)

Migration zu 2.0

Das Konstrukt aliased() kann verwendet werden, um ORM-Abfragen gegen eine Entität auszugeben, die sich in Bezug auf eine beliebige wählbare enthält. Es wurde in Version 1.4 verbessert, um die mehrfache Verwendung gegen dieselbe Unterabfrage für verschiedene Entitäten reibungslos zu unterstützen. Dies kann im 1.x-Stil mit Query wie unten gezeigt verwendet werden; beachten Sie, dass, da die endgültige Abfrage sowohl in Bezug auf die User- als auch auf die Address-Entitäten abfragen soll, zwei separate aliased()-Konstrukte erstellt werden

from sqlalchemy.orm import aliased

subq = session.query(User, Address.email_address).join(User.addresses).subquery()

ua = aliased(User, subq)

aa = aliased(Address, subq)

q = session.query(ua, aa).order_by(aa.email_address)

Die gleiche Form kann im 2.0-Stil verwendet werden

from sqlalchemy.orm import aliased

subq = select(User, Address.email_address).join(User.addresses).subquery()

ua = aliased(User, subq)

aa = aliased(Address, subq)

stmt = select(ua, aa).order_by(aa.email_address)

result = session.execute(stmt)

Diskussion

Die Methode Query.from_self() ist eine sehr komplizierte Methode, die selten verwendet wird. Der Zweck dieser Methode ist es, eine Query in eine Unterabfrage umzuwandeln und dann eine neue Query zurückzugeben, die aus dieser Unterabfrage SELECTiert. Der ausgefeilte Aspekt dieser Methode besteht darin, dass die zurückgegebene Abfrage eine automatische Übersetzung von ORM-Entitäten und Spalten anwendet, um in der SELECT in Bezug auf die Unterabfrage ausgedrückt zu werden, sowie dass sie die Entitäten und Spalten, aus denen ausgewählt werden kann, modifiziert.

Da Query.from_self() eine intensive implizite Übersetzung in die erzeugte SQL packt, ermöglicht sie zwar die Ausführung einer bestimmten Art von Muster sehr prägnant, die reale Verwendung dieser Methode ist jedoch selten, da sie nicht einfach zu verstehen ist.

Der neue Ansatz nutzt das Konstrukt aliased(), so dass die ORM-Interna nicht raten müssen, welche Entitäten und Spalten wie angepasst werden sollen; im obigen Beispiel geben die Objekte ua und aa, beides Instanzen von AliasedClass, den Interna einen unzweideutigen Marker, wo die Unterabfrage referenziert werden soll und welche Entitätsspalte oder Beziehung für eine bestimmte Komponente der Abfrage in Betracht gezogen wird.

SQLAlchemy 1.4 bietet auch einen verbesserten Labeling-Stil, der keine langen Labels mehr erfordert, die den Tabellennamen enthalten, um Spalten mit gleichen Namen aus verschiedenen Tabellen zu disambiguieren. In den obigen Beispielen können wir, auch wenn unsere User- und Address-Entitäten überlappende Spaltennamen haben, gleichzeitig aus beiden Entitäten auswählen, ohne eine bestimmte Kennzeichnung angeben zu müssen

# 1.4 / 2.0 code

subq = select(User, Address).join(User.addresses).subquery()

ua = aliased(User, subq)
aa = aliased(Address, subq)

stmt = select(ua, aa).order_by(aa.email_address)
result = session.execute(stmt)

Die obige Abfrage disambiguiert die .id-Spalte von User und Address, wobei Address.id als id_1 gerendert und verfolgt wird

SELECT anon_1.id AS anon_1_id, anon_1.id_1 AS anon_1_id_1,
       anon_1.user_id AS anon_1_user_id,
       anon_1.email_address AS anon_1_email_address
FROM (
  SELECT "user".id AS id, address.id AS id_1,
  address.user_id AS user_id, address.email_address AS email_address
  FROM "user" JOIN address ON "user".id = address.user_id
) AS anon_1 ORDER BY anon_1.email_address

#5221

Auswahl von Entitäten aus alternativen wählbaren; Query.select_entity_from()

Synopsis

Die Methode Query.select_entity_from() wird in 2.0 entfernt

subquery = session.query(User).filter(User.id == 5).subquery()

user = session.query(User).select_entity_from(subquery).first()

Migration zu 2.0

Wie im Abschnitt Auswahl aus der Abfrage selbst als Unterabfrage, z. B. "from_self()" beschrieben, bietet das Objekt aliased() einen einzigen Ort, an dem Operationen wie "Auswahl einer Entität aus einer Unterabfrage" erreicht werden können. Verwendung im 1.x-Stil

from sqlalchemy.orm import aliased

subquery = session.query(User).filter(User.name.like("%somename%")).subquery()

ua = aliased(User, subquery)

user = session.query(ua).order_by(ua.id).first()

Verwendung im 2.0-Stil

from sqlalchemy.orm import aliased

subquery = select(User).where(User.name.like("%somename%")).subquery()

ua = aliased(User, subquery)

# note that LIMIT 1 is not automatically supplied, if needed
user = session.execute(select(ua).order_by(ua.id).limit(1)).scalars().first()

Diskussion

Die Punkte hier sind im Wesentlichen die gleichen wie die, die unter Auswahl aus der Abfrage selbst als Unterabfrage, z. B. "from_self()" besprochen wurden. Die Methode Query.select_from_entity() war eine weitere Möglichkeit, die Abfrage anzuweisen, Zeilen für eine bestimmte ORM-gemappte Entität aus einer alternativen wählbaren zu laden, was dazu führte, dass das ORM eine automatische Aliasbildung für diese Entität anwendete, wo immer sie später in der Abfrage verwendet wurde, z. B. in der WHERE-Klausel oder ORDER BY. Diese äußerst komplexe Funktion wird selten auf diese Weise verwendet, da, wie bei Query.from_self(), es viel einfacher ist, zu verstehen, was vor sich geht, wenn ein explizites aliased()-Objekt verwendet wird, sowohl aus Benutzersicht als auch im Hinblick darauf, wie die Interna des SQLAlchemy ORM damit umgehen müssen.

ORM-Zeilen werden standardmäßig nicht eindeutig gemacht

Synopsis

ORM-Zeilen, die von session.execute(stmt) zurückgegeben werden, werden nicht mehr automatisch "eindeutig" gemacht. Dies wird normalerweise eine willkommene Änderung sein, außer in dem Fall, dass die "joined eager loading"-Loader-Strategie mit Sammlungen verwendet wird

# In the legacy API, many rows each have the same User primary key, but
# only one User per primary key is returned
users = session.query(User).options(joinedload(User.addresses))

# In the new API, uniquing is available but not implicitly
# enabled
result = session.execute(select(User).options(joinedload(User.addresses)))

# this actually will raise an error to let the user know that
# uniquing should be applied
rows = result.all()

Migration zu 2.0

Bei Verwendung eines Joined Load einer Sammlung ist die Methode Result.unique() erforderlich. Das ORM setzt tatsächlich einen Standard-Zeilenhandler, der einen Fehler auslöst, wenn dies nicht geschieht, um sicherzustellen, dass ein Joined Eager Load einer Sammlung keine doppelten Zeilen zurückgibt, während die Explizitheit beibehalten wird

# 1.4 / 2.0 code

stmt = select(User).options(joinedload(User.addresses))

# statement will raise if unique() is not used, due to joinedload()
# of a collection.  in all other cases, unique() is not needed.
# By stating unique() explicitly, confusion over discrepancies between
# number of objects/ rows returned vs. "SELECT COUNT(*)" is resolved
rows = session.execute(stmt).unique().all()

Diskussion

Die Situation hier ist etwas ungewöhnlich, da SQLAlchemy die Aufrufung einer Methode verlangt, die es tatsächlich vollständig automatisch ausführen kann. Der Grund für die Anforderung, dass die Methode aufgerufen wird, ist, um sicherzustellen, dass der Entwickler die Verwendung der Methode Result.unique() "opt-in" macht, damit er nicht verwirrt ist, wenn eine einfache Zählung von Zeilen nicht mit der Zählung der Datensätze im tatsächlichen Ergebnissatz kollidiert, was seit vielen Jahren eine lang anhaltende Quelle von Verwirrung für Benutzer und Fehlermeldungen war. Dass die Eindeutigmachung in allen anderen Fällen nicht standardmäßig erfolgt, verbessert die Leistung und auch die Klarheit in Fällen, in denen eine automatische Eindeutigmachung zu verwirrenden Ergebnissen führte.

In dem Maße, in dem das Aufrufen von Result.unique() bei Joined Eager Load-Sammlungen unpraktisch ist, stellt in modernem SQLAlchemy die selectinload()-Strategie einen Sammlungs-orientierten Eager Loader dar, der in den meisten Aspekten besser ist als joinedload() und bevorzugt werden sollte.

"Dynamische" Beziehungsloader, ersetzt durch "Nur Schreiben"

Synopsis

Die lazy="dynamic"-Beziehungsloader-Strategie, diskutiert in Dynamische Beziehungsloader, verwendet das Objekt Query, das in 2.0 ein Legacy-Objekt ist. Die "dynamische" Beziehung ist nicht direkt mit asyncio ohne Workarounds kompatibel und erfüllt darüber hinaus nicht ihren ursprünglichen Zweck, die Iteration großer Sammlungen zu verhindern, da sie mehrere Verhaltensweisen aufweist, bei denen diese Iteration implizit erfolgt.

Eine neue Loader-Strategie namens lazy="write_only" wird eingeführt, die über die Sammlungs-Klasse WriteOnlyCollection eine sehr strenge "keine implizite Iteration"-API bietet und darüber hinaus mit der 2.0-Stil-Statement-Ausführung integriert, asynchrono unterstützt sowie direkte Integrationen mit dem neuen ORM-fähige Bulk DML-Funktionsumfang.

Gleichzeitig bleibt lazy="dynamic" in Version 2.0 vollständig unterstützt; Anwendungen können mit der Migration dieses speziellen Musters warten, bis sie vollständig auf der 2.0-Serie laufen.

Migration zu 2.0

Die neue "write only"-Funktion ist nur in SQLAlchemy 2.0 verfügbar und nicht Teil von 1.4. Gleichzeitig bleibt die lazy="dynamic"-Loader-Strategie in Version 2.0 vollständig unterstützt und beinhaltet sogar neue PEP-484- und annotierte Mapping-Unterstützung.

Daher ist die beste Strategie für die Migration von "dynamic", warten Sie, bis die Anwendung vollständig unter 2.0 läuft, migrieren Sie dann direkt von AppenderQuery, dem Sammlungs-Typ, der von der "dynamic"-Strategie verwendet wird, zu WriteOnlyCollection, dem Sammlungs-Typ, der von der "write_only"-Strategie verwendet wird.

Es gibt einige Techniken, um lazy="dynamic" unter 1.4 in einem "2.0"-ähnlicheren Stil zu verwenden. Es gibt zwei Möglichkeiten, 2.0-Stil-Abfragen zu erreichen, die sich auf eine bestimmte Beziehung beziehen

  • Nutzen Sie das Attribut Query.statement auf einer vorhandenen lazy="dynamic" Beziehung. Wir können Methoden wie Session.scalars() mit dem dynamischen Loader sofort wie folgt verwenden

    class User(Base):
        __tablename__ = "user"
    
        posts = relationship(Post, lazy="dynamic")
    
    
    jack = session.get(User, 5)
    
    # filter Jack's blog posts
    posts = session.scalars(jack.posts.statement.where(Post.headline == "this is a post"))
  • Verwenden Sie die Funktion with_parent(), um direkt eine select()-Konstruktion zu erstellen

    from sqlalchemy.orm import with_parent
    
    jack = session.get(User, 5)
    
    posts = session.scalars(
        select(Post)
        .where(with_parent(jack, User.posts))
        .where(Post.headline == "this is a post")
    )

Diskussion

Die ursprüngliche Idee war, dass die Funktion with_parent() ausreichen sollte, jedoch bleibt die weitere Nutzung spezieller Attribute an der Beziehung selbst attraktiv, und es gibt keinen Grund, warum eine Konstruktion im 2.0-Stil hier nicht auch funktionieren könnte.

Die neue Loader-Strategie „write_only“ bietet eine neue Art von Sammlung, die keine implizite Iteration oder Zugriff auf Elemente unterstützt. Stattdessen wird das Lesen des Inhalts der Sammlung durch Aufrufen ihrer Methode .select() durchgeführt, um beim Erstellen einer geeigneten SELECT-Anweisung zu helfen. Die Sammlung enthält auch die Methoden .insert(), .update(), .delete(), die verwendet werden können, um Bulk-DML-Anweisungen für die Elemente in der Sammlung auszugeben. Ähnlich wie bei der „dynamic“-Funktion gibt es auch die Methoden .add(), .add_all() und .remove(), die einzelne Mitglieder für die Hinzufügung oder Entfernung über den Unit-of-Work-Prozess in die Warteschlange stellen. Eine Einführung in die neue Funktion finden Sie unter Neue „Write Only“-Beziehungsstrategie ersetzt „dynamic“.

Autocommit-Modus aus Session entfernt; Autobegin-Unterstützung hinzugefügt

Synopsis

Die Session unterstützt den „autocommit“-Modus nicht mehr, das heißt, dieses Muster

from sqlalchemy.orm import Session

sess = Session(engine, autocommit=True)

# no transaction begun, but emits SQL, won't be supported
obj = sess.query(Class).first()


# session flushes in a transaction that it begins and
# commits, won't be supported
sess.flush()

Migration zu 2.0

Der Hauptgrund für die Verwendung einer Session im „autocommit“-Modus ist, dass die Methode Session.begin() verfügbar ist, damit Framework-Integrationen und Ereignis-Hooks steuern können, wann dieses Ereignis eintritt. In 1.4 verfügt die Session nun über autobegin-Verhalten, das dieses Problem löst; die Methode Session.begin() kann nun aufgerufen werden

from sqlalchemy.orm import Session

sess = Session(engine)

sess.begin()  # begin explicitly; if not called, will autobegin
# when database access is needed

sess.add(obj)

sess.commit()

Diskussion

Der „autocommit“-Modus ist ein weiteres Überbleibsel aus den ersten Versionen von SQLAlchemy. Das Flag blieb hauptsächlich zur Unterstützung der expliziten Verwendung von Session.begin() erhalten, was nun durch 1.4 gelöst ist, sowie zur Ermöglichung der Verwendung von „Subtransactions“, die ebenfalls in 2.0 entfernt werden.

Session „Subtransaction“-Verhalten entfernt

Synopsis

Das „Subtransaction“-Muster, das häufig mit dem Autocommit-Modus verwendet wurde, ist ebenfalls in 1.4 veraltet. Dieses Muster erlaubte die Verwendung der Methode Session.begin(), wenn bereits eine Transaktion begonnen wurde, was zu einer Konstruktion namens „Subtransaction“ führte, die im Wesentlichen ein Block war, der verhinderte, dass die Methode Session.commit() tatsächlich committete.

Migration zu 2.0

Um die Abwärtskompatibilität für Anwendungen zu gewährleisten, die dieses Muster verwenden, kann der folgende Kontextmanager oder eine ähnliche Implementierung basierend auf einem Dekorator verwendet werden

import contextlib


@contextlib.contextmanager
def transaction(session):
    if not session.in_transaction():
        with session.begin():
            yield
    else:
        yield

Der obige Kontextmanager kann auf die gleiche Weise wie das „Subtransaction“-Flag verwendet werden, z. B. im folgenden Beispiel

# method_a starts a transaction and calls method_b
def method_a(session):
    with transaction(session):
        method_b(session)


# method_b also starts a transaction, but when
# called from method_a participates in the ongoing
# transaction.
def method_b(session):
    with transaction(session):
        session.add(SomeObject("bat", "lala"))


Session = sessionmaker(engine)

# create a Session and call method_a
with Session() as session:
    method_a(session)

Um dies mit dem bevorzugten idiomatischen Muster zu vergleichen, sollte der Begin-Block auf der äußersten Ebene liegen. Dies beseitigt die Notwendigkeit für einzelne Funktionen oder Methoden, sich mit den Details der Transaktionsabgrenzung zu befassen.

def method_a(session):
    method_b(session)


def method_b(session):
    session.add(SomeObject("bat", "lala"))


Session = sessionmaker(engine)

# create a Session and call method_a
with Session() as session:
    with session.begin():
        method_a(session)

Diskussion

Es hat sich gezeigt, dass dieses Muster in realen Anwendungen verwirrend ist, und es ist vorzuziehen, dass eine Anwendung sicherstellt, dass die oberste Ebene der Datenbankoperationen mit einem einzelnen Begin/Commit-Paar ausgeführt wird.

2.0 Migration - ORM-Erweiterung und Rezeptänderungen

Dogpile Cache Rezept und Horizontal Sharding verwenden die neue Session API

Da das Query-Objekt veraltet ist, verwenden diese beiden Rezepte, die zuvor auf der Unterklasse des Query-Objekts basierten, nun den Hook SessionEvents.do_orm_execute(). Siehe den Abschnitt Erneutes Ausführen von Anweisungen für ein Beispiel.

Baked Query Extension ersetzt durch integrierte Caching-Funktion

Die Baked Query Extension wird durch das integrierte Caching-System ersetzt und wird von den ORM-Interna nicht mehr verwendet.

Siehe SQL-Kompilierungs-Caching für vollständige Hintergrundinformationen zum neuen Caching-System.

Asyncio-Unterstützung

SQLAlchemy 1.4 beinhaltet Asyncio-Unterstützung für Core und ORM. Die neue API verwendet ausschließlich die oben genannten „Future“-Muster. Siehe Asynchrone E/A-Unterstützung für Core und ORM für Hintergrundinformationen.