Was ist neu in SQLAlchemy 1.3?

Über dieses Dokument

Dieses Dokument beschreibt die Änderungen zwischen SQLAlchemy Version 1.2 und SQLAlchemy Version 1.3.

Einleitung

Diese Anleitung stellt die Neuerungen in SQLAlchemy Version 1.3 vor und dokumentiert auch Änderungen, die Benutzer betreffen, die ihre Anwendungen von der 1.2er-Serie von SQLAlchemy auf 1.3 migrieren.

Bitte überprüfen Sie die Abschnitte über Verhaltensänderungen sorgfältig auf potenziell abwärtsinkompatible Änderungen im Verhalten.

Allgemein

Deprecation-Warnungen werden für alle veralteten Elemente ausgegeben; neue Veralterungen hinzugefügt

Release 1.3 stellt sicher, dass alle veralteten Verhaltensweisen und APIs, einschließlich aller, die seit Jahren als "Legacy" aufgeführt sind, DeprecationWarning-Warnungen ausgeben. Dies gilt auch bei der Verwendung von Parametern wie Session.weak_identity_map und Klassen wie MapperExtension. Obwohl alle Veralterungen in der Dokumentation vermerkt sind, verwendeten sie oft keine ordnungsgemäße re strukturierte Textdirektive oder gaben an, in welcher Version sie veraltet waren. Ob eine bestimmte API-Funktion tatsächlich eine Veralterungswarnung ausgab, war nicht konsistent. Die allgemeine Haltung war, dass die meisten oder alle dieser veralteten Funktionen als langfristige Legacy-Funktionen behandelt wurden, ohne Pläne zu deren Entfernung.

Die Änderung beinhaltet, dass alle dokumentierten Veralterungen nun eine ordnungsgemäße re strukturierte Textdirektive in der Dokumentation mit einer Versionsnummer verwenden, die Formulierung, dass die Funktion oder der Anwendungsfall in einer zukünftigen Version entfernt wird, wird explizit gemacht (z. B. keine Legacy für immer mehr), und die Verwendung einer solchen Funktion oder eines solchen Anwendungsfalls gibt definitiv eine DeprecationWarning aus, die in Python 3 sowie bei der Verwendung moderner Testwerkzeuge wie Pytest nun im Standard-Fehlerstrom expliziter gemacht werden. Das Ziel ist, dass diese lange veralteten Funktionen, die bis zu Version 0.7 oder 0.6 zurückreichen, vollständig entfernt werden sollten, anstatt sie als "Legacy"-Funktionen beizubehalten. Darüber hinaus werden ab Version 1.3 einige wichtige neue Veralterungen hinzugefügt. Da SQLAlchemy 14 Jahre reale Nutzung durch Tausende von Entwicklern hinter sich hat, ist es möglich, einen einzigen Strom von Anwendungsfällen zu identifizieren, die gut miteinander verschmelzen, und Funktionen und Muster zu kürzen, die diesem einzigen Arbeitsweg entgegenstehen.

Der größere Kontext ist, dass SQLAlchemy bestrebt ist, sich auf die kommende reine Python 3-Welt sowie auf eine typisierte Welt einzustellen, und zu diesem Ziel gibt es vorläufige Pläne für eine umfassende Überarbeitung von SQLAlchemy, die hoffentlich die kognitive Belastung der API erheblich reduzieren und eine umfassende Überprüfung der vielen Unterschiede in Implementierung und Nutzung zwischen Core und ORM durchführen würde. Da sich diese beiden Systeme nach der ersten Veröffentlichung von SQLAlchemy dramatisch entwickelt haben, behält insbesondere das ORM immer noch viele "aufgesetzte" Verhaltensweisen bei, die die Trennwand zwischen Core und ORM zu hoch halten. Durch die Fokussierung der API im Voraus auf ein einziges Muster für jeden unterstützten Anwendungsfall wird die endgültige Aufgabe der Migration zu einer erheblich veränderten API einfacher.

Die wichtigsten neuen Veralterungen in 1.3 finden Sie in den unten verlinkten Abschnitten.

#4393

Neue Features und Verbesserungen - ORM

Beziehung zu AliasedClass ersetzt die Notwendigkeit für nicht-primäre Mapper

Der "nicht-primäre Mapper" ist ein Mapper, der im Imperativen Mapping-Stil erstellt wird und als zusätzlicher Mapper gegen eine bereits gemappte Klasse gegen eine andere Art von wählbarer Entität fungiert. Der nicht-primäre Mapper hat seine Wurzeln in der 0.1, 0.2 Serie von SQLAlchemy, als erwartet wurde, dass das Mapper-Objekt die primäre Schnittstelle zur Abfragekonstruktion sein würde, bevor das Query-Objekt existierte.

Mit dem Aufkommen von Query und später der AliasedClass-Konstruktion entfielen die meisten Anwendungsfälle für den nicht-primären Mapper. Das war gut, da sich SQLAlchemy um die 0.5 Serie herum auch vom "klassischen" Mapping verabschiedete und dem deklarativen System den Vorzug gab.

Ein Anwendungsfall für nicht-primäre Mapper blieb erhalten, als erkannt wurde, dass einige sehr schwer zu definierende relationship()-Konfigurationen möglich wurden, wenn ein nicht-primärer Mapper mit einer alternativen wählbaren Entität als Mapping-Ziel erstellt wurde, anstatt zu versuchen, ein relationship.primaryjoin zu konstruieren, das die gesamte Komplexität einer bestimmten Objektbeziehung umfasste.

Mit zunehmender Beliebtheit dieses Anwendungsfalls wurden seine Einschränkungen offensichtlich, darunter, dass der nicht-primäre Mapper schwierig gegen eine wählbare Entität zu konfigurieren ist, die neue Spalten hinzufügt, dass der Mapper die Beziehungen des ursprünglichen Mappings nicht erbt, dass explizit auf dem nicht-primären Mapper konfigurierte Beziehungen nicht gut mit Loader-Optionen funktionieren und dass der nicht-primäre Mapper auch keinen voll funktionsfähigen Namespace spaltenbasierter Attribute bietet, die in Abfragen verwendet werden können (was, wie erwähnt, in den alten Tagen 0.1 - 0.4, man direkt Table-Objekte mit dem ORM verwenden würde).

Das fehlende Glied war, die relationship() direkt auf die AliasedClass verweisen zu lassen. Die AliasedClass erledigt bereits alles, was wir vom nicht-primären Mapper erwarten; sie ermöglicht es einer bestehenden gemappten Klasse, von einer alternativen wählbaren Entität geladen zu werden, sie erbt alle Attribute und Beziehungen des bestehenden Mappers, sie funktioniert hervorragend mit Loader-Optionen und sie bietet ein klassenähnliches Objekt, das genauso wie die Klasse selbst in Abfragen gemischt werden kann. Mit dieser Änderung werden die Rezepte, die früher für nicht-primäre Mapper unter Konfiguration von Beziehungs-Joins zu finden waren, zu Aliased Class geändert.

Unter Beziehung zu Aliased Class sah der ursprüngliche nicht-primäre Mapper so aus:

j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)

B_viacd = mapper(
    B,
    j,
    non_primary=True,
    primary_key=[j.c.b_id],
    properties={
        "id": j.c.b_id,  # so that 'id' looks the same as before
        "c_id": j.c.c_id,  # needed for disambiguation
        "d_c_id": j.c.d_c_id,  # needed for disambiguation
        "b_id": [j.c.b_id, j.c.d_b_id],
        "d_id": j.c.d_id,
    },
)

A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)

Die Eigenschaften waren notwendig, um die zusätzlichen Spalten neu zu mappen, damit sie nicht mit den bereits für B gemappten Spalten kollidierten, und es war notwendig, einen neuen Primärschlüssel zu definieren.

Mit dem neuen Ansatz verschwindet diese ganze Ausführlichkeit, und die zusätzlichen Spalten werden direkt bei der Erstellung der Beziehung referenziert:

j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)

B_viacd = aliased(B, j, flat=True)

A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)

Der nicht-primäre Mapper ist nun veraltet, mit dem Ziel, dass klassische Mappings als Funktion ganz verschwinden. Die Deklarative API würde das einzige Mittel zur Abbildung werden, was hoffentlich interne Verbesserungen und Vereinfachungen sowie eine klarere Dokumentationsstory ermöglicht.

#4423

selectin-Laden verwendet nicht mehr JOIN für einfache Eins-zu-Viele-Beziehungen

Das in 1.2 hinzugefügte "selectin"-Ladefeature führte eine extrem performante neue Methode zum eageren Laden von Sammlungen ein, in vielen Fällen viel schneller als das "subquery" eager loading, da es nicht auf die Wiederholung der ursprünglichen SELECT-Abfrage angewiesen ist und stattdessen eine einfache IN-Klausel verwendet. Der "selectin"-Ladevorgang verwendete jedoch weiterhin ein JOIN zwischen den Eltern- und den zugehörigen Tabellen, da er die Primärschlüsselwerte des Elternobjekts in der Zeile benötigte, um die Zeilen zuzuordnen. In 1.3 wurde eine neue Optimierung hinzugefügt, die dieses JOIN im häufigsten Fall eines einfachen Eins-zu-Viele-Ladevorgangs weglässt, bei dem die zugehörige Zeile bereits den Primärschlüssel der Elternzeile in ihren Fremdschlüsselspalten enthält. Dies sorgt wiederum für eine dramatische Leistungssteigerung, da das ORM nun große Mengen von Sammlungen in einer einzigen Abfrage laden kann, ohne JOINs oder Subqueries überhaupt zu verwenden.

Gegeben eine Zuordnung

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    bs = relationship("B", lazy="selectin")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

In der 1.2er-Version des "selectin"-Ladens sieht das Laden von A nach B so aus:

SELECT a.id AS a_id FROM a
SELECT a_1.id AS a_1_id, b.id AS b_id, b.a_id AS b_a_id
FROM a AS a_1 JOIN b ON a_1.id = b.a_id
WHERE a_1.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY a_1.id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Mit dem neuen Verhalten sieht das Laden so aus:

SELECT a.id AS a_id FROM a
SELECT b.a_id AS b_a_id, b.id AS b_id FROM b
WHERE b.a_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY b.a_id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Das Verhalten wird automatisch freigegeben und verwendet eine ähnliche Heuristik wie das lazy loading, um festzustellen, ob zugehörige Entitäten direkt aus der Identity Map geladen werden können. Wie bei den meisten Abfragefunktionen wurde die Implementierung aufgrund fortgeschrittener Szenarien bezüglich polymorpher Ladungen komplexer. Wenn Probleme auftreten, sollten Benutzer einen Fehler melden. Die Änderung beinhaltet jedoch auch ein Flag relationship.omit_join, das auf False auf der relationship() gesetzt werden kann, um die Optimierung zu deaktivieren.

#4340

Verbesserung des Verhaltens von Many-to-One-Abfrageausdrücken

Beim Erstellen einer Abfrage, die eine Many-to-One-Beziehung mit einem Objektwert vergleicht, z. B.

u1 = session.query(User).get(5)

query = session.query(Address).filter(Address.user == u1)

Der obige Ausdruck Address.user == u1, der letztendlich in einen SQL-Ausdruck kompiliert wird, der normalerweise auf den Primärschlüsselspalten des User-Objekts basiert, wie z. B. "address.user_id = 5", verwendet einen verzögerten Aufruf, um den Wert 5 innerhalb des gebundenen Ausdrucks so spät wie möglich abzurufen. Dies ist sowohl für den Anwendungsfall geeignet, bei dem der Ausdruck Address.user == u1 gegen ein noch nicht gefluschtes User-Objekt gerichtet ist, das auf einen vom Server generierten Primärschlüsselwert angewiesen ist, als auch dafür, dass der Ausdruck immer das richtige Ergebnis liefert, auch wenn sich der Primärschlüsselwert von u1 seit der Erstellung des Ausdrucks geändert hat.

Eine Nebenwirkung dieses Verhaltens ist jedoch, dass, wenn u1 zum Zeitpunkt der Auswertung des Ausdrucks abgelaufen ist, dies zu einer zusätzlichen SELECT-Anweisung führt, und falls u1 auch von der Session getrennt wurde, würde dies einen Fehler auslösen.

u1 = session.query(User).get(5)

query = session.query(Address).filter(Address.user == u1)

session.expire(u1)
session.expunge(u1)

query.all()  # <-- would raise DetachedInstanceError

Das Ablaufen / Entfernen des Objekts kann implizit auftreten, wenn die Session committet wird und die u1-Instanz aus dem Gültigkeitsbereich fällt, da der Ausdruck Address.user == u1 das Objekt selbst nicht stark referenziert, sondern nur seinen InstanceState.

Die Korrektur besteht darin, dass der Ausdruck Address.user == u1 den Wert 5 auswertet, indem er versucht, den Wert wie bisher abzurufen oder zu laden, aber wenn das Objekt getrennt und abgelaufen ist, wird es über einen neuen Mechanismus auf dem InstanceState abgerufen, der den zuletzt bekannten Wert für ein bestimmtes Attribut auf diesem Zustand memoisiert, wenn dieses Attribut abgelaufen ist. Dieser Mechanismus wird nur für ein bestimmtes Attribut / InstanceState aktiviert, wenn er von der Ausdrucksfunktion benötigt wird, um die Leistung / den Speicheraufwand zu sparen.

Ursprünglich wurden einfachere Ansätze versucht, wie die sofortige Auswertung des Ausdrucks mit verschiedenen Anordnungen, um den Wert später zu laden, wenn er nicht vorhanden ist. Der schwierige Sonderfall ist jedoch der Wert eines Spaltenattributs (typischerweise ein natürlicher Primärschlüssel), der geändert wird. Um sicherzustellen, dass ein Ausdruck wie Address.user == u1 immer die richtige Antwort für den aktuellen Zustand von u1 liefert, wird der aktuelle, in der Datenbank gespeicherte Wert für ein persistentes Objekt zurückgegeben, wobei bei Bedarf über eine SELECT-Abfrage abgelaufen wird, und für ein getrenntes Objekt wird der zuletzt bekannte Wert zurückgegeben, unabhängig davon, wann das Objekt abgelaufen ist, mithilfe einer neuen Funktion innerhalb des InstanceState, die den zuletzt bekannten Wert eines Spaltenattributs verfolgt, wenn das Attribut abgelaufen werden soll.

Moderne Attribut-API-Funktionen werden verwendet, um spezifische Fehlermeldungen zu generieren, wenn der Wert nicht ausgewertet werden kann. Die beiden Fälle sind, wenn die Spaltenattribute noch nie gesetzt wurden und wenn das Objekt beim ersten Auswerten bereits abgelaufen war und nun getrennt ist. In allen Fällen wird DetachedInstanceError nicht mehr ausgelöst.

#4359

Many-to-One-Ersetzung löst keine Fehler für "raiseload" oder detached für "alte" Objekte aus

Wenn ein lazy load für eine Many-to-One-Beziehung durchgeführt wird, um den "alten" Wert zu laden, und die Beziehung nicht das Flag relationship.active_history angibt, wird für ein getrenntes Objekt keine Assertion ausgelöst.

a1 = session.query(Address).filter_by(id=5).one()

session.expunge(a1)

a1.user = some_user

Oben, wenn das Attribut .user auf dem getrennten Objekt a1 ersetzt wird, würde eine DetachedInstanceError ausgelöst werden, da das Attribut versucht, den vorherigen Wert von .user aus der Identity Map abzurufen. Die Änderung besteht darin, dass die Operation nun ohne das Laden des alten Werts fortgesetzt wird.

Die gleiche Änderung wird auch für die lazy="raise" Loader-Strategie vorgenommen:

class Address(Base):
    # ...

    user = relationship("User", ..., lazy="raise")

Zuvor würde die Zuordnung von a1.user die "raiseload"-Ausnahme auslösen, da das Attribut versucht, den vorherigen Wert abzurufen. Diese Assertion wird nun im Fall des Ladens des "alten" Werts übersprungen.

#4353

"del" implementiert für ORM-Attribute

Die Python-Operation del war für gemappte Attribute, sowohl Skalarspalten als auch Objektverweise, nicht wirklich nutzbar. Die Unterstützung dafür wurde hinzugefügt, damit dies korrekt funktioniert, wobei die del-Operation im Wesentlichen dem Setzen des Attributs auf den Wert None entspricht.

some_object = session.query(SomeObject).get(5)

del some_object.some_attribute  # from a SQL perspective, works like "= None"

#4354

info-Dictionary hinzugefügt zu InstanceState

Das .info-Dictionary wurde zur Klasse InstanceState hinzugefügt, dem Objekt, das durch Aufrufen von inspect() auf einem gemappten Objekt erhalten wird. Dies ermöglicht benutzerdefinierten Rezepten, zusätzliche Informationen über ein Objekt hinzuzufügen, die zusammen mit dem gesamten Lebenszyklus dieses Objekts im Speicher mitgeführt werden.

from sqlalchemy import inspect

u1 = User(id=7, name="ed")

inspect(u1).info["user_info"] = "7|ed"

#4257

Horizontal Sharding Extension unterstützt Bulk-Update- und Delete-Methoden

Das ShardedQuery-Erweiterungsobjekt unterstützt die Bulk-Update-/Delete-Methoden Query.update() und Query.delete(). Der `query_chooser`-Callable wird aufgerufen, wenn sie mit bestimmten Kriterien aufgerufen werden, um das Update/Delete über mehrere Shards auszuführen.

#4196

Verbesserungen bei Association Proxy

Auch wenn es keinen besonderen Grund dafür gab, gab es im Association Proxy Extension in diesem Zyklus viele Verbesserungen.

Association Proxy hat ein neues Flag cascade_scalar_deletes

Gegeben eine Zuordnung als

class A(Base):
    __tablename__ = "test_a"
    id = Column(Integer, primary_key=True)
    ab = relationship("AB", backref="a", uselist=False)
    b = association_proxy(
        "ab", "b", creator=lambda b: AB(b=b), cascade_scalar_deletes=True
    )


class B(Base):
    __tablename__ = "test_b"
    id = Column(Integer, primary_key=True)
    ab = relationship("AB", backref="b", cascade="all, delete-orphan")


class AB(Base):
    __tablename__ = "test_ab"
    a_id = Column(Integer, ForeignKey(A.id), primary_key=True)
    b_id = Column(Integer, ForeignKey(B.id), primary_key=True)

Eine Zuweisung an A.b erzeugt ein AB-Objekt:

a.b = B()

Die A.b-Zuordnung ist skalar und enthält ein neues Flag AssociationProxy.cascade_scalar_deletes. Wenn dieses gesetzt ist, entfernt das Setzen von A.b auf None auch A.ab. Das Standardverhalten bleibt unverändert: a.ab wird beibehalten.

a.b = None
assert a.ab is None

Obwohl es zunächst intuitiv erschien, dass diese Logik nur das "cascade"-Attribut der bestehenden Beziehung berücksichtigen sollte, ist aus dieser allein nicht klar ersichtlich, ob das proxy-Objekt entfernt werden soll, weshalb das Verhalten als explizite Option zur Verfügung gestellt wird.

Zusätzlich funktioniert del nun auch für Skalare auf ähnliche Weise wie das Setzen auf None.

del a.b
assert a.ab is None

#4308

AssociationProxy speichert klassenspezifischen Zustand pro Klasse

Das AssociationProxy-Objekt trifft viele Entscheidungen basierend auf der übergeordneten gemappten Klasse, mit der es assoziiert ist. Während das AssociationProxy historisch als relativ einfacher "Getter" begann, wurde früh klar, dass es auch Entscheidungen bezüglich der Art des Attributs treffen musste, auf das es sich bezieht - wie z. B. skalar oder Sammlung, gemapptes Objekt oder einfacher Wert und so weiter. Um dies zu erreichen, muss es das gemappte Attribut oder einen anderen referenzierenden Deskriptor oder ein Attribut inspizieren, wie es von seiner übergeordneten Klasse referenziert wird. In den Python-Deskriptormechanismen erfährt ein Deskriptor jedoch nur dann von seiner "übergeordneten" Klasse, wenn er im Kontext dieser Klasse aufgerufen wird, z. B. durch Aufrufen von MyClass.some_descriptor, was die __get__()-Methode aufruft, die die Klasse übergibt. Das AssociationProxy-Objekt würde dann klassenspezifische Zustände speichern, aber erst, nachdem diese Methode aufgerufen wurde; der Versuch, diesen Zustand im Voraus zu inspizieren, ohne zuerst auf das AssociationProxy als Deskriptor zuzugreifen, würde einen Fehler auslösen. Darüber hinaus würde es davon ausgehen, dass die erste von __get__() gesehene Klasse die einzige übergeordnete Klasse ist, die es kennen muss. Dies geschieht, obwohl das association proxy, wenn eine bestimmte Klasse erbende Unterklassen hat, eigentlich für mehr als eine übergeordnete Klasse arbeitet, auch wenn es nicht explizit wiederverwendet wurde. Selbst mit dieser Einschränkung würde das association proxy mit seinem aktuellen Verhalten recht weit kommen, hinterlässt aber in einigen Fällen weiterhin Mängel sowie das komplexe Problem, die beste "Besitzer"-Klasse zu bestimmen.

Diese Probleme werden nun gelöst, indem das AssociationProxy seinen eigenen internen Zustand nicht mehr ändert, wenn __get__() aufgerufen wird; stattdessen wird pro Klasse ein neues Objekt namens AssociationProxyInstance generiert, das den gesamten klassenspezifischen Zustand verwaltet (wenn die übergeordnete Klasse nicht gemappt ist, wird keine AssociationProxyInstance generiert). Das Konzept einer einzelnen "Besitzerklasse" für das association proxy, das nonetheless in 1.1 verbessert wurde, wurde im Wesentlichen durch einen Ansatz ersetzt, bei dem die AP nun jede Anzahl von "Besitzer"-Klassen gleich behandeln kann.

Um Anwendungen zu unterstützen, die diesen Zustand für ein AssociationProxy inspizieren möchten, ohne unbedingt __get__() aufzurufen, wird eine neue Methode AssociationProxy.for_class() hinzugefügt, die direkten Zugriff auf eine klassenspezifische AssociationProxyInstance bietet, wie hier gezeigt:

class User(Base):
    # ...

    keywords = association_proxy("kws", "keyword")


proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)

Sobald wir das Objekt AssociationProxyInstance haben, das im obigen Beispiel in der Variablen proxy_state gespeichert ist, können wir Attribute betrachten, die spezifisch für den User.keywords-Proxy sind, wie z. B. target_class.

>>> proxy_state.target_class
Keyword

#3423

AssociationProxy bietet jetzt Standard-Spaltenoperatoren für ein spaltenorientiertes Ziel

Gegeben ein AssociationProxy, bei dem das Ziel eine Datenbankspalte ist und keine Objekt-Referenz oder ein anderer Association Proxy ist

class User(Base):
    # ...

    elements = relationship("Element")

    # column-based association proxy
    values = association_proxy("elements", "value")


class Element(Base):
    # ...

    value = Column(String)

Der User.values Association Proxy verweist auf die Element.value Spalte. Standardmäßige Spaltenoperationen sind nun verfügbar, wie z.B. like

>>> print(s.query(User).filter(User.values.like("%foo%")))
SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND element.value LIKE :value_1)

gleich:

>>> print(s.query(User).filter(User.values == "foo"))
SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND element.value = :value_1)

Beim Vergleich mit None wird der Ausdruck IS NULL um einen Test erweitert, der besagt, dass die zugehörige Zeile überhaupt nicht existiert; dies ist das gleiche Verhalten wie zuvor.

>>> print(s.query(User).filter(User.values == None))
SELECT "user".id AS user_id FROM "user" WHERE (EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND element.value IS NULL)) OR NOT (EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id))

Beachten Sie, dass der Operator ColumnOperators.contains() tatsächlich ein String-Vergleichsoperator ist; dies ist eine Verhaltensänderung, da der Association Proxy zuvor .contains nur als Operator für die Mitgliedschaft in Listen verwendete. Mit einem spaltenorientierten Vergleich verhält er sich nun wie "like".

>>> print(s.query(User).filter(User.values.contains("foo")))
SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND (element.value LIKE '%' || :value_1 || '%'))

Um die User.values Sammlung auf einfache Mitgliedschaft des Werts "foo" zu testen, sollte der Gleichheitsoperator (z. B. User.values == 'foo') verwendet werden; dies funktioniert auch in früheren Versionen.

Bei Verwendung eines objektbasierten Association Proxys mit einer Sammlung ist das Verhalten wie zuvor, nämlich das Testen auf Mitgliedschaft in der Sammlung, z.B. gegeben eine Zuordnung

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    user_elements = relationship("UserElement")

    # object-based association proxy
    elements = association_proxy("user_elements", "element")


class UserElement(Base):
    __tablename__ = "user_element"

    id = Column(Integer, primary_key=True)
    user_id = Column(ForeignKey("user.id"))
    element_id = Column(ForeignKey("element.id"))
    element = relationship("Element")


class Element(Base):
    __tablename__ = "element"

    id = Column(Integer, primary_key=True)
    value = Column(String)

Die Methode .contains() erzeugt denselben Ausdruck wie zuvor und prüft die Liste von User.elements auf die Anwesenheit eines Element -Objekts.

>>> print(s.query(User).filter(User.elements.contains(Element(id=1))))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_element
WHERE "user".id = user_element.user_id AND :param_1 = user_element.element_id)

Insgesamt wird die Änderung durch die architektonische Änderung ermöglicht, die Teil von AssociationProxy speichert klassenspezifischen Zustand auf Basis pro Klasse ist; da der Proxy nun beim Erstellen eines Ausdrucks zusätzliche Zustände abzweigt, gibt es sowohl eine objektorientierte als auch eine spaltenorientierte Version der Klasse AssociationProxyInstance.

#4351

Association Proxy referenziert nun das Elternobjekt stark

Das langjährige Verhalten der Association Proxy Collection, die nur eine schwache Referenz auf das Elternobjekt unterhält, wird rückgängig gemacht; der Proxy unterhält nun eine starke Referenz auf das Elternteil, solange die Proxy Collection selbst im Speicher ist, wodurch der Fehler "stale association proxy" behoben wird. Diese Änderung erfolgt experimentell, um zu sehen, ob Anwendungsfälle auftreten, bei denen sie Nebenwirkungen verursacht.

Als Beispiel, gegeben eine Zuordnung mit Association Proxy

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    bs = relationship("B")
    b_data = association_proxy("bs", "data")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    data = Column(String)


a1 = A(bs=[B(data="b1"), B(data="b2")])

b_data = a1.b_data

Zuvor, wenn a1 aus dem Gültigkeitsbereich gelöscht wurde

del a1

Der Versuch, die b_data Sammlung zu durchlaufen, nachdem a1 aus dem Gültigkeitsbereich gelöscht wurde, würde den Fehler "stale association proxy, parent object has gone out of scope" auslösen. Dies liegt daran, dass der Association Proxy auf die tatsächliche a1.bs Sammlung zugreifen muss, um eine Ansicht zu erzeugen, und davor nur eine schwache Referenz auf a1 unterhielt. Insbesondere stießen Benutzer häufig auf diesen Fehler, wenn sie eine Inline-Operation durchführten, wie z. B.:

collection = session.query(A).filter_by(id=1).first().b_data

Oben, da das A-Objekt vor der tatsächlichen Verwendung der b_data-Sammlung vom Garbage Collector bereinigt würde.

Die Änderung besteht darin, dass die b_data-Sammlung nun eine starke Referenz auf das a1-Objekt unterhält, so dass es vorhanden bleibt.

assert b_data == ["b1", "b2"]

Diese Änderung führt zu der Nebenwirkung, dass, wenn eine Anwendung die Sammlung wie oben übergibt, das Elternobjekt nicht vom Garbage Collector bereinigt wird, bis die Sammlung ebenfalls verworfen ist. Wie immer, wenn a1 in einer bestimmten Session persistent ist, bleibt es Teil des Zustands dieser Sitzung, bis es vom Garbage Collector bereinigt wird.

Beachten Sie, dass diese Änderung überarbeitet werden kann, falls sie zu Problemen führt.

#4268

Implementierter Massen-Ersatz für Sets, Dictionaries mit AssociationProxy

Die Zuweisung eines Sets oder Dictionaries an eine Association Proxy Collection sollte nun korrekt funktionieren, während sie zuvor für vorhandene Schlüssel Association Proxy Mitglieder neu erstellt hätte, was zu potenziellen Flush-Fehlern aufgrund des Löschens+Einfügens desselben Objekts führte. Nun sollten nur noch neue Association-Objekte erstellt werden, wo dies angebracht ist.

class A(Base):
    __tablename__ = "test_a"

    id = Column(Integer, primary_key=True)
    b_rel = relationship(
        "B",
        collection_class=set,
        cascade="all, delete-orphan",
    )
    b = association_proxy("b_rel", "value", creator=lambda x: B(value=x))


class B(Base):
    __tablename__ = "test_b"
    __table_args__ = (UniqueConstraint("a_id", "value"),)

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("test_a.id"), nullable=False)
    value = Column(String)


# ...

s = Session(e)
a = A(b={"x", "y", "z"})
s.add(a)
s.commit()

# re-assign where one B should be deleted, one B added, two
# B's maintained
a.b = {"x", "z", "q"}

# only 'q' was added, so only one new B object.  previously
# all three would have been re-created leading to flush conflicts
# against the deleted ones.
assert len(s.new) == 1

#2642

Many-to-one Backref prüft auf Duplikate in der Sammlung während des Entfernvorgangs

Wenn eine ORM-gemappte Sammlung, die als Python-Sequenz existierte, typischerweise eine Python list, wie es die Standardeinstellung für relationship() ist, Duplikate enthielt und das Objekt aus einer seiner Positionen, aber nicht aus anderen entfernt wurde, würde eine Many-to-one Backref ihr Attribut auf None setzen, obwohl die One-to-Many-Seite das Objekt immer noch als vorhanden darstellte. Obwohl One-to-Many-Sammlungen im relationalen Modell keine Duplikate haben können, kann eine ORM-gemappte relationship(), die eine Sequenz-Sammlung verwendet, Duplikate darin im Speicher enthalten, mit der Einschränkung, dass dieser Duplikat-Zustand weder in die Datenbank geschrieben noch daraus abgerufen werden kann. Insbesondere ist das vorübergehende Vorhandensein eines Duplikats in der Liste für eine Python-"Swap"-Operation wesentlich. Gegeben ein Standard One-to-Many/Many-to-One-Setup

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    bs = relationship("B", backref="a")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

Wenn wir ein A-Objekt mit zwei B-Mitgliedern haben und einen Swap durchführen

a1 = A(bs=[B(), B()])

a1.bs[0], a1.bs[1] = a1.bs[1], a1.bs[0]

Während des obigen Vorgangs liefert die Abfangen der Standard-Python-Methoden __setitem__ und __delitem__ einen Zwischenzustand, in dem das zweite B()-Objekt zweimal in der Sammlung vorhanden ist. Wenn das B()-Objekt aus einer der Positionen entfernt wird, würde die B.a-Backref die Referenz auf None setzen, was während des Flushens die Verbindung zwischen dem A- und dem B-Objekt entfernt. Das gleiche Problem kann mit einfachen Duplikaten demonstriert werden.

>>> a1 = A()
>>> b1 = B()
>>> a1.bs.append(b1)
>>> a1.bs.append(b1)  # append the same b1 object twice
>>> del a1.bs[1]
>>> a1.bs  # collection is unaffected so far...
[<__main__.B object at 0x7f047af5fb70>]
>>> b1.a  # however b1.a is None
>>>
>>> session.add(a1)
>>> session.commit()  # so upon flush + expire....
>>> a1.bs  # the value is gone
[]

Die Korrektur stellt sicher, dass beim Auslösen der Backref, die vor der Mutation der Sammlung erfolgt, die Sammlung auf genau eine oder keine Instanz des Zielobjekts geprüft wird, bevor die Many-to-one-Seite aufgehoben wird, und zwar mit einer linearen Suche, die derzeit list.search und list.__contains__ nutzt.

Ursprünglich wurde angenommen, dass ein ereignisbasierter Referenzzählungsmechanismus innerhalb der Sammlungsinternen benötigt würde, damit alle Duplikatinstanzen während des gesamten Lebenszyklus der Sammlung verfolgt werden könnten, was die Leistung/Speicher/Komplexität aller Sammlungsvorgänge, einschließlich der sehr häufigen Vorgänge des Ladens und Hinzufügens, erhöhen würde. Der stattdessen gewählte Ansatz beschränkt die zusätzlichen Kosten auf die weniger häufigen Operationen des Entfernens und Massen-Ersetzens von Sammlungen, und die beobachtete Überlastung des linearen Scans ist vernachlässigbar; lineare Scans von beziehungsgebundenen Sammlungen werden bereits innerhalb der Transaktion sowie beim Massen-Ersetzen einer Sammlung verwendet.

#1103

Wichtige Verhaltensänderungen – ORM

Query.join() behandelt Mehrdeutigkeiten bei der Entscheidung der "linken" Seite expliziter

Historisch gesehen würde eine Abfrage wie die folgende

u_alias = aliased(User)
session.query(User, u_alias).join(Address)

bei den Standard-Tutorial-Zuordnungen eine FROM-Klausel wie folgt erzeugen:

SELECT ...
FROM users AS users_1, users JOIN addresses ON users.id = addresses.user_id

Das heißt, der JOIN wäre implizit gegen die erste übereinstimmende Entität. Das neue Verhalten ist, dass eine Ausnahme verlangt, dass diese Mehrdeutigkeit aufgelöst wird.

sqlalchemy.exc.InvalidRequestError: Can't determine which FROM clause to
join from, there are multiple FROMS which can join to this entity.
Try adding an explicit ON clause to help resolve the ambiguity.

Die Lösung besteht darin, eine ON-Klausel bereitzustellen, entweder als Ausdruck

# join to User
session.query(User, u_alias).join(Address, Address.user_id == User.id)

# join to u_alias
session.query(User, u_alias).join(Address, Address.user_id == u_alias.id)

Oder das relationship-Attribut zu verwenden, falls vorhanden.

# join to User
session.query(User, u_alias).join(Address, User.addresses)

# join to u_alias
session.query(User, u_alias).join(Address, u_alias.addresses)

Die Änderung beinhaltet, dass ein Join nun korrekt mit einer FROM-Klausel verknüpft werden kann, die nicht das erste Element in der Liste ist, wenn der Join ansonsten eindeutig ist.

session.query(func.current_timestamp(), User).join(Address)

Vor dieser Verbesserung würde die obige Abfrage Folgendes auslösen:

sqlalchemy.exc.InvalidRequestError: Don't know how to join from
CURRENT_TIMESTAMP; please use select_from() to establish the
left entity/selectable of this join

Nun funktioniert die Abfrage einwandfrei.

SELECT CURRENT_TIMESTAMP AS current_timestamp_1, users.id AS users_id,
users.name AS users_name, users.fullname AS users_fullname,
users.password AS users_password
FROM users JOIN addresses ON users.id = addresses.user_id

Insgesamt geht die Änderung direkt auf die Philosophie von Python "explizit ist besser als implizit" zurück.

#4365

FOR UPDATE-Klausel wird innerhalb der joined eager load Subquery sowie außerhalb gerendert

Diese Änderung gilt speziell für die Verwendung der joinedload() Lade-Strategie in Verbindung mit einer zeilenbegrenzten Abfrage, z.B. mit Query.first() oder Query.limit(), sowie bei Verwendung der Methode Query.with_for_update().

Gegeben eine Abfrage wie

session.query(A).options(joinedload(A.b)).limit(5)

Das Query-Objekt rendert ein SELECT der folgenden Form, wenn joined eager loading mit LIMIT kombiniert wird.

SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
    SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id

Dies dient dazu, dass das Zeilenlimit für die primäre Entität gilt, ohne das joined eager load der zugehörigen Elemente zu beeinträchtigen. Wenn die obige Abfrage mit "SELECT..FOR UPDATE" kombiniert wird, war das Verhalten dieses:

SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
    SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE

MySQL erlaubt jedoch aufgrund von https://bugs.mysql.com/bug.php?id=90693 keine Sperrung der Zeilen innerhalb der Subquery, im Gegensatz zu PostgreSQL und anderen Datenbanken. Daher wird die obige Abfrage nun wie folgt gerendert:

SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
    SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE

Auf dem Oracle-Dialekt wird das innere "FOR UPDATE" nicht gerendert, da Oracle diese Syntax nicht unterstützt und der Dialekt "FOR UPDATE"-Anweisungen, die sich gegen eine Subquery richten, überspringt; dies ist ohnehin nicht notwendig, da Oracle, wie PostgreSQL, alle Elemente der zurückgegebenen Zeile korrekt sperrt.

Bei Verwendung des Modifikators Query.with_for_update.of wird, typischerweise auf PostgreSQL, das äußere "FOR UPDATE" weggelassen, und das OF wird nun nach innen gerendert; zuvor wurde das OF-Ziel nicht konvertiert, um die Subquery korrekt zu berücksichtigen. Gegeben also

session.query(A).options(joinedload(A.b)).with_for_update(of=A).limit(5)

Die Abfrage würde nun wie folgt gerendert werden:

SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
    SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE OF a
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id

Die obige Form dürfte auf PostgreSQL zusätzlich hilfreich sein, da PostgreSQL die Klausel FOR UPDATE nicht nach dem LEFT OUTER JOIN-Ziel zulässt.

Insgesamt bleibt FOR UPDATE sehr spezifisch für die verwendete Zieldatenbank und kann für komplexere Abfragen nicht einfach verallgemeinert werden.

#4246

passive_deletes='all' lässt FK unverändert für aus Sammlung entferntes Objekt

Die Option relationship.passive_deletes akzeptiert den Wert "all", um anzuzeigen, dass keine Fremdschlüsselattribute geändert werden sollen, wenn das Objekt geflusht wird, auch wenn die Sammlung / Referenz der Beziehung entfernt wurde. Zuvor geschah dies nicht für One-to-Many- oder One-to-One-Beziehungen in der folgenden Situation:

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    addresses = relationship("Address", passive_deletes="all")


class Address(Base):
    __tablename__ = "addresses"
    id = Column(Integer, primary_key=True)
    email = Column(String)

    user_id = Column(Integer, ForeignKey("users.id"))
    user = relationship("User")


u1 = session.query(User).first()
address = u1.addresses[0]
u1.addresses.remove(address)
session.commit()

# would fail and be set to None
assert address.user_id == u1.id

Die Korrektur beinhaltet nun, dass address.user_id unverändert bleibt, gemäß passive_deletes="all". Diese Art von Funktionalität ist nützlich für den Aufbau benutzerdefinierter "Versionstabellen"-Schemata und dergleichen, bei denen Zeilen archiviert und nicht gelöscht werden.

#3844

Neue Features und Verbesserungen - Core

Neue Token für Mehrspalten-Namenskonventionen, lange Namen werden gekürzt

Um den Fall zu berücksichtigen, dass eine Namenskonvention für MetaData zwischen Mehrspalten-Constraints unterscheiden muss und alle Spalten im generierten Constraint-Namen verwenden möchte, wird eine neue Reihe von Namenskonvention-Token hinzugefügt, darunter column_0N_name, column_0_N_name, column_0N_key, column_0_N_key, referred_column_0N_name, referred_column_0_N_name usw., die den Spaltennamen (oder Schlüssel oder Label) für alle Spalten im Constraint rendern, verbunden entweder ohne Trennzeichen oder mit einem Unterstrich-Trennzeichen. Unten definieren wir eine Konvention, die UniqueConstraint Constraints mit einem Namen versieht, der die Namen aller Spalten zusammenfügt.

metadata_obj = MetaData(
    naming_convention={"uq": "uq_%(table_name)s_%(column_0_N_name)s"}
)

table = Table(
    "info",
    metadata_obj,
    Column("a", Integer),
    Column("b", Integer),
    Column("c", Integer),
    UniqueConstraint("a", "b", "c"),
)

Die CREATE TABLE-Anweisung für die obige Tabelle wird wie folgt gerendert:

CREATE TABLE info (
    a INTEGER,
    b INTEGER,
    c INTEGER,
    CONSTRAINT uq_info_a_b_c UNIQUE (a, b, c)
)

Darüber hinaus wird die Logik zur Kürzung langer Namen nun auf die von Namenskonventionen generierten Namen angewendet, insbesondere um Mehrspalten-Labels zu berücksichtigen, die sehr lange Namen erzeugen können. Diese Logik, die identisch mit der zum Kürzen langer Label-Namen in einer SELECT-Anweisung ist, ersetzt überschüssige Zeichen, die das Bezeichnerlängenlimit für die Zieldatenbank überschreiten, durch einen deterministisch generierten 4-stelligen Hash. Zum Beispiel wird auf PostgreSQL, wo Bezeichner nicht länger als 63 Zeichen sein dürfen, ein langer Constraint-Name normalerweise aus der folgenden Tabellendefinition generiert:

long_names = Table(
    "long_names",
    metadata_obj,
    Column("information_channel_code", Integer, key="a"),
    Column("billing_convention_name", Integer, key="b"),
    Column("product_identifier", Integer, key="c"),
    UniqueConstraint("a", "b", "c"),
)

Die Kürzungungslogik stellt sicher, dass kein zu langer Name für den UNIQUE-Constraint generiert wird.

CREATE TABLE long_names (
    information_channel_code INTEGER,
    billing_convention_name INTEGER,
    product_identifier INTEGER,
    CONSTRAINT uq_long_names_information_channel_code_billing_conventi_a79e
    UNIQUE (information_channel_code, billing_convention_name, product_identifier)
)

Das obige Suffix a79e basiert auf dem MD5-Hash des langen Namens und erzeugt jedes Mal denselben Wert, um konsistente Namen für ein gegebenes Schema zu erzeugen.

Beachten Sie, dass die Kürzungungslogik auch IdentifierError auslöst, wenn ein Constraint-Name für ein bestimmtes Dialekt explizit zu groß ist. Dies ist seit langem das Verhalten für ein Index-Objekt, wird nun aber auch auf andere Arten von Constraints angewendet.

from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import Table
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects import postgresql
from sqlalchemy.schema import AddConstraint

m = MetaData()
t = Table("t", m, Column("x", Integer))
uq = UniqueConstraint(
    t.c.x,
    name="this_is_too_long_of_a_name_for_any_database_backend_even_postgresql",
)

print(AddConstraint(uq).compile(dialect=postgresql.dialect()))

wird ausgeben

sqlalchemy.exc.IdentifierError: Identifier
'this_is_too_long_of_a_name_for_any_database_backend_even_postgresql'
exceeds maximum length of 63 characters

Die Ausnahme verhindert die Erzeugung von nicht-deterministischen Constraint-Namen, die vom Datenbank-Backend gekürzt werden und dann nicht mit zukünftigen Datenbankmigrationen kompatibel sind.

Um die Kürzungungsregeln auf SQLAlchemy-Seite auf den obigen Bezeichner anzuwenden, verwenden Sie das Konstrukt conv().

uq = UniqueConstraint(
    t.c.x,
    name=conv("this_is_too_long_of_a_name_for_any_database_backend_even_postgresql"),
)

Dies wird wieder deterministisch gekürzte SQL-Ausgaben erzeugen, wie in

ALTER TABLE t ADD CONSTRAINT this_is_too_long_of_a_name_for_any_database_backend_eve_ac05 UNIQUE (x)

Es gibt derzeit keine Option, die Namen durchlaufen zu lassen, um die Datenbank-seitige Kürzung zu ermöglichen. Dies war für Index-Namen schon länger der Fall und es gab keine Beschwerden.

Die Änderung behebt auch zwei weitere Probleme. Eines ist, dass das Token column_0_key nicht verfügbar war, obwohl dieses Token dokumentiert war. Das andere war, dass das Token referred_column_0_name versehentlich den .key und nicht den .name der Spalte gerendert hätte, wenn diese beiden Werte unterschiedlich waren.

#3989

Binäre Vergleichsauswertung für SQL-Funktionen

Diese Erweiterung wird auf Core-Ebene implementiert, ist aber hauptsächlich für ORM anwendbar.

Eine SQL-Funktion, die zwei Elemente vergleicht, kann nun als "Vergleichsobjekt" verwendet werden, das sich für die Verwendung in einer ORM relationship() eignet, indem zuerst die Funktion wie gewohnt mit der Factory func erstellt wird, und dann, wenn die Funktion abgeschlossen ist, der Modifier FunctionElement.as_comparison() aufgerufen wird, um eine BinaryExpression zu erzeugen, die eine "linke" und eine "rechte" Seite hat.

class Venue(Base):
    __tablename__ = "venue"
    id = Column(Integer, primary_key=True)
    name = Column(String)

    descendants = relationship(
        "Venue",
        primaryjoin=func.instr(remote(foreign(name)), name + "/").as_comparison(1, 2)
        == 1,
        viewonly=True,
        order_by=name,
    )

Oben wird die relationship.primaryjoin der "descendants"-Beziehung einen "linken" und einen "rechten" Ausdruck basierend auf dem ersten und zweiten Argument von instr() erzeugen. Dies ermöglicht Funktionen wie das ORM lazyload, um SQL wie folgt zu erzeugen:

SELECT venue.id AS venue_id, venue.name AS venue_name
FROM venue
WHERE instr(venue.name, (? || ?)) = ? ORDER BY venue.name
('parent1', '/', 1)

und ein joinedload, wie z.B.

v1 = (
    s.query(Venue)
    .filter_by(name="parent1")
    .options(joinedload(Venue.descendants))
    .one()
)

funktioniert wie

SELECT venue.id AS venue_id, venue.name AS venue_name,
  venue_1.id AS venue_1_id, venue_1.name AS venue_1_name
FROM venue LEFT OUTER JOIN venue AS venue_1
  ON instr(venue_1.name, (venue.name || ?)) = ?
WHERE venue.name = ? ORDER BY venue_1.name
('/', 1, 'parent1')

Es wird erwartet, dass diese Funktion bei Situationen hilft, wie z.B. der Verwendung von Geometriefunktionen in Beziehung-Join-Bedingungen oder in Fällen, in denen die ON-Klausel des SQL-Joins durch eine SQL-Funktion ausgedrückt wird.

#3831

Die erweiterte IN-Funktion unterstützt nun leere Listen

Die in Version 1.2 unter Spät erweiterte IN-Parameter-Sets ermöglichen IN-Ausdrücke mit gecachten Statements eingeführte Funktion "erweiterte IN" unterstützt nun leere Listen, die an den Operator ColumnOperators.in_() übergeben werden. Die Implementierung für eine leere Liste erzeugt einen "leeren Satz"-Ausdruck, der für ein bestimmtes Backend spezifisch ist, wie z.B. "SELECT CAST(NULL AS INTEGER) WHERE 1!=1" für PostgreSQL, "SELECT 1 FROM (SELECT 1) as _empty_set WHERE 1!=1" für MySQL.

>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
...     conn.execute(
...         select([literal_column("1")]).where(
...             literal_column("1").in_(bindparam("q", expanding=True))
...         ),
...         q=[],
...     )
{exexsql}SELECT 1 WHERE 1 IN (SELECT CAST(NULL AS INTEGER) WHERE 1!=1)

Die Funktion funktioniert auch für Tupel-orientierte IN-Anweisungen, bei denen der "leere IN"-Ausdruck erweitert wird, um die innerhalb des Tupels angegebenen Elemente zu unterstützen, wie z. B. auf PostgreSQL.

>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, tuple_, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
...     conn.execute(
...         select([literal_column("1")]).where(
...             tuple_(50, "somestring").in_(bindparam("q", expanding=True))
...         ),
...         q=[],
...     )
{exexsql}SELECT 1 WHERE (%(param_1)s, %(param_2)s)
IN (SELECT CAST(NULL AS INTEGER), CAST(NULL AS VARCHAR) WHERE 1!=1)

#4271

TypeEngine-Methoden bind_expression, column_expression arbeiten mit Variant, typspezifischen Typen

Die Methoden TypeEngine.bind_expression() und TypeEngine.column_expression() funktionieren nun, wenn sie auf dem "impl" eines bestimmten Datentyps vorhanden sind, was es Dialekten sowie Anwendungsfällen von TypeDecorator und Variant ermöglicht, diese Methoden zu verwenden.

Das folgende Beispiel illustriert eine TypeDecorator, die SQL-Zeitkonvertierungsfunktionen auf ein LargeBinary anwendet. Damit dieser Typ im Kontext eines Variant funktioniert, muss der Compiler in das "impl" des Variant-Ausdrucks bohren, um diese Methoden zu finden.

from sqlalchemy import TypeDecorator, LargeBinary, func


class CompressedLargeBinary(TypeDecorator):
    impl = LargeBinary

    def bind_expression(self, bindvalue):
        return func.compress(bindvalue, type_=self)

    def column_expression(self, col):
        return func.uncompress(col, type_=self)


MyLargeBinary = LargeBinary().with_variant(CompressedLargeBinary(), "sqlite")

Der obige Ausdruck wird nur auf SQLite eine Funktion in SQL rendern.

from sqlalchemy import select, column
from sqlalchemy.dialects import sqlite

print(select([column("x", CompressedLargeBinary)]).compile(dialect=sqlite.dialect()))

wird rendern

SELECT uncompress(x) AS x

Die Änderung beinhaltet auch, dass Dialekte TypeEngine.bind_expression() und TypeEngine.column_expression() auf dialekt-level Implementierungstypen implementieren können, wo sie nun verwendet werden; dies wird insbesondere für MySQLs neue Anforderung eines "binären Präfixes" sowie für die Umwandlung von Dezimalbindewerten für MySQL verwendet.

#3981

Neue Last-In-First-Out-Strategie für QueuePool

Der von create_engine() normalerweise verwendete Connection Pool wird QueuePool genannt. Dieser Pool verwendet ein Objekt, das der integrierten Queue-Klasse von Python entspricht, um Datenbankverbindungen zu speichern, die darauf warten, verwendet zu werden. Die Queue weist ein First-In-First-Out-Verhalten auf, das dazu bestimmt ist, die Datenbankverbindungen, die sich dauerhaft im Pool befinden, im Round-Robin-Verfahren zu nutzen. Ein potenzieller Nachteil ist jedoch, dass bei geringer Auslastung des Pools die serielle Wiederverwendung jeder Verbindung verhindert, dass eine serverseitige Timeout-Strategie diese Verbindungen herunterfährt. Um diesem Anwendungsfall gerecht zu werden, wird eine neue Flagge create_engine.pool_use_lifo hinzugefügt, die die Methode .get() der Queue umkehrt, um die Verbindung vom Anfang der Warteschlange anstelle vom Ende zu ziehen, was die "Warteschlange" im Wesentlichen zu einem "Stapel" macht (die Hinzufügung eines ganz neuen Pools namens StackPool wurde in Erwägung gezogen, war aber zu ausführlich).

Wichtige Änderungen – Core

Koexistenz von String-SQL-Fragmenten zu text() vollständig entfernt

Die Warnungen, die zuerst in Version 1.0 hinzugefügt wurden und unter Warnungen bei der Umwandlung vollständiger SQL-Fragmente in text() beschrieben sind, wurden nun in Ausnahmen umgewandelt. Fortgesetzte Bedenken wurden hinsichtlich der automatischen Umwandlung von String-Fragmenten, die an Methoden wie Query.filter() und Select.order_by() übergeben werden, in text()-Konstrukte geäußert, obwohl dies eine Warnung erzeugt hat. Im Fall von Select.order_by(), Query.order_by(), Select.group_by() und Query.group_by() wird ein String-Label oder Spaltenname immer noch in den entsprechenden Ausdruckskonstrukt aufgelöst, jedoch wenn die Auflösung fehlschlägt, wird eine CompileError ausgelöst, wodurch die direkte Wiedergabe von rohem SQL-Text verhindert wird.

#4481

„threadlocal“-Engine-Strategie veraltet

Die "threadlocal engine strategy" wurde um SQLAlchemy 0.2 herum hinzugefügt, um das Problem zu lösen, dass der Standardweg des Arbeitens in SQLAlchemy 0.1, der sich als "threadlocal alles" zusammenfassen lässt, als unzureichend empfunden wurde. Rückblickend erscheint es ziemlich absurd, dass in den ersten Releases von SQLAlchemy, die in jeder Hinsicht "alpha" waren, Bedenken geäußert wurden, dass zu viele Benutzer sich bereits an die bestehende API gewöhnt hatten, um sie einfach zu ändern.

Das ursprüngliche Nutzungsmodell für SQLAlchemy sah so aus:

engine.begin()

table.insert().execute(parameters)
result = table.select().execute()

table.update().execute(parameters)

engine.commit()

Nach einigen Monaten praktischer Nutzung war klar, dass der Versuch, eine "Verbindung" oder eine "Transaktion" als verstecktes Implementierungsdetail auszugeben, eine schlechte Idee war, insbesondere sobald jemand mehr als eine Datenbankverbindung gleichzeitig handhaben musste. So wurde das heute gesehene Nutzungsparadigma eingeführt, abzüglich der Kontextmanager, da diese in Python noch nicht existierten.

conn = engine.connect()
try:
    trans = conn.begin()

    conn.execute(table.insert(), parameters)
    result = conn.execute(table.select())

    conn.execute(table.update(), parameters)

    trans.commit()
except:
    trans.rollback()
    raise
finally:
    conn.close()

Das obige Paradigma war das, was die Leute brauchten, aber da es (wegen fehlender Kontextmanager) immer noch etwas umständlich war, wurde der alte Arbeitsweg beibehalten und wurde zur threadlocal engine strategy.

Heute ist die Arbeit mit Core dank Kontextmanagern viel prägnanter und noch prägnanter als das ursprüngliche Muster.

with engine.begin() as conn:
    conn.execute(table.insert(), parameters)
    result = conn.execute(table.select())

    conn.execute(table.update(), parameters)

Zu diesem Zeitpunkt wird jeglicher verbleibende Code, der sich noch auf den "threadlocal"-Stil verlässt, durch diese Deprecation aufgefordert, sich zu modernisieren - die Funktion soll in der nächsten Hauptversion von SQLAlchemy, z. B. 1.4, vollständig entfernt werden. Der Connection-Pool-Parameter Pool.use_threadlocal ist ebenfalls veraltet, da er in den meisten Fällen keine Auswirkungen hat, ebenso wie die Methode Engine.contextual_connect(), die normalerweise mit der Methode Engine.connect() gleichbedeutend ist, außer wenn die threadlocal Engine in Gebrauch ist.

#4393

convert_unicode-Parameter veraltet

Die Parameter String.convert_unicode und create_engine.convert_unicode sind veraltet. Der Zweck dieser Parameter war es, SQLAlchemy anzuweisen, sicherzustellen, dass eingehende Python-Unicode-Objekte unter Python 2 in Byte-Strings kodiert wurden, bevor sie an die Datenbank übergeben wurden, und Byte-Strings aus der Datenbank zu erwarten und zurück in Python-Unicode-Objekte zu konvertieren. In der Ära vor Python 3 war dies eine enorme Aufgabe, um sie richtig zu machen, da praktisch alle Python DBAPIs standardmäßig keine Unicode-Unterstützung aktiviert hatten und die meisten erhebliche Probleme mit den Unicode-Erweiterungen hatten, die sie bereitstellten. Schließlich fügte SQLAlchemy C-Erweiterungen hinzu, deren Hauptzweck darin bestand, den Unicode-Dekodierungsprozess innerhalb von Ergebnismengen zu beschleunigen.

Nach der Einführung von Python 3 begannen DBAPIs, Unicode umfassender und vor allem standardmäßig zu unterstützen. Die Bedingungen, unter denen eine bestimmte DBAPI Unicode-Daten aus einem Ergebnis zurückgab oder nicht, sowie Python-Unicode-Werte als Parameter akzeptierte, blieben jedoch äußerst kompliziert. Dies war der Beginn der Obsoleszenz der "convert_unicode"-Flags, da sie nicht mehr ausreichten, um sicherzustellen, dass die Kodierung/Dekodierung nur dort erfolgte, wo sie benötigt wurde, und nicht dort, wo sie nicht benötigt wurde. Stattdessen wurde "convert_unicode" von Dialekten automatisch erkannt. Teilweise ist dies in den SQL-Anweisungen "SELECT 'test plain returns'" und "SELECT 'test_unicode_returns'" sichtbar, die eine Engine beim ersten Verbindungsaufbau ausgibt; der Dialekt testet, ob die aktuelle DBAPI mit ihren aktuellen Einstellungen und der Backend-Datenbankverbindung standardmäßig Unicode zurückgibt oder nicht.

Das Endergebnis ist, dass die Verwendung der "convert_unicode"-Flags durch den Endbenutzer unter keinen Umständen mehr erforderlich sein sollte. Wenn doch, muss das SQLAlchemy-Projekt wissen, welche Fälle dies sind und warum. Derzeit bestehen Hunderte von Unicode-Round-Trip-Tests über alle großen Datenbanken hinweg ohne die Verwendung dieses Flags, sodass es ein ziemlich hohes Vertrauen gibt, dass sie nicht mehr benötigt werden, außer in argumentativ nicht verwendeten Fällen wie dem Zugriff auf falsch kodierte Daten aus einer Legacy-Datenbank, was besser mit benutzerdefinierten Typen gehandhabt werden sollte.

#4393

Dialektverbesserungen und Änderungen - PostgreSQL

Grundlegende Reflexionsunterstützung für PostgreSQL-partitionierte Tabellen hinzugefügt

SQLAlchemy kann die "PARTITION BY"-Sequenz in einer PostgreSQL CREATE TABLE-Anweisung mit der in Version 1.2.6 hinzugefügten Flagge postgresql_partition_by rendern. Der Typ 'p' war jedoch bis jetzt nicht Teil der verwendeten Reflexionsabfragen.

Gegeben ein Schema wie

dv = Table(
    "data_values",
    metadata_obj,
    Column("modulus", Integer, nullable=False),
    Column("data", String(30)),
    postgresql_partition_by="range(modulus)",
)

sa.event.listen(
    dv,
    "after_create",
    sa.DDL(
        "CREATE TABLE data_values_4_10 PARTITION OF data_values "
        "FOR VALUES FROM (4) TO (10)"
    ),
)

Die beiden Tabellennamen 'data_values' und 'data_values_4_10' werden von Inspector.get_table_names() zurückgegeben und zusätzlich werden die Spalten von Inspector.get_columns('data_values') sowie von Inspector.get_columns('data_values_4_10') zurückgegeben. Dies gilt auch für die Verwendung von Table(..., autoload=True) mit diesen Tabellen.

#4237

Dialektverbesserungen und Änderungen - MySQL

Protokollebene-Ping wird jetzt für Pre-Ping verwendet

Die MySQL-Dialekte, einschließlich mysqlclient, python-mysql, PyMySQL und mysql-connector-python, verwenden nun die Methode connection.ping() für die Pool-Pre-Ping-Funktion, die unter Disconnect Handling - Pessimistic beschrieben ist. Dies ist ein wesentlich leichterer Ping als die bisherige Methode, bei der "SELECT 1" auf der Verbindung ausgeführt wurde.

Kontrolle der Parameterreihenfolge in ON DUPLICATE KEY UPDATE

Die Reihenfolge der UPDATE-Parameter in der Klausel ON DUPLICATE KEY UPDATE kann nun durch Übergabe einer Liste von 2-Tupeln explizit sortiert werden.

from sqlalchemy.dialects.mysql import insert

insert_stmt = insert(my_table).values(id="some_existing_id", data="inserted value")

on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
    [
        ("data", "some data"),
        ("updated_at", func.current_timestamp()),
    ],
)

Dialektverbesserungen und Änderungen – SQLite

Unterstützung für SQLite JSON hinzugefügt

Ein neuer Datentyp JSON wird hinzugefügt, der die JSON-Member-Zugriffsfunktionen von SQLite im Namen des JSON-Basistyp implementiert. Die SQLite-Funktionen JSON_EXTRACT und JSON_QUOTE werden von der Implementierung verwendet, um grundlegende JSON-Unterstützung bereitzustellen.

Beachten Sie, dass der Name des Datentyps selbst, wie er in der Datenbank gerendert wird, "JSON" lautet. Dies erstellt einen SQLite-Datentyp mit der "numerischen" Affinität, was normalerweise kein Problem darstellen sollte, außer im Falle eines JSON-Werts, der aus einem einzelnen ganzzahligen Wert besteht. Nichtsdestotrotz wird nach einem Beispiel in der eigenen Dokumentation von SQLite unter https://www.sqlite.org/json1.html der Name JSON aus Vertrautheitsgründen verwendet.

#3850

Unterstützung für SQLite ON CONFLICT in Constraints hinzugefügt

SQLite unterstützt eine nicht standardmäßige ON CONFLICT-Klausel, die für eigenständige Constraints sowie einige Inline-Constraints von Spalten wie NOT NULL angegeben werden kann. Die Unterstützung für diese Klauseln wurde über das Schlüsselwort sqlite_on_conflict hinzugefügt, das zu Objekten wie UniqueConstraint sowie mehreren Column-spezifischen Varianten hinzugefügt wurde.

some_table = Table(
    "some_table",
    metadata_obj,
    Column("id", Integer, primary_key=True, sqlite_on_conflict_primary_key="FAIL"),
    Column("data", Integer),
    UniqueConstraint("id", "data", sqlite_on_conflict="IGNORE"),
)

Die obige Tabelle würde in einer CREATE TABLE-Anweisung wie folgt gerendert:

CREATE TABLE some_table (
    id INTEGER NOT NULL,
    data INTEGER,
    PRIMARY KEY (id) ON CONFLICT FAIL,
    UNIQUE (id, data) ON CONFLICT IGNORE
)

#4360

Dialektverbesserungen und Änderungen - Oracle

Nationale Zeichendatentypen für generisches Unicode zurückgestellt, mit Option wieder aktiviert

Die Datentypen Unicode und UnicodeText entsprechen standardmäßig den Datentypen VARCHAR2 und CLOB unter Oracle, anstatt NVARCHAR2 und NCLOB (auch bekannt als "nationale" Zeichensatztypen). Dies wird sich in Verhaltensweisen zeigen, wie z. B. in der Art und Weise, wie sie in CREATE TABLE-Anweisungen gerendert werden, sowie darin, dass kein Typobjekt an setinputsizes() übergeben wird, wenn gebundene Parameter verwendet werden, die Unicode oder UnicodeText verwenden; cx_Oracle behandelt den Zeichenfolgenwert nativ. Diese Änderung basiert auf dem Rat des Maintainers von cx_Oracle, dass die "nationalen" Datentypen in Oracle weitgehend obsolet und nicht performant sind. Sie stören auch in einigen Situationen, z. B. wenn sie auf den Format-Spezifizierer für Funktionen wie trunc() angewendet werden.

Der einzige Fall, in dem NVARCHAR2 und verwandte Typen benötigt werden könnten, ist für eine Datenbank, die keinen Unicode-kompatiblen Zeichensatz verwendet. In diesem Fall kann das Flag use_nchar_for_unicode an create_engine() übergeben werden, um das alte Verhalten wieder zu aktivieren.

Wie immer, wird die explizite Verwendung der Datentypen NVARCHAR2 und NCLOB weiterhin NVARCHAR2 und NCLOB verwenden, sowohl in DDL als auch bei der Behandlung von gebundenen Parametern mit setinputsizes() von cx_Oracle.

Auf der Leseseite wurde die automatische Unicode-Konvertierung unter Python 2 zu CHAR/VARCHAR/CLOB-Ergebniszeilen hinzugefügt, um das Verhalten von cx_Oracle unter Python 3 zu entsprechen. Um die Leistungseinbußen zu mildern, die das cx_Oracle-Dialekt zuvor mit diesem Verhalten unter Python 2 hatte, werden unter Python 2 die sehr performanten (wenn C-Erweiterungen erstellt werden) nativen Unicode-Handler von SQLAlchemy verwendet. Die automatische Unicode-Koerzierung kann durch Setzen des Flags coerce_to_unicode auf False deaktiviert werden. Dieses Flag ist jetzt standardmäßig True und gilt für alle Zeichenfolgendaten, die in einem Ergebnis-Set zurückgegeben werden und nicht explizit unter Unicode oder den Oracle-Datentypen NVARCHAR2/NCHAR/NCLOB stehen.

#4242

cx_Oracle Connect-Argumente modernisiert, veraltete Parameter entfernt

Eine Reihe von Modernisierungen der Parameter, die vom cx_oracle-Dialekt sowie der URL-Zeichenfolge akzeptiert werden.

  • Die veralteten Parameter auto_setinputsizes, allow_twophase, exclude_setinputsizes werden entfernt.

  • Der Wert des Parameters threaded, der für das SQLAlchemy-Dialekt immer auf True gesetzt war, wird nicht mehr standardmäßig generiert. Das SQLAlchemy Connection-Objekt selbst ist nicht threadsicher, daher muss dieses Flag nicht übergeben werden.

  • Es ist veraltet, threaded an create_engine() selbst zu übergeben. Um den Wert von threaded auf True zu setzen, übergeben Sie ihn entweder an das Wörterbuch create_engine.connect_args oder verwenden Sie die Abfragezeichenfolge, z. B. oracle+cx_oracle://...?threaded=true.

  • Alle Parameter, die auf der URL-Abfragezeichenfolge übergeben werden und nicht anderweitig speziell verarbeitet werden, werden nun an die Funktion cx_Oracle.connect() übergeben. Eine Auswahl davon wird entweder in cx_Oracle-Konstanten oder Booleans umgewandelt, einschließlich mode, purity, events und threaded.

  • Wie zuvor werden alle cx_Oracle .connect()-Argumente über das Wörterbuch create_engine.connect_args akzeptiert; die Dokumentation war diesbezüglich ungenau.

#4369

Dialektverbesserungen und Änderungen - SQL Server

Unterstützung für pyodbc fast_executemany

Der kürzlich hinzugefügte "fast_executemany"-Modus von Pyodbc, der bei Verwendung des Microsoft ODBC-Treibers verfügbar ist, ist nun eine Option für das pyodbc / mssql-Dialekt. Übergeben Sie ihn über create_engine().

engine = create_engine(
    "mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server",
    fast_executemany=True,
)

#4158

Neue Parameter zur Beeinflussung von IDENTITY-Start und -Inkrement, Verwendung von Sequence veraltet

SQL Server unterstützt ab SQL Server 2012 Sequenzen mit echter CREATE SEQUENCE-Syntax. In #4235 fügt SQLAlchemy Unterstützung für diese mithilfe von Sequence auf die gleiche Weise wie für jedes andere Dialekt hinzu. Die aktuelle Situation ist jedoch, dass Sequence unter SQL Server speziell umfunktioniert wurde, um die Parameter "start" und "increment" für die IDENTITY-Spezifikation einer Primärschlüsselspalte zu beeinflussen. Um den Übergang zu normal verfügbaren Sequenzen zu ermöglichen, gibt die Verwendung von Sequence während der gesamten 1.3-Serie eine Deprecation-Warnung aus. Um "start" und "increment" zu beeinflussen, verwenden Sie die neuen Parameter mssql_identity_start und mssql_identity_increment auf Column.

test = Table(
    "test",
    metadata_obj,
    Column(
        "id",
        Integer,
        primary_key=True,
        mssql_identity_start=100,
        mssql_identity_increment=10,
    ),
    Column("name", String(20)),
)

Um IDENTITY auf einer Nicht-Primärschlüsselspalte auszugeben, was ein wenig genutzter, aber gültiger SQL Server-Anwendungsfall ist, verwenden Sie das Flag Column.autoincrement und setzen Sie es auf der Zielspalte auf True und auf jeder ganzzahligen Primärschlüsselspalte auf False.

test = Table(
    "test",
    metadata_obj,
    Column("id", Integer, primary_key=True, autoincrement=False),
    Column("number", Integer, autoincrement=True),
)

#4362

#4235

Formatierung von StatementError geändert (Zeilenumbrüche und %s)

Zwei Änderungen werden an der String-Darstellung von StatementError vorgenommen. Die "detail"- und "SQL"-Teile der String-Darstellung werden nun durch Zeilenumbrüche getrennt, und Zeilenumbrüche, die in der ursprünglichen SQL-Anweisung vorhanden sind, werden beibehalten. Ziel ist es, die Lesbarkeit zu verbessern und gleichzeitig die ursprüngliche Fehlermeldung für Protokollierungszwecke auf einer Zeile zu belassen.

Das bedeutet, dass eine Fehlermeldung, die zuvor so aussah:

sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is
required for bind parameter 'id' [SQL: 'select * from reviews\nwhere id = ?']
(Background on this error at: https://sqlalche.me/e/cd3x)

Wird nun so aussehen:

sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'id'
[SQL: select * from reviews
where id = ?]
(Background on this error at: https://sqlalche.me/e/cd3x)

Die Hauptauswirkung dieser Änderung ist, dass Verbraucher nicht mehr davon ausgehen können, dass eine vollständige Ausnahmemeldung auf einer einzelnen Zeile steht. Der ursprüngliche "error"-Teil, der vom DBAPI-Treiber oder der SQLAlchemy-Interna generiert wird, wird jedoch weiterhin auf der ersten Zeile stehen.

#4500