Was ist neu in SQLAlchemy 1.1?

Über dieses Dokument

Dieses Dokument beschreibt Änderungen zwischen SQLAlchemy Version 1.0 und SQLAlchemy Version 1.1.

Einleitung

Diese Anleitung stellt die Neuerungen in SQLAlchemy Version 1.1 vor und dokumentiert auch Änderungen, die Benutzer beim Migrieren ihrer Anwendungen von der 1.0er-Serie von SQLAlchemy auf 1.1 betreffen.

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

Plattform / Installer-Änderungen

Setuptools ist jetzt für die Installation erforderlich

Die Datei setup.py von SQLAlchemy unterstützt seit vielen Jahren sowohl den Betrieb mit installiertem Setuptools als auch ohne; sie unterstützt einen „Fallback“-Modus, der direkt Distutils verwendet. Da eine Python-Umgebung ohne Setuptools inzwischen ungehört ist und um das Feature-Set von Setuptools besser unterstützen zu können, insbesondere um die Integration von py.test damit sowie Dinge wie „extras“ zu unterstützen, hängt setup.py jetzt vollständig von Setuptools ab.

#3489

Das Aktivieren / Deaktivieren von C-Erweiterungs-Builds erfolgt nur über eine Umgebungsvariable

Die C-Erweiterungen werden bei der Installation standardmäßig erstellt, solange dies möglich ist. Um C-Erweiterungs-Builds zu deaktivieren, wurde die Umgebungsvariable DISABLE_SQLALCHEMY_CEXT seit SQLAlchemy 0.8.6 / 0.9.4 verfügbar gemacht. Der vorherige Ansatz, das Argument --without-cextensions zu verwenden, wurde entfernt, da er auf veralteten Funktionen von Setuptools basiert.

#3500

Neue Features und Verbesserungen - ORM

Neue Session-Lifecycle-Ereignisse

Die Session unterstützt seit langem Ereignisse, die ein gewisses Maß an Nachverfolgung von Zustandsänderungen an Objekten ermöglichen, einschließlich SessionEvents.before_attach(), SessionEvents.after_attach() und SessionEvents.before_flush(). Die Session-Dokumentation beschreibt auch wichtige Objektzustände unter Kurze Einführung in Objektzustände. Es gab jedoch nie ein System zur gezielten Nachverfolgung von Objekten, während sie diese Übergänge durchlaufen. Darüber hinaus war der Status von „gelöschten“ Objekten historisch unklar, da die Objekte irgendwo zwischen den Zuständen „persistent“ und „detached“ agieren.

Um diesen Bereich zu bereinigen und den Bereich des Session-Zustandsübergangs vollständig transparent zu gestalten, wurde eine neue Reihe von Ereignissen hinzugefügt, die darauf abzielen, jede mögliche Art und Weise abzudecken, wie ein Objekt zwischen Zuständen wechseln kann, und zusätzlich wurde dem „gelöschten“-Status ein eigener offizieller Zustandsname im Bereich der Session-Objektzustände gegeben.

Neue Zustandsübergangsereignisse

Übergänge zwischen allen Zuständen eines Objekts wie persistent, pending und anderen können jetzt in Bezug auf ein Session-Level-Ereignis abgefangen werden, das einen bestimmten Übergang abdecken soll. Übergänge, wenn Objekte in eine Session gelangen, aus einer Session austreten und sogar alle Übergänge, die auftreten, wenn die Transaktion mit Session.rollback() zurückgerollt wird, sind explizit in der Schnittstelle von SessionEvents vorhanden.

Insgesamt gibt es zehn neue Ereignisse. Eine Zusammenfassung dieser Ereignisse finden Sie in einem neu geschriebenen Dokumentationsabschnitt Objekt-Lifecycle-Ereignisse.

Neuer Objektzustand „deleted“ hinzugefügt, gelöschte Objekte sind nicht mehr „persistent“

Der persistent-Zustand eines Objekts in der Session wurde immer als ein Objekt dokumentiert, das eine gültige Datenbankidentität hat; im Falle von Objekten, die während eines Flushes gelöscht wurden, befanden sie sich jedoch immer in einem Graubereich, in dem sie noch nicht wirklich „detached“ von der Session waren, da sie immer noch innerhalb eines Rollbacks wiederhergestellt werden konnten, aber nicht wirklich „persistent“ waren, da ihre Datenbankidentität gelöscht wurde und sie nicht im Identitätsmap vorhanden waren.

Um diesen Graubereich angesichts der neuen Ereignisse aufzulösen, wird ein neuer Objektzustand deleted eingeführt. Dieser Zustand existiert zwischen den Zuständen „persistent“ und „detached“. Ein Objekt, das über Session.delete() zum Löschen markiert wird, bleibt im Zustand „persistent“, bis ein Flush erfolgt; zu diesem Zeitpunkt wird es aus dem Identitätsmap entfernt, wechselt in den Zustand „deleted“ und der Hook SessionEvents.persistent_to_deleted() wird aufgerufen. Wenn die Transaktion des Session-Objekts zurückgerollt wird, wird das Objekt als persistent wiederhergestellt; der Übergang SessionEvents.deleted_to_persistent() wird aufgerufen. Andernfalls, wenn die Transaktion des Session-Objekts committet wird, wird der Übergang SessionEvents.deleted_to_detached() aufgerufen.

Zusätzlich gibt der Accessor InstanceState.persistent für ein Objekt im neuen „deleted“-Zustand **nicht mehr True** zurück; stattdessen wurde der Accessor InstanceState.deleted erweitert, um diesen neuen Zustand zuverlässig zu melden. Wenn das Objekt detached ist, gibt InstanceState.deleted False zurück und der Accessor InstanceState.detached ist stattdessen True. Um festzustellen, ob ein Objekt entweder in der aktuellen Transaktion oder in einer vorherigen Transaktion gelöscht wurde, verwenden Sie den Accessor InstanceState.was_deleted.

Starkes Identitätsmap ist veraltet

Eine der Inspirationen für die neue Reihe von Übergangsereignissen war die Möglichkeit, Objekte beim Ein- und Aussteigen aus dem Identitätsmap lecksicher zu verfolgen, sodass eine „starke Referenz“ gespiegelt werden kann, wie das Objekt in und aus diesem Map ein- und aussteigt. Mit dieser neuen Funktionalität ist kein Bedarf mehr für den Parameter Session.weak_identity_map und das entsprechende StrongIdentityMap Objekt. Diese Option war viele Jahre lang in SQLAlchemy vorhanden, da das „starke Referenzieren“-Verhalten früher das einzig verfügbare war und viele Anwendungen so geschrieben wurden, dass sie dieses Verhalten annahmen. Es wurde seit langem empfohlen, dass die starke Referenzverfolgung von Objekten keine intrinsische Aufgabe der Session ist, sondern stattdessen eine anwendungsebene Konstruktion ist, die bei Bedarf von der Anwendung erstellt wird; das neue Ereignismodell ermöglicht es sogar, das exakte Verhalten des starken Identitätsmaps zu replizieren. Siehe Session-Referenzverhalten für ein neues Rezept, das zeigt, wie das starke Identitätsmap ersetzt werden kann.

#2677

Neues init_scalar() Ereignis fängt Standardwerte auf ORM-Ebene ab

Das ORM erzeugt einen Wert von None, wenn auf ein Attribut, das noch nicht gesetzt wurde, zum ersten Mal zugegriffen wird, für ein nicht-persistentes Objekt

>>> obj = MyObj()
>>> obj.some_value
None

Es gibt einen Anwendungsfall dafür, dass dieser In-Python-Wert dem eines Core-generierten Standardwerts entspricht, noch bevor das Objekt persistiert wird. Um diesen Anwendungsfall zu erfüllen, wird ein neues Ereignis AttributeEvents.init_scalar() hinzugefügt. Das neue Beispiel active_column_defaults.py unter Attributinstrumentierung veranschaulicht eine Beispielverwendung, sodass die Auswirkung stattdessen sein kann

>>> obj = MyObj()
>>> obj.some_value
"my default"

#1311

Änderungen bezüglich „unhashable“ Typen, Auswirkungen auf die Deduplizierung von ORM-Zeilen

Das Query-Objekt hat ein bekanntes Verhalten, zurückgegebene Zeilen zu „deduplizieren“, die mindestens eine ORM-zugeordnete Entität enthalten (z. B. ein vollständiges zugeordnetes Objekt im Gegensatz zu einzelnen Spaltenwerten). Der Hauptzweck hierfür ist, dass die Handhabung von Entitäten reibungslos mit dem Identitätsmap zusammenarbeitet, einschließlich der Berücksichtigung der doppelten Entitäten, die normalerweise beim Joined Eager Loading dargestellt werden, sowie wenn Joins zum Zweck der Filterung auf zusätzlichen Spalten verwendet werden.

Diese Deduplizierung beruht auf der Hashbarkeit der Elemente innerhalb der Zeile. Mit der Einführung von PostgreSQLs speziellen Typen wie ARRAY, HSTORE und JSON ist die Erfahrung von Typen innerhalb von Zeilen, die nicht hashbar sind und Probleme verursachen, häufiger als zuvor.

Tatsächlich enthält SQLAlchemy seit Version 0.8 eine Flagge auf Datentypen, die als „unhashable“ gekennzeichnet sind. Diese Flagge wurde jedoch nicht konsistent auf integrierte Typen angewendet. Wie in ARRAY und JSON Typen geben jetzt korrekt „unhashable“ an beschrieben, ist diese Flagge jetzt konsistent für alle „strukturellen“ Typen von PostgreSQL gesetzt.

Die Flagge „unhashable“ ist auch auf den Typ NullType gesetzt, da NullType verwendet wird, um sich auf jeden Ausdruck unbekannten Typs zu beziehen.

Da NullType auf die meisten Verwendungen von func angewendet wird, da func in den meisten Fällen nichts über die angegebenen Funktionsnamen weiß, deaktiviert die Verwendung von func() oft die Zeilendeduplizierung, es sei denn, es wird eine explizite Typisierung angewendet. Die folgenden Beispiele veranschaulichen func.substr() angewendet auf einen String-Ausdruck und func.date() angewendet auf einen Datetime-Ausdruck; beide Beispiele geben doppelte Zeilen zurück, aufgrund des Joined Eager Loads, es sei denn, es wird eine explizite Typisierung angewendet

result = (
    session.query(func.substr(A.some_thing, 0, 4), A).options(joinedload(A.bs)).all()
)

users = (
    session.query(
        func.date(User.date_created, "start of month").label("month"),
        User,
    )
    .options(joinedload(User.orders))
    .all()
)

Die obigen Beispiele sollten, um die Deduplizierung beizubehalten, wie folgt angegeben werden

result = (
    session.query(func.substr(A.some_thing, 0, 4, type_=String), A)
    .options(joinedload(A.bs))
    .all()
)

users = (
    session.query(
        func.date(User.date_created, "start of month", type_=DateTime).label("month"),
        User,
    )
    .options(joinedload(User.orders))
    .all()
)

Zusätzlich ist die Behandlung eines sogenannten „unhashable“-Typs leicht anders als in früheren Releases; intern verwenden wir die Funktion id(), um einen „Hash-Wert“ von diesen Strukturen zu erhalten, genau wie bei jedem gewöhnlichen zugeordneten Objekt. Dies ersetzt den vorherigen Ansatz, der einen Zähler auf das Objekt angewendet hat.

#3499

Spezifische Prüfungen hinzugefügt für die Übergabe von zugeordneten Klassen, Instanzen als SQL-Literale

Das Typsystem enthält jetzt spezifische Prüfungen für die Übergabe von SQLAlchemy „inspektierbaren“ Objekten in Kontexten, in denen sie andernfalls als Literalwerte behandelt würden. Jedes SQLAlchemy integrierte Objekt, das als SQL-Wert übergeben werden kann (was noch keine ClauseElement-Instanz ist), enthält eine Methode __clause_element__(), die einen gültigen SQL-Ausdruck für dieses Objekt liefert. Für SQLAlchemy-Objekte, die dies nicht bereitstellen, wie z. B. zugeordnete Klassen, Mapper und zugeordnete Instanzen, wird eine aussagekräftigere Fehlermeldung ausgegeben, anstatt zuzulassen, dass die DBAPI das Objekt empfängt und später fehlschlägt. Ein Beispiel ist unten dargestellt, wo ein stringbasierter Attribut User.name mit einer vollständigen Instanz von User() verglichen wird, anstatt mit einem String-Wert

>>> some_user = User()
>>> q = s.query(User).filter(User.name == some_user)
sqlalchemy.exc.ArgumentError: Object <__main__.User object at 0x103167e90> is not legal as a SQL literal value

Die Ausnahme tritt jetzt sofort auf, wenn der Vergleich zwischen User.name == some_user erfolgt. Zuvor würde ein Vergleich wie der oben genannte einen SQL-Ausdruck erzeugen, der erst bei der Auflösung zu einem DBAPI-Ausführungaufruf fehlschlägt; das zugeordnete User-Objekt würde schließlich zu einem gebundenen Parameter werden, der von der DBAPI abgelehnt wird.

Beachten Sie, dass im obigen Beispiel der Ausdruck fehlschlägt, da User.name ein stringbasierter (z. B. spaltenorientierter) Attribut ist. Die Änderung beeinträchtigt *nicht* den üblichen Fall des Vergleichs eines Many-to-One-Beziehungsattributs mit einem Objekt, was separat behandelt wird

>>> # Address.user refers to the User mapper, so
>>> # this is of course still OK!
>>> q = s.query(Address).filter(Address.user == some_user)

#3321

Neue Indexable ORM-Erweiterung

Die Indexable-Erweiterung ist eine Erweiterung der Hybrid-Attributfunktion, die die Konstruktion von Attributen ermöglicht, die sich auf bestimmte Elemente eines „indexierbaren“ Datentyps beziehen, wie z. B. ein Array oder ein JSON-Feld

class Person(Base):
    __tablename__ = "person"

    id = Column(Integer, primary_key=True)
    data = Column(JSON)

    name = index_property("data", "name")

Oben liest/schreibt das Attribut name das Feld "name" aus der JSON-Spalte data, nachdem es zu einem leeren Wörterbuch initialisiert wurde

>>> person = Person(name="foobar")
>>> person.name
foobar

Die Erweiterung löst auch ein Änderungsereignis aus, wenn das Attribut geändert wird, sodass die Verwendung von MutableDict zur Verfolgung dieser Änderung nicht mehr erforderlich ist.

Siehe auch

Indexierbar

Neue Optionen, die explizite Persistenz von NULL über einen Standardwert zulassen

In Bezug auf die neue JSON-NULL-Unterstützung, die für PostgreSQL als Teil von JSON „null“ wird wie erwartet bei ORM-Operationen eingefügt, weggelassen, wenn nicht vorhanden, unterstützt die Basisklasse TypeEngine jetzt eine Methode TypeEngine.evaluates_none(), die ein positives Setzen des Werts None auf einem Attribut ermöglicht, das als NULL persistiert wird, anstatt die Spalte aus der INSERT-Anweisung wegzulassen, was zur Folge hat, dass der spaltengesteuerte Standardwert verwendet wird. Dies ermöglicht eine Konfiguration auf Mapper-Ebene der bestehenden Technik auf Objektebene, null() dem Attribut zuzuweisen.

#3250

Weitere Korrekturen an Single-Table-Inheritance-Abfragen

Fortsetzung von 1.0s Änderung an Single-Table-Inheritance-Kriterien bei Verwendung von from_self(), count(), sollte die Query nicht mehr unangemessen das „Single-Inheritance“-Kriterium hinzufügen, wenn die Abfrage gegen einen Subquery-Ausdruck wie ein exists gerichtet ist

class Widget(Base):
    __tablename__ = "widget"
    id = Column(Integer, primary_key=True)
    type = Column(String)
    data = Column(String)
    __mapper_args__ = {"polymorphic_on": type}


class FooWidget(Widget):
    __mapper_args__ = {"polymorphic_identity": "foo"}


q = session.query(FooWidget).filter(FooWidget.data == "bar").exists()

session.query(q).all()

Erzeugt

SELECT EXISTS (SELECT 1
FROM widget
WHERE widget.data = :data_1 AND widget.type IN (:type_1)) AS anon_1

Die IN-Klausel auf der Innenseite ist angemessen, um auf FooWidget-Objekte beschränkt zu werden, aber zuvor wurde die IN-Klausel auch ein zweites Mal außerhalb des Subqueries generiert.

#3582

Verbesserter Session-Zustand, wenn ein SAVEPOINT von der Datenbank abgebrochen wird

Ein häufiger Fall bei MySQL ist, dass ein SAVEPOINT bei einem Deadlock innerhalb der Transaktion abgebrochen wird. Die Session wurde modifiziert, um mit diesem Fehlerfall etwas gnädiger umzugehen, sodass die äußere, nicht-savepoint Transaktion weiterhin nutzbar bleibt

s = Session()
s.begin_nested()

s.add(SomeObject())

try:
    # assume the flush fails, flush goes to rollback to the
    # savepoint and that also fails
    s.flush()
except Exception as err:
    print("Something broke, and our SAVEPOINT vanished too")

# this is the SAVEPOINT transaction, marked as
# DEACTIVE so the rollback() call succeeds
s.rollback()

# this is the outermost transaction, remains ACTIVE
# so rollback() or commit() can succeed
s.rollback()

Dieses Problem ist eine Fortsetzung von #2696, bei dem wir eine Warnung ausgeben, damit der ursprüngliche Fehler unter Python 2 sichtbar ist, obwohl die SAVEPOINT-Ausnahme Vorrang hat. Unter Python 3 werden Ausnahmen verkettet, sodass beide Fehler einzeln gemeldet werden.

#3680

Fehlerhafte „new instance X conflicts with persistent instance Y“-Flush-Fehler behoben

Die Methode Session.rollback() ist dafür verantwortlich, Objekte zu entfernen, die in die Datenbank eingefügt wurden, z. B. von pending zu persistent verschoben, innerhalb dieser nun zurückgerollten Transaktion. Objekte, die diese Zustandsänderung durchlaufen, werden in einer schwach referenzierten Sammlung verfolgt, und wenn ein Objekt aus dieser Sammlung durch Garbage Collection entfernt wird, kümmert sich die Session nicht mehr darum (andernfalls würde es für Operationen, die viele neue Objekte innerhalb einer Transaktion einfügen, nicht skalieren). Ein Problem tritt jedoch auf, wenn die Anwendung dieselbe, durch Garbage Collection entfernte Zeile innerhalb der Transaktion neu lädt, bevor das Rollback erfolgt; wenn eine starke Referenz auf dieses Objekt in die nächste Transaktion reicht, würde die Tatsache, dass dieses Objekt nicht eingefügt wurde und entfernt werden sollte, verloren gehen, und der Flush würde fälschlicherweise einen Fehler auslösen

from sqlalchemy import Column, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)


e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)

# persist an object
s.add(A(id=1))
s.flush()

# rollback buffer loses reference to A

# load it again, rollback buffer knows nothing
# about it
a1 = s.query(A).first()

# roll back the transaction; all state is expired but the
# "a1" reference remains
s.rollback()

# previous "a1" conflicts with the new one because we aren't
# checking that it never got committed
s.add(A(id=1))
s.commit()

Das obige Programm würde Folgendes auslösen:

FlushError: New instance <User at 0x7f0287eca4d0> with identity key
(<class 'test.orm.test_transaction.User'>, ('u1',)) conflicts
with persistent instance <User at 0x7f02889c70d0>

Der Fehler besteht darin, dass bei der Auslösung der oben genannten Ausnahme die Unit of Work mit dem ursprünglichen Objekt arbeitet, unter der Annahme, dass es sich um eine Live-Zeile handelt, obwohl das Objekt abgelaufen ist und beim Testen festgestellt wird, dass es verschwunden ist. Die Korrektur testet diese Bedingung jetzt, sodass wir im SQL-Log Folgendes sehen:

BEGIN (implicit)

INSERT INTO a (id) VALUES (?)
(1,)

SELECT a.id AS a_id FROM a LIMIT ? OFFSET ?
(1, 0)

ROLLBACK

BEGIN (implicit)

SELECT a.id AS a_id FROM a WHERE a.id = ?
(1,)

INSERT INTO a (id) VALUES (?)
(1,)

COMMIT

Oben führt die Unit of Work jetzt einen SELECT für die Zeile durch, die wir als Konflikt melden wollen, stellt fest, dass sie nicht existiert und fährt normal fort. Die Kosten dieses SELECTs fallen nur an, wenn wir ohnehin fälschlicherweise eine Ausnahme auslösen würden.

#3677

passive_deletes Feature für Joined-Inheritance-Mappings

Ein Joined-Table-Inheritance-Mapping kann jetzt zulassen, dass ein DELETE als Ergebnis von Session.delete() durchgeführt wird, der nur ein DELETE für die Basistabelle und nicht für die Subclass-Tabelle ausgibt, was konfiguriertes ON DELETE CASCADE für die konfigurierten Fremdschlüssel ermöglicht. Dies wird mit der Option mapper.passive_deletes konfiguriert

from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column("id", Integer, primary_key=True)
    type = Column(String)

    __mapper_args__ = {
        "polymorphic_on": type,
        "polymorphic_identity": "a",
        "passive_deletes": True,
    }


class B(A):
    __tablename__ = "b"
    b_table_id = Column("b_table_id", Integer, primary_key=True)
    bid = Column("bid", Integer, ForeignKey("a.id", ondelete="CASCADE"))
    data = Column("data", String)

    __mapper_args__ = {"polymorphic_identity": "b"}

Mit dem obigen Mapping wird die Option mapper.passive_deletes auf dem Basis-Mapper konfiguriert; sie wirkt sich auf alle Nicht-Basis-Mapper aus, die Nachkommen des Mappers mit gesetzter Option sind. Ein DELETE für ein Objekt vom Typ B muss den Primärschlüsselwert von b_table_id nicht mehr abrufen, wenn er nicht geladen ist, und muss auch keine DELETE-Anweisung für die Tabelle selbst ausgeben

session.delete(some_b)
session.commit()

Gibt SQL aus als

DELETE FROM a WHERE a.id = %(id)s
-- {'id': 1}
COMMIT

Wie immer muss die Zieldatenbank über Fremdschlüsselunterstützung mit ON DELETE CASCADE verfügen.

#2349

Gleichnamige Backrefs lösen keinen Fehler aus, wenn sie auf Concrete-Inheritance-Unterklassen angewendet werden

Das folgende Mapping war schon immer problemlos möglich

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b = relationship("B", foreign_keys="B.a_id", backref="a")


class A1(A):
    __tablename__ = "a1"
    id = Column(Integer, primary_key=True)
    b = relationship("B", foreign_keys="B.a1_id", backref="a1")
    __mapper_args__ = {"concrete": True}


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

    a_id = Column(ForeignKey("a.id"))
    a1_id = Column(ForeignKey("a1.id"))

Oben, obwohl Klasse A und Klasse A1 eine Beziehung namens b haben, tritt keine Konfliktwarnung oder -fehler auf, da Klasse A1 als „concrete“ markiert ist.

Wenn die Beziehungen jedoch andersherum konfiguriert wären, würde ein Fehler auftreten

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)


class A1(A):
    __tablename__ = "a1"
    id = Column(Integer, primary_key=True)
    __mapper_args__ = {"concrete": True}


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

    a_id = Column(ForeignKey("a.id"))
    a1_id = Column(ForeignKey("a1.id"))

    a = relationship("A", backref="b")
    a1 = relationship("A1", backref="b")

Die Korrektur verbessert das Backref-Feature so, dass kein Fehler mehr ausgegeben wird, sowie eine zusätzliche Prüfung innerhalb der Mapper-Logik, um Warnungen für ein ersetztes Attribut zu umgehen.

#3630

Gleichnamige Beziehungen auf ererbten Mappern geben keine Warnung mehr aus

Beim Erstellen zweier Mapper in einem Vererbungsszenario würde das Platzieren einer Beziehung auf beiden mit demselben Namen die Warnung „relationship ‘<name>’ on mapper <name> supersedes the same relationship on inherited mapper ‘<name>’; this can cause dependency issues during flush“ ausgeben. Ein Beispiel ist wie folgt

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    bs = relationship("B")


class ASub(A):
    __tablename__ = "a_sub"
    id = Column(Integer, ForeignKey("a.id"), primary_key=True)
    bs = relationship("B")


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

Diese Warnung stammt aus der 0.4-Serie im Jahr 2007 und basiert auf einer Version des Unit-of-Work-Codes, die seitdem vollständig neu geschrieben wurde. Derzeit gibt es kein bekanntes Problem, bei dem gleichnamige Beziehungen auf einer Basisklasse und einer abgeleiteten Klasse platziert werden, daher wird die Warnung aufgehoben. Beachten Sie jedoch, dass dieser Anwendungsfall aufgrund der Warnung in der Praxis wahrscheinlich nicht verbreitet ist. Obwohl rudimentäre Testunterstützung für diesen Anwendungsfall hinzugefügt wurde, ist es möglich, dass ein neues Problem mit diesem Muster identifiziert wird.

Neu in Version 1.1.0b3.

#3749

Hybride Eigenschaften und Methoden leiten jetzt die Docstring sowie .info weiter

Eine hybride Methode oder Eigenschaft spiegelt nun den __doc__ -Wert wider, der im ursprünglichen Docstring vorhanden ist

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)

    name = Column(String)

    @hybrid_property
    def some_name(self):
        """The name field"""
        return self.name

Der obige Wert von A.some_name.__doc__ wird nun berücksichtigt

>>> A.some_name.__doc__
The name field

Um dies zu erreichen, wird die Mechanik hybrider Eigenschaften notwendigerweise komplexer. Zuvor war der Klassen-Zugriff für ein Hybrid ein einfacher Durchgang, d.h. dieser Test würde erfolgreich sein

>>> assert A.name is A.some_name

Mit der Änderung wird der von A.some_name zurückgegebene Ausdruck in seinen eigenen QueryableAttribute -Wrapper verpackt

>>> A.some_name
<sqlalchemy.orm.attributes.hybrid_propertyProxy object at 0x7fde03888230>

Viel Testaufwand wurde betrieben, um sicherzustellen, dass dieser Wrapper korrekt funktioniert, einschließlich ausgeklügelter Schemata wie dem des Rezepts Custom Value Object. Wir werden jedoch darauf achten, dass für Benutzer keine weiteren Regressionen auftreten.

Als Teil dieser Änderung wird die hybrid_property.info -Sammlung nun auch vom Hybrid-Deskriptor selbst weitergeleitet, anstatt vom zugrunde liegenden Ausdruck. Das heißt, der Zugriff auf A.some_name.info gibt nun das gleiche Wörterbuch zurück, das Sie von inspect(A).all_orm_descriptors['some_name'].info erhalten würden.

>>> A.some_name.info["foo"] = "bar"
>>> from sqlalchemy import inspect
>>> inspect(A).all_orm_descriptors["some_name"].info
{'foo': 'bar'}

Beachten Sie, dass dieses .info -Wörterbuch von dem eines zugeordneten Attributs getrennt ist, das der Hybrid-Deskriptor direkt weiterleitet; dies ist eine Verhaltensänderung gegenüber 1.0. Der Wrapper leitet weiterhin andere nützliche Attribute eines gespiegelten Attributs wie QueryableAttribute.property und QueryableAttribute.class_ weiter.

#3653

Session.merge löst ausstehende Konflikte genauso auf wie persistente

Die Methode Session.merge() verfolgt nun die Identitäten von Objekten, die innerhalb eines Graphen übergeben werden, um die Eindeutigkeit des Primärschlüssels zu gewährleisten, bevor ein INSERT ausgegeben wird. Wenn doppelte Objekte derselben Identität angetroffen werden, werden Nicht-Primärschlüssel-Attribute überschrieben, wenn die Objekte angetroffen werden, was im Wesentlichen nicht deterministisch ist. Dieses Verhalten entspricht dem, wie persistente Objekte, d.h. Objekte, die bereits über den Primärschlüssel in der Datenbank vorhanden sind, bereits behandelt werden, sodass dieses Verhalten intern konsistenter ist.

Gegeben

u1 = User(id=7, name="x")
u1.orders = [
    Order(description="o1", address=Address(id=1, email_address="a")),
    Order(description="o2", address=Address(id=1, email_address="b")),
    Order(description="o3", address=Address(id=1, email_address="c")),
]

sess = Session()
sess.merge(u1)

Oben verschmelzen wir ein User -Objekt mit drei neuen Order -Objekten, die jeweils auf eine separate Address -Objekt verweisen, jedoch jeweils denselben Primärschlüssel erhalten. Das aktuelle Verhalten von Session.merge() besteht darin, in der Identitätszuordnung nach diesem Address -Objekt zu suchen und dieses als Ziel zu verwenden. Wenn das Objekt vorhanden ist, d.h. die Datenbank bereits eine Zeile für Address mit dem Primärschlüssel „1“ hat, sehen wir, dass das Feld email_address der Address dreimal überschrieben wird, in diesem Fall mit den Werten a, b und schließlich c.

Wenn die Address -Zeile mit dem Primärschlüssel „1“ jedoch nicht vorhanden wäre, würde Session.merge() stattdessen drei separate Address -Instanzen erstellen, und wir würden dann bei INSERT einen Primärschlüsselkonflikt erhalten. Das neue Verhalten besteht darin, dass die vorgeschlagenen Primärschlüssel für diese Address -Objekte in einem separaten Wörterbuch verfolgt werden, sodass wir den Zustand der drei vorgeschlagenen Address -Objekte auf ein Address -Objekt zusammenführen, das eingefügt werden soll.

Es wäre vielleicht wünschenswert gewesen, wenn der ursprüngliche Fall eine Art Warnung ausgegeben hätte, dass widersprüchliche Daten in einem einzelnen Merge-Baum vorhanden sind. Die nicht deterministische Zusammenführung von Werten ist jedoch seit vielen Jahren das Verhalten für den persistenten Fall; nun ist es auch für den ausstehenden Fall dasselbe. Eine Funktion, die vor widersprüchlichen Werten warnt, könnte für beide Fälle immer noch machbar sein, würde jedoch erhebliche Leistungseinbußen mit sich bringen, da jeder Spaltenwert während des Mergens verglichen werden müsste.

#3601

Behebung von vielen-zu-eins-Objektverschiebungen mit benutzerinitiierten Fremdschlüsselmanipulationen

Ein Fehler wurde bei den Mechanismen zum Ersetzen einer vielen-zu-eins-Referenz auf ein Objekt durch ein anderes Objekt behoben. Während des Attributvorgangs verwendet die Position des zuvor referenzierten Objekts nun den datenbankbestätigten Fremdschlüsselwert anstelle des aktuellen Fremdschlüsselwerts. Die Hauptwirkung der Behebung besteht darin, dass ein Backref-Ereignis für eine Sammlung genauer ausgelöst wird, wenn eine viele-zu-eins-Änderung vorgenommen wird, auch wenn das Fremdschlüsselattribut zuvor manuell auf den neuen Wert verschoben wurde. Angenommen, eine Zuordnung der Klassen Parent und SomeClass, wobei SomeClass.parent auf Parent verweist und Parent.items auf die Sammlung von SomeClass -Objekten verweist.

some_object = SomeClass()
session.add(some_object)
some_object.parent_id = some_parent.id
some_object.parent = some_parent

Oben haben wir ein ausstehendes Objekt some_object erstellt, seinen Fremdschlüssel zu Parent manipuliert, um darauf zu verweisen, und *dann* die Beziehung eingerichtet. Vor der Fehlerbehebung wäre der Backref nicht ausgelöst worden.

# before the fix
assert some_object not in some_parent.items

Die jetzige Behebung besteht darin, dass wir, wenn wir den vorherigen Wert von some_object.parent lokalisieren wollen, die manuell gesetzte Eltern-ID ignorieren und nach dem datenbankbestätigten Wert suchen. In diesem Fall ist es None, da das Objekt ausstehend ist, sodass das Ereignissystem some_object.parent als Nettoänderung protokolliert.

# after the fix, backref fired off for some_object.parent = some_parent
assert some_object in some_parent.items

Obwohl die Manipulation von Fremdschlüsselattributen, die von Beziehungen verwaltet werden, nicht empfohlen wird, gibt es eine begrenzte Unterstützung für diesen Anwendungsfall. Anwendungen, die Fremdschlüssel manipulieren, um das Laden zu ermöglichen, nutzen oft die Funktionen Session.enable_relationship_loading() und RelationshipProperty.load_on_pending, die dazu führen, dass Beziehungen auf der Grundlage von im Speicher befindlichen Fremdschlüsselwerten, die nicht persistent sind, Lazy Loads auslösen. Unabhängig davon, ob diese Funktionen verwendet werden, wird diese Verhaltensverbesserung nun sichtbar sein.

#3708

Verbesserungen der Query.correlate-Methode mit polymorphen Entitäten

In neueren SQLAlchemy-Versionen ist die von vielen Formen "polymorpher" Abfragen generierte SQL "flacher" als zuvor, wobei ein JOIN mehrerer Tabellen nicht mehr bedingungslos in eine Unterabfrage gebündelt wird. Um dies zu ermöglichen, extrahiert die Methode Query.correlate() nun die einzelnen Tabellen aus einem solchen polymorphen wählbaren Objekt und stellt sicher, dass alle Teil des "Korrelats" für die Unterabfrage sind. Angenommen, die Person/Manager/Engineer->Company -Konfiguration aus der Mapping-Dokumentation, mit_polymorphic verwendet.

sess.query(Person.name).filter(
    sess.query(Company.name)
    .filter(Company.company_id == Person.company_id)
    .correlate(Person)
    .as_scalar()
    == "Elbonia, Inc."
)

Die obige Abfrage erzeugt nun

SELECT people.name AS people_name
FROM people
LEFT OUTER JOIN engineers ON people.person_id = engineers.person_id
LEFT OUTER JOIN managers ON people.person_id = managers.person_id
WHERE (SELECT companies.name
FROM companies
WHERE companies.company_id = people.company_id) = ?

Vor der Behebung hätte der Aufruf von correlate(Person) unbeabsichtigt versucht, mit dem Join von Person, Engineer und Manager als einer Einheit zu korrelieren, sodass Person nicht korreliert worden wäre.

-- old, incorrect query
SELECT people.name AS people_name
FROM people
LEFT OUTER JOIN engineers ON people.person_id = engineers.person_id
LEFT OUTER JOIN managers ON people.person_id = managers.person_id
WHERE (SELECT companies.name
FROM companies, people
WHERE companies.company_id = people.company_id) = ?

Die Verwendung korrelierter Unterabfragen gegen polymorphe Zuordnungen hat immer noch einige unpolierte Kanten. Wenn beispielsweise Person polymorph mit einer sogenannten "Concrete Polymorphic Union" -Abfrage verknüpft ist, bezieht sich die obige Unterabfrage möglicherweise nicht korrekt auf diese Unterabfrage. In allen Fällen ist eine Möglichkeit, die "polymorphe" Entität vollständig zu referenzieren, die Erstellung eines aliased() -Objekts daraus.

# works with all SQLAlchemy versions and all types of polymorphic
# aliasing.

paliased = aliased(Person)
sess.query(paliased.name).filter(
    sess.query(Company.name)
    .filter(Company.company_id == paliased.company_id)
    .correlate(paliased)
    .as_scalar()
    == "Elbonia, Inc."
)

Das aliased() -Konstrukt garantiert, dass die "polymorphe auswählbare" in eine Unterabfrage verpackt ist. Durch die explizite Referenzierung in der korrelierten Unterabfrage wird die polymorphe Form korrekt verwendet.

#3662

Stringify von Query konsultiert die Session für das richtige Dialekt

Das Aufrufen von str() für ein Query -Objekt konsultiert die Session für das richtige "bind" (Bindung), um die SQL-Anweisung zu rendern, die an die Datenbank gesendet würde. Insbesondere ermöglicht dies, dass eine Query, die auf Dialekt-spezifische SQL-Konstrukte verweist, gerendert werden kann, vorausgesetzt, die Query ist mit einer geeigneten Session verbunden. Zuvor galt dieses Verhalten nur, wenn die MetaData, mit der die Zuordnungen verknüpft waren, selbst an die Ziel- Engine gebunden war.

Wenn weder die zugrunde liegende MetaData noch die Session mit einer gebundenen Engine verbunden sind, wird auf das "Standard"-Dialekt zurückgegriffen, um die SQL-Zeichenfolge zu generieren.

#3081

Joined Eager Loading, wenn dieselbe Entität mehrmals in einer Zeile vorkommt

Es wurde eine Korrektur für den Fall vorgenommen, dass ein Attribut per Joined Eager Loading geladen wird, auch wenn die Entität bereits aus der Zeile auf einem anderen "Pfad" geladen wurde, der das Attribut nicht enthält. Dies ist ein tiefer Anwendungsfall, der schwer zu reproduzieren ist, aber die allgemeine Idee ist wie folgt.

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey("b.id"))
    c_id = Column(ForeignKey("c.id"))

    b = relationship("B")
    c = relationship("C")


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

    c = relationship("C")


class C(Base):
    __tablename__ = "c"
    id = Column(Integer, primary_key=True)
    d_id = Column(ForeignKey("d.id"))
    d = relationship("D")


class D(Base):
    __tablename__ = "d"
    id = Column(Integer, primary_key=True)


c_alias_1 = aliased(C)
c_alias_2 = aliased(C)

q = s.query(A)
q = q.join(A.b).join(c_alias_1, B.c).join(c_alias_1.d)
q = q.options(
    contains_eager(A.b).contains_eager(B.c, alias=c_alias_1).contains_eager(C.d)
)
q = q.join(c_alias_2, A.c)
q = q.options(contains_eager(A.c, alias=c_alias_2))

Die obige Abfrage gibt SQL wie folgt aus

SELECT
    d.id AS d_id,
    c_1.id AS c_1_id, c_1.d_id AS c_1_d_id,
    b.id AS b_id, b.c_id AS b_c_id,
    c_2.id AS c_2_id, c_2.d_id AS c_2_d_id,
    a.id AS a_id, a.b_id AS a_b_id, a.c_id AS a_c_id
FROM
    a
    JOIN b ON b.id = a.b_id
    JOIN c AS c_1 ON c_1.id = b.c_id
    JOIN d ON d.id = c_1.d_id
    JOIN c AS c_2 ON c_2.id = a.c_id

Wir sehen, dass die Tabelle c zweimal ausgewählt wird; einmal im Kontext von A.b.c -> c_alias_1 und einmal im Kontext von A.c -> c_alias_2. Außerdem ist es durchaus möglich, dass die C -Identität für eine einzelne Zeile für c_alias_1 und c_alias_2 dieselbe ist, was bedeutet, dass zwei Sätze von Spalten in einer Zeile nur zu einem neuen Objekt führen, das der Identitätszuordnung hinzugefügt wird.

Die obigen Abfrageoptionen fordern nur das Laden des Attributs C.d im Kontext von c_alias_1, aber nicht von c_alias_2. Ob das endgültige C -Objekt, das wir in der Identitätszuordnung erhalten, das Attribut C.d geladen hat oder nicht, hängt davon ab, wie die Zuordnungen durchlaufen werden, was zwar nicht vollständig zufällig ist, aber im Wesentlichen nicht deterministisch ist. Die Behebung besteht darin, dass selbst wenn der Lader für c_alias_1 nach dem von c_alias_2 für eine einzelne Zeile verarbeitet wird, die beide auf dieselbe Identität verweisen, das Element C.d trotzdem geladen wird. Zuvor hat der Lader nicht versucht, das Laden einer Entität zu modifizieren, die bereits über einen anderen Pfad geladen wurde. Der Lader, der die Entität zuerst erreicht, war schon immer nicht deterministisch, daher kann diese Behebung in einigen Situationen als Verhaltensänderung erkennbar sein und in anderen nicht.

Die Behebung umfasst Tests für zwei Varianten des Falls "mehrere Pfade zu einer Entität", und die Behebung sollte hoffentlich alle anderen Szenarien dieser Art abdecken.

#3431

Neue Helferklassen MutableList und MutableSet zur Erweiterung der Mutationsverfolgung hinzugefügt

Neue Helferklassen MutableList und MutableSet wurden zur Mutationsverfolgung -Erweiterung hinzugefügt, um den bestehenden Helfer MutableDict zu ergänzen.

#3297

Neue Laderstrategien "raise" / "raise_on_sql"

Um den Anwendungsfall zu unterstützen, unerwünschte Lazy Loads nach dem Laden einer Reihe von Objekten zu verhindern, können die neuen Strategien "lazy='raise'" und "lazy='raise_on_sql'" sowie die entsprechende Ladeoption raiseload() auf ein Beziehungsobjekt angewendet werden, das bewirkt, dass ein InvalidRequestError ausgelöst wird, wenn auf ein nicht eager-geladenes Attribut zum Lesen zugegriffen wird. Die beiden Varianten testen entweder einen Lazy Load jeder Art, einschließlich derjenigen, die nur None zurückgeben oder aus der Identitätszuordnung abrufen.

>>> from sqlalchemy.orm import raiseload
>>> a1 = s.query(A).options(raiseload(A.some_b)).first()
>>> a1.some_b
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'A.some_b' is not available due to lazy='raise'

Oder ein Lazy Load nur, wenn SQL ausgegeben werden würde

>>> from sqlalchemy.orm import raiseload
>>> a1 = s.query(A).options(raiseload(A.some_b, sql_only=True)).first()
>>> a1.some_b
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'A.bs' is not available due to lazy='raise_on_sql'

#3512

Mapper.order_by ist veraltet

Dieser alte Parameter aus den allerersten Versionen von SQLAlchemy war Teil des ursprünglichen Designs der ORM, bei der das Objekt Mapper als eine öffentliche Abfragestruktur diente. Diese Rolle wurde längst vom Objekt Query übernommen, wo wir Query.order_by() verwenden, um die Reihenfolge der Ergebnisse anzugeben, auf eine Weise, die für jede Kombination von SELECT-Anweisungen, Entitäten und SQL-Ausdrücken konsistent funktioniert. Es gibt viele Bereiche, in denen Mapper.order_by nicht wie erwartet funktioniert (oder was erwartet wird, ist unklar), z. B. wenn Abfragen zu Unionen kombiniert werden; diese Fälle werden nicht unterstützt.

#3394

Neue Features und Verbesserungen - Core

Engines ungültigen jetzt Verbindungen, führen Fehlerbehandlungsroutinen für BaseException aus

Neu in Version 1.1: Diese Änderung ist eine späte Ergänzung der 1.1-Serie kurz vor 1.1 Final und ist nicht in den 1.1 Beta-Versionen enthalten.

Die Python-Klasse BaseException liegt unterhalb von Exception, ist aber die identifizierbare Basis für systemweite Ausnahmen wie KeyboardInterrupt, SystemExit und insbesondere die Ausnahme GreenletExit, die von eventlet und gevent verwendet wird. Diese Ausnahmeklasse wird nun von den Ausnahmebehandlungsroutinen der Connection abgefangen und beinhaltet die Behandlung durch das Ereignis ConnectionEvents.handle_error(). Die Connection wird nun standardmäßig bei einer systemweiten Ausnahme, die keine Unterklasse von Exception ist, ungültig gemacht, da davon ausgegangen wird, dass eine Operation unterbrochen wurde und die Verbindung möglicherweise in einem unbrauchbaren Zustand ist. Die MySQL-Treiber sind von dieser Änderung am stärksten betroffen, aber die Änderung betrifft alle DBAPIs.

Beachten Sie, dass nach der Ungültigmachung die unmittelbare DBAPI-Verbindung, die von der Connection verwendet wird, entsorgt wird, und die Connection, wenn sie nach dem Auslösen der Ausnahme weiterhin verwendet wird, eine neue DBAPI-Verbindung für nachfolgende Operationen bei der nächsten Verwendung verwendet; der Zustand einer laufenden Transaktion geht jedoch verloren und die entsprechende .rollback() -Methode muss gegebenenfalls aufgerufen werden, bevor diese Wiederverwendung fortgesetzt werden kann.

Um diese Änderung zu identifizieren, war es einfach, zu demonstrieren, dass eine pymysql- oder mysqlclient / MySQL-Python-Verbindung in einen beschädigten Zustand gerät, wenn diese Ausnahmen mitten in der Arbeit der Verbindung auftreten. Die Verbindung würde dann zum Connection-Pool zurückgegeben, wo nachfolgende Verwendungen fehlschlagen würden, oder sogar vor der Rückgabe an den Pool zu sekundären Fehlern in Kontextmanagern führen, die .rollback() beim Abfangen der Ausnahme aufrufen. Das Verhalten hier soll die Häufigkeit des MySQL-Fehlers "commands out of sync" sowie die ResourceClosedError reduzieren, die auftreten kann, wenn der MySQL-Treiber cursor.description nicht korrekt meldet, wenn unter Greenlet-Bedingungen ausgeführt wird, bei denen Greenlets getötet werden, oder wenn KeyboardInterrupt -Ausnahmen ohne vollständiges Beenden des Programms behandelt werden.

Das Verhalten unterscheidet sich von der üblichen automatischen Ungültigmachungsfunktion, da es nicht davon ausgeht, dass die Backend-Datenbank selbst heruntergefahren oder neu gestartet wurde; es recycelt nicht den gesamten Connection-Pool, wie es bei üblichen DBAPI-Trennungsausnahmen der Fall ist.

Diese Änderung sollte eine Nettoverbesserung für alle Benutzer darstellen, mit Ausnahme von allen Anwendungen, die derzeit ``KeyboardInterrupt`` oder ``GreenletExit`` abfangen und innerhalb derselben Transaktion weiterarbeiten möchten. Eine solche Operation ist theoretisch mit anderen DBAPIs möglich, die nicht von KeyboardInterrupt betroffen zu sein scheinen, wie z. B. psycopg2. Für diese DBAPIs deaktiviert die folgende Problemumgehung die Wiederverwendung der Verbindung für bestimmte Ausnahmen.

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


@event.listens_for(engine, "handle_error")
def cancel_disconnect(ctx):
    if isinstance(ctx.original_exception, KeyboardInterrupt):
        ctx.is_disconnect = False

#3803

CTE-Unterstützung für INSERT, UPDATE, DELETE

Eine der am häufigsten nachgefragten Funktionen ist die Unterstützung von Common Table Expressions (CTE), die mit INSERT, UPDATE, DELETE funktionieren, und ist nun implementiert. Ein INSERT/UPDATE/DELETE kann sowohl aus einer WITH-Klausel schöpfen, die am Anfang der SQL-Anweisung angegeben ist, als auch selbst als CTE im Kontext einer größeren Anweisung dienen.

Als Teil dieser Änderung wird ein INSERT aus SELECT, der eine CTE enthält, nun am Anfang der gesamten Anweisung gerendert, anstatt wie in 1.0 in der SELECT-Anweisung verschachtelt zu sein.

Unten ist ein Beispiel, das UPDATE, INSERT und SELECT alles in einer Anweisung rendert

>>> from sqlalchemy import table, column, select, literal, exists
>>> orders = table(
...     "orders",
...     column("region"),
...     column("amount"),
...     column("product"),
...     column("quantity"),
... )
>>>
>>> upsert = (
...     orders.update()
...     .where(orders.c.region == "Region1")
...     .values(amount=1.0, product="Product1", quantity=1)
...     .returning(*(orders.c._all_columns))
...     .cte("upsert")
... )
>>>
>>> insert = orders.insert().from_select(
...     orders.c.keys(),
...     select([literal("Region1"), literal(1.0), literal("Product1"), literal(1)]).where(
...         ~exists(upsert.select())
...     ),
... )
>>>
>>> print(insert)  # Note: formatting added for clarity
WITH upsert AS (UPDATE orders SET amount=:amount, product=:product, quantity=:quantity WHERE orders.region = :region_1 RETURNING orders.region, orders.amount, orders.product, orders.quantity ) INSERT INTO orders (region, amount, product, quantity) SELECT :param_1 AS anon_1, :param_2 AS anon_2, :param_3 AS anon_3, :param_4 AS anon_4 WHERE NOT ( EXISTS ( SELECT upsert.region, upsert.amount, upsert.product, upsert.quantity FROM upsert))

#2551

Unterstützung für RANGE- und ROWS-Spezifikation in Window-Funktionen

Neue Parameter over.range_ und over.rows ermöglichen RANGE- und ROWS-Ausdrücke für Window-Funktionen.

>>> from sqlalchemy import func

>>> print(func.row_number().over(order_by="x", range_=(-5, 10)))
row_number() OVER (ORDER BY x RANGE BETWEEN :param_1 PRECEDING AND :param_2 FOLLOWING)
>>> print(func.row_number().over(order_by="x", rows=(None, 0)))
row_number() OVER (ORDER BY x ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
>>> print(func.row_number().over(order_by="x", range_=(-2, None)))
row_number() OVER (ORDER BY x RANGE BETWEEN :param_1 PRECEDING AND UNBOUNDED FOLLOWING)

over.range_ und over.rows werden als 2-Tupel angegeben und bezeichnen negative und positive Werte für spezifische Bereiche, 0 für "CURRENT ROW" und None für UNBOUNDED.

#3049

Unterstützung für das SQL-Schlüsselwort LATERAL

Das LATERAL-Schlüsselwort wird derzeit nur von PostgreSQL 9.3 und höher unterstützt, aber da es Teil des SQL-Standards ist, wurde die Unterstützung für dieses Schlüsselwort zu Core hinzugefügt. Die Implementierung von Select.lateral() verwendet spezielle Logik über das bloße Rendern des LATERAL-Schlüsselworts hinaus, um die Korrelation von Tabellen zu ermöglichen, die aus derselben FROM-Klausel wie das auswählbare Objekt stammen, z. B. laterale Korrelation.

>>> from sqlalchemy import table, column, select, true
>>> people = table("people", column("people_id"), column("age"), column("name"))
>>> books = table("books", column("book_id"), column("owner_id"))
>>> subq = (
...     select([books.c.book_id])
...     .where(books.c.owner_id == people.c.people_id)
...     .lateral("book_subq")
... )
>>> print(select([people]).select_from(people.join(subq, true())))
SELECT people.people_id, people.age, people.name FROM people JOIN LATERAL (SELECT books.book_id AS book_id FROM books WHERE books.owner_id = people.people_id) AS book_subq ON true

#2857

Unterstützung für TABLESAMPLE

Der SQL-Standard TABLESAMPLE kann mit der Methode FromClause.tablesample() gerendert werden, die ein TableSample -Konstrukt ähnlich einem Alias zurückgibt.

from sqlalchemy import func

selectable = people.tablesample(func.bernoulli(1), name="alias", seed=func.random())
stmt = select([selectable.c.people_id])

Unter der Annahme, dass people eine Spalte people_id hat, würde die obige Anweisung als

SELECT alias.people_id FROM
people AS alias TABLESAMPLE bernoulli(:bernoulli_1)
REPEATABLE (random())

#3718

Die Direktive .autoincrement wird für eine zusammengesetzte Primärschlüsselspalte nicht mehr implizit aktiviert

SQLAlchemy hatte schon immer die praktische Funktion, die "Autoincrement"-Funktion der Backend-Datenbank für einen einzelnen Integer-Primärschlüssel zu aktivieren; mit "Autoincrement" meinen wir, dass die Datenbankspalte die DDL-Direktiven enthält, die die Datenbank zur Angabe eines automatisch inkrementierenden Integer-Identifikators bereitstellt, wie z. B. das SERIAL-Schlüsselwort unter PostgreSQL oder AUTO_INCREMENT unter MySQL, und zusätzlich, dass das Dialekt diese generierten Werte aus der Ausführung eines Table.insert() -Konstrukts mit Techniken empfängt, die für dieses Backend geeignet sind.

Was sich geändert hat, ist, dass diese Funktion nicht mehr automatisch für einen zusammengesetzten Primärschlüssel aktiviert wird; zuvor hätte eine Tabellendefinition wie diese

Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True),
)

Hätte die "Autoincrement"-Semantik auf die Spalte 'x' angewendet, nur weil sie die erste in der Liste der Primärschlüsselspalten ist. Um dies zu deaktivieren, müsste man autoincrement auf allen Spalten ausschalten.

# old way
Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True, autoincrement=False),
    Column("y", Integer, primary_key=True, autoincrement=False),
)

Mit dem neuen Verhalten werden für den zusammengesetzten Primärschlüssel keine Autoincrement-Semantiken angewendet, es sei denn, eine Spalte ist explizit mit autoincrement=True markiert.

# column 'y' will be SERIAL/AUTO_INCREMENT/ auto-generating
Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True, autoincrement=True),
)

Um einige potenzielle rückwärtskompatible Szenarien zu antizipieren, führt das Konstrukt Table.insert() gründlichere Prüfungen auf fehlende Primärschlüsselwerte für zusammengesetzte Primärschlüsselspalten durch, für die kein Autoincrement eingerichtet ist. Angesichts einer Tabelle wie dieser

Table(
    "b",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True),
)

Ein mit keinen Werten für diese Tabelle ausgegebenes INSERT erzeugt diese Warnung.

SAWarning: Column 'b.x' is marked as a member of the primary
key for table 'b', but has no Python-side or server-side default
generator indicated, nor does it indicate 'autoincrement=True',
and no explicit value is passed.  Primary key columns may not
store NULL. Note that as of SQLAlchemy 1.1, 'autoincrement=True'
must be indicated explicitly for composite (e.g. multicolumn)
primary keys if AUTO_INCREMENT/SERIAL/IDENTITY behavior is
expected for one of the columns in the primary key. CREATE TABLE
statements are impacted by this change as well on most backends.

Für eine Spalte, die Primärschlüsselwerte von einem serverseitigen Standard oder etwas Ungewöhnlicherem wie einem Trigger erhält, kann die Anwesenheit eines Wertgenerators mithilfe von FetchedValue angezeigt werden.

Table(
    "b",
    metadata,
    Column("x", Integer, primary_key=True, server_default=FetchedValue()),
    Column("y", Integer, primary_key=True, server_default=FetchedValue()),
)

Für den sehr unwahrscheinlichen Fall, dass ein zusammengesetzter Primärschlüssel tatsächlich NULL in einer oder mehreren seiner Spalten speichern soll (nur unterstützt unter SQLite und MySQL), geben Sie die Spalte mit nullable=True an.

Table(
    "b",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True, nullable=True),
)

In einer verwandten Änderung kann das Flag autoincrement auf eine Spalte gesetzt werden, die einen client- oder serverseitigen Standardwert hat. Dies hat in der Regel wenig Einfluss auf das Verhalten der Spalte während eines INSERT.

#3216

Unterstützung für IS DISTINCT FROM und IS NOT DISTINCT FROM

Neue Operatoren ColumnOperators.is_distinct_from() und ColumnOperators.isnot_distinct_from() ermöglichen die SQL-Operation IS DISTINCT FROM und IS NOT DISTINCT FROM.

>>> print(column("x").is_distinct_from(None))
x IS DISTINCT FROM NULL

Es wird eine Behandlung für NULL, True und False bereitgestellt.

>>> print(column("x").isnot_distinct_from(False))
x IS NOT DISTINCT FROM false

Für SQLite, das diesen Operator nicht hat, wird "IS" / "IS NOT" gerendert, was unter SQLite für NULL funktioniert, im Gegensatz zu anderen Backends.

>>> from sqlalchemy.dialects import sqlite
>>> print(column("x").is_distinct_from(None).compile(dialect=sqlite.dialect()))
x IS NOT NULL

Core- und ORM-Unterstützung für FULL OUTER JOIN

Das neue Flag FromClause.outerjoin.full, das auf Core- und ORM-Ebene verfügbar ist, weist den Compiler an, FULL OUTER JOIN zu rendern, wo normalerweise LEFT OUTER JOIN gerendert wird.

stmt = select([t1]).select_from(t1.outerjoin(t2, full=True))

Das Flag funktioniert auch auf ORM-Ebene.

q = session.query(MyClass).outerjoin(MyOtherClass, full=True)

#1957

Verbesserungen bei der Spaltenübereinstimmung von Ergebnismengen; positionelle Spaltenkonfiguration für textuelles SQL

Eine Reihe von Verbesserungen wurden im 1.0er-Release am ResultProxy-System als Teil von #918 vorgenommen. Dabei wird die interne Struktur neu organisiert, um Cursor-gebundene Ergebnisspalten positionsbezogen mit Tabellen-/ORM-Metadaten abzugleichen, anstatt nach Namen. Dies gilt für kompilierte SQL-Konstrukte, die vollständige Informationen über die zurückzugebenden Ergebniszeilen enthalten. Dies ermöglicht eine dramatische Einsparung von Python-Overhead sowie eine wesentlich genauere Verknüpfung von ORM- und Core-SQL-Ausdrücken mit Ergebniszeilen. In Version 1.1 wurde diese Reorganisation intern weiter ausgebaut und steht über die kürzlich hinzugefügte Methode TextClause.columns() auch für reine Text-SQL-Konstrukte zur Verfügung.

TextAsFrom.columns() arbeitet jetzt positionsbezogen

Die in 0.9 hinzugefügte Methode TextClause.columns() akzeptiert spaltenbasierte Argumente positionsbezogen; in Version 1.1 wird, wenn alle Spalten positionsbezogen übergeben werden, die Korrelation dieser Spalten zum endgültigen Ergebnisdatensatz ebenfalls positionsbezogen durchgeführt. Der Hauptvorteil hierbei ist, dass Text-SQL nun mit einem Ergebnisdatensatz auf ORM-Ebene verknüpft werden kann, ohne sich mit mehrdeutigen oder doppelten Spaltennamen auseinandersetzen zu müssen oder mit der Notwendigkeit, Benennungsschemata an ORM-Benennungsschemata anzupassen. Alles, was jetzt benötigt wird, ist die gleiche Reihenfolge der Spalten im Text-SQL und die an TextClause.columns() übergebenen Spaltenargumente.

from sqlalchemy import text

stmt = text(
    "SELECT users.id, addresses.id, users.id, "
    "users.name, addresses.email_address AS email "
    "FROM users JOIN addresses ON users.id=addresses.user_id "
    "WHERE users.id = 1"
).columns(User.id, Address.id, Address.user_id, User.name, Address.email_address)

query = session.query(User).from_statement(stmt).options(contains_eager(User.addresses))
result = query.all()

Oben enthält das Text-SQL die Spalte "id" dreimal, was normalerweise mehrdeutig wäre. Mit der neuen Funktion können wir die abgebildeten Spalten aus der Klasse User und Address direkt anwenden, sogar die Spalte Address.user_id mit der Spalte users.id im Text-SQL verknüpfen, und das Query-Objekt erhält Zeilen, die korrekt wie benötigt adressiert werden können, auch für ein Eager Load.

Diese Änderung ist rückwärts inkompatibel mit Code, der die Spalten in einer anderen Reihenfolge an die Methode übergibt, als sie in der Textanweisung vorhanden ist. Es wird gehofft, dass diese Auswirkung gering sein wird, da diese Methode schon immer mit der Abbildung der Spalten in derselben Reihenfolge wie in der Text-SQL-Anweisung dokumentiert wurde, wie es intuitiv erscheinen würde, auch wenn die internen Mechanismen dies nicht geprüft haben. Die Methode selbst wurde ohnehin erst ab Version 0.9 hinzugefügt und hat möglicherweise noch keine weite Verbreitung. Hinweise zur genauen Handhabung dieser Verhaltensänderung für Anwendungen, die sie nutzen, finden Sie unter TextClause.columns() gleicht Spalten positionsbezogen ab, nicht nach Namen, wenn positionsbezogen übergeben.

Positionsbezogenes Abgleichen hat Vorrang vor namensbasiertem Abgleichen für Core/ORM SQL-Konstrukte

Ein weiterer Aspekt dieser Änderung ist, dass die Regeln für das Abgleichen von Spalten modifiziert wurden, um auch für kompilierte SQL-Konstrukte stärker auf ein "positionsbezogenes" Abgleichen zu vertrauen. Angenommen, eine Anweisung wie die folgende:

ua = users.alias("ua")
stmt = select([users.c.user_id, ua.c.user_id])

Die obige Anweisung wird kompiliert zu

SELECT users.user_id, ua.user_id FROM users, users AS ua

In 1.0 würde die obige Anweisung, wenn sie ausgeführt wird, per positionsbezogenem Abgleichen mit ihrem ursprünglichen kompilierten Konstrukt abgeglichen, aber da die Anweisung das doppelte Label 'user_id' enthält, wäre die Regel "mehrdeutige Spalte" immer noch involviert und hätte das Abrufen der Spalten aus einer Zeile verhindert. Ab Version 1.1 wirkt sich die Regel "mehrdeutige Spalte" nicht auf eine exakte Übereinstimmung von einer Spaltenkonstruktion mit der SQL-Spalte aus, was die ORM zum Abrufen von Spalten verwendet.

result = conn.execute(stmt)
row = result.first()

# these both match positionally, so no error
user_id = row[users.c.user_id]
ua_id = row[ua.c.user_id]

# this still raises, however
user_id = row["user_id"]

Viel seltener eine "mehrdeutige Spalte"-Fehlermeldung

Als Teil dieser Änderung wurde die Formulierung der Fehlermeldung Ambiguous column name '<name>' in result set! try 'use_labels' option on select statement. abgeschwächt; da diese Meldung nun extrem selten ist, wenn ORM oder Core kompilierte SQL-Konstrukte verwendet werden, besagt sie nun lediglich Ambiguous column name '<name>' in result set column descriptions und nur dann, wenn eine Ergebnisspalte unter Verwendung des tatsächlichen, mehrdeutigen String-Namens abgerufen wird, z. B. row['user_id'] im obigen Beispiel. Sie bezieht sich nun auch auf den tatsächlichen mehrdeutigen Namen aus der gerenderten SQL-Anweisung selbst, anstatt den Schlüssel oder Namen anzugeben, der lokal für das für den Abruf verwendete Konstrukt war.

#3501

Unterstützung für Pythons natives enum-Typ und kompatible Formen

Der Enum-Typ kann nun mit jedem PEP-435-konformen aufgezählten Typ konstruiert werden. Bei Verwendung dieses Modus sind Eingabe- und Ausgabewerte die tatsächlichen aufgezählten Objekte, nicht die String-/Integer-/etc.-Werte.

import enum
from sqlalchemy import Table, MetaData, Column, Enum, create_engine


class MyEnum(enum.Enum):
    one = 1
    two = 2
    three = 3


t = Table("data", MetaData(), Column("value", Enum(MyEnum)))

e = create_engine("sqlite://")
t.create(e)

e.execute(t.insert(), {"value": MyEnum.two})
assert e.scalar(t.select()) is MyEnum.two

Die Sammlung Enum.enums ist jetzt eine Liste anstelle eines Tupels

Als Teil der Änderungen am Enum ist die Sammlung von Elementen Enum.enums nun eine Liste anstelle eines Tupels. Dies liegt daran, dass Listen für variable Sequenzen homogener Elemente geeignet sind, bei denen die Position des Elements semantisch nicht signifikant ist.

#3292

Negative Ganzzahl-Indizes werden von Core-Ergebniszeilen unterstützt

Das RowProxy-Objekt unterstützt nun einzelne negative Ganzzahl-Indizes wie eine reguläre Python-Sequenz, sowohl in der reinen Python- als auch in der C-Extension-Version. Zuvor funktionierten negative Werte nur in Slices.

>>> from sqlalchemy import create_engine
>>> e = create_engine("sqlite://")
>>> row = e.execute("select 1, 2, 3").first()
>>> row[-1], row[-2], row[1], row[-2:2]
3 2 2 (2,)

Der Enum-Typ führt nun In-Python-Validierung von Werten durch

Um Python-native aufgezählte Objekte zu unterstützen, sowie für Randfälle wie die Verwendung eines nicht-nativen ENUM-Typs innerhalb eines ARRAY und wenn eine CHECK-Beschränkung unmöglich ist, fügt der Enum-Datentyp nun eine In-Python-Validierung von Eingabewerten hinzu, wenn das Flag Enum.validate_strings verwendet wird (1.1.0b2).

>>> from sqlalchemy import Table, MetaData, Column, Enum, create_engine
>>> t = Table(
...     "data",
...     MetaData(),
...     Column("value", Enum("one", "two", "three", validate_strings=True)),
... )
>>> e = create_engine("sqlite://")
>>> t.create(e)
>>> e.execute(t.insert(), {"value": "four"})
Traceback (most recent call last):
  ...
sqlalchemy.exc.StatementError: (exceptions.LookupError)
"four" is not among the defined enum values
[SQL: u'INSERT INTO data (value) VALUES (?)']
[parameters: [{'value': 'four'}]]

Diese Validierung ist standardmäßig deaktiviert, da bereits Anwendungsfälle identifiziert wurden, bei denen Benutzer keine solche Validierung wünschen (z. B. String-Vergleiche). Für Nicht-String-Typen erfolgt dies notwendigerweise in allen Fällen. Die Prüfung erfolgt auch bedingungslos auf der Ergebnisverarbeitungsseite, wenn Werte aus der Datenbank zurückgegeben werden.

Diese Validierung ist zusätzlich zum bestehenden Verhalten der Erstellung einer CHECK-Beschränkung, wenn ein nicht-nativer aufgezählter Typ verwendet wird. Die Erstellung dieser CHECK-Beschränkung kann nun mit dem neuen Flag Enum.create_constraint deaktiviert werden.

#3095

Nicht-native boolesche Ganzzahlwerte werden in allen Fällen zu Null/Eins/None konvertiert

Der Boolean-Datentyp konvertiert Python-Booleans in Ganzzahlwerte für Backends, die keinen nativen Booleschen Typ haben, wie SQLite und MySQL. Auf diesen Backends wird normalerweise eine CHECK-Beschränkung eingerichtet, die sicherstellt, dass die Werte in der Datenbank tatsächlich einer dieser beiden Werte sind. MySQL ignoriert jedoch CHECK-Beschränkungen, die Beschränkung ist optional, und eine bestehende Datenbank hat diese Beschränkung möglicherweise nicht. Der Boolean-Datentyp wurde repariert, sodass ein eingehender Python-Wert, der bereits ein Ganzzahlwert ist, zu Null oder Eins konvertiert wird und nicht nur unverändert übernommen wird; zusätzlich verwendet der C-Extension-Prozessor für Ergebnisse die gleiche Python-Boolesche Interpretation des Wertes, anstatt einen exakten Eins- oder Nullwert zu fordern. Dies ist nun konsistent mit dem reinen Python-Prozessor für Boolesche Ganzzahlen und ist nachsichtiger gegenüber vorhandenen Daten in der Datenbank. Werte von None/NULL bleiben wie bisher None/NULL.

Hinweis

Diese Änderung hatte eine unbeabsichtigte Nebenwirkung, dass sich die Interpretation von Nicht-Ganzzahlwerten, wie z. B. Strings, ebenfalls geändert hat, sodass der Stringwert "0" als "wahr" interpretiert wurde, aber nur auf Backends, die keinen nativen Booleschen Datentyp haben - auf "nativen Booleschen" Backends wie PostgreSQL wird der Stringwert "0" direkt an den Treiber übergeben und als "falsch" interpretiert. Dies ist eine Inkonsistenz, die mit der vorherigen Implementierung nicht auftrat. Es ist zu beachten, dass die Übergabe von Strings oder anderen Werten außerhalb von None, True, False, 1, 0 an den Boolean-Datentyp nicht unterstützt wird und Version 1.2 in diesem Szenario einen Fehler auslösen wird (oder möglicherweise nur eine Warnung ausgibt, TBD). Siehe auch #4102.

#3730

Große Parameter- und Zeilenwerte werden nun in der Protokollierung und bei der Anzeige von Ausnahmen gekürzt

Ein großer Wert, der als gebundener Parameter für eine SQL-Anweisung vorhanden ist, sowie ein großer Wert, der in einer Ergebniszeile vorhanden ist, werden nun bei der Anzeige in Protokollen, Ausnahmeberichten sowie bei der repr() der Zeile selbst gekürzt.

>>> from sqlalchemy import create_engine
>>> import random
>>> e = create_engine("sqlite://", echo="debug")
>>> some_value = "".join(chr(random.randint(52, 85)) for i in range(5000))
>>> row = e.execute("select ?", [some_value]).first()
... # (lines are wrapped for clarity) ...
2016-02-17 13:23:03,027 INFO sqlalchemy.engine.base.Engine select ?
2016-02-17 13:23:03,027 INFO sqlalchemy.engine.base.Engine
('E6@?>9HPOJB<<BHR:@=TS:5ILU=;JLM<4?B9<S48PTNG9>:=TSTLA;9K;9FPM4M8M@;NM6GU
LUAEBT9QGHNHTHR5EP75@OER4?SKC;D:TFUMD:M>;C6U:JLM6R67GEK<A6@S@C@J7>4=4:P
GJ7HQ6 ... (4702 characters truncated) ... J6IK546AJMB4N6S9L;;9AKI;=RJP
HDSSOTNBUEEC9@Q:RCL:I@5?FO<9K>KJAGAO@E6@A7JI8O:J7B69T6<8;F:S;4BEIJS9HM
K:;5OLPM@JR;R:J6<SOTTT=>Q>7T@I::OTDC:CC<=NGP6C>BC8N',)
2016-02-17 13:23:03,027 DEBUG sqlalchemy.engine.base.Engine Col ('?',)
2016-02-17 13:23:03,027 DEBUG sqlalchemy.engine.base.Engine
Row (u'E6@?>9HPOJB<<BHR:@=TS:5ILU=;JLM<4?B9<S48PTNG9>:=TSTLA;9K;9FPM4M8M@;
NM6GULUAEBT9QGHNHTHR5EP75@OER4?SKC;D:TFUMD:M>;C6U:JLM6R67GEK<A6@S@C@J7
>4=4:PGJ7HQ ... (4703 characters truncated) ... J6IK546AJMB4N6S9L;;9AKI;=
RJPHDSSOTNBUEEC9@Q:RCL:I@5?FO<9K>KJAGAO@E6@A7JI8O:J7B69T6<8;F:S;4BEIJS9HM
K:;5OLPM@JR;R:J6<SOTTT=>Q>7T@I::OTDC:CC<=NGP6C>BC8N',)
>>> print(row)
(u'E6@?>9HPOJB<<BHR:@=TS:5ILU=;JLM<4?B9<S48PTNG9>:=TSTLA;9K;9FPM4M8M@;NM6
GULUAEBT9QGHNHTHR5EP75@OER4?SKC;D:TFUMD:M>;C6U:JLM6R67GEK<A6@S@C@J7>4
=4:PGJ7HQ ... (4703 characters truncated) ... J6IK546AJMB4N6S9L;;9AKI;
=RJPHDSSOTNBUEEC9@Q:RCL:I@5?FO<9K>KJAGAO@E6@A7JI8O:J7B69T6<8;F:S;4BEIJS9H
MK:;5OLPM@JR;R:J6<SOTTT=>Q>7T@I::OTDC:CC<=NGP6C>BC8N',)

#2837

JSON-Unterstützung zu Core hinzugefügt

Da MySQL neben dem PostgreSQL JSON-Datentyp nun auch einen JSON-Datentyp besitzt, erhält Core nun einen sqlalchemy.types.JSON-Datentyp, der die Basis für beide ist. Die Verwendung dieses Typs ermöglicht den Zugriff auf den "getitem"-Operator sowie den "getpath"-Operator in einer weise, die agnostisch gegenüber PostgreSQL und MySQL ist.

Der neue Datentyp verfügt auch über eine Reihe von Verbesserungen bei der Handhabung von NULL-Werten sowie bei der Ausdrucksverarbeitung.

#3619

JSON "null" wird wie erwartet mit ORM-Operationen eingefügt, weggelassen, wenn nicht vorhanden

Der JSON-Typ und seine abgeleiteten Typen JSON und JSON verfügen über ein Flag JSON.none_as_null, das bei True angibt, dass der Python-Wert None in ein SQL NULL und nicht in ein JSON NULL-Wert übersetzt werden soll. Dieses Flag ist standardmäßig auf False gesetzt, was bedeutet, dass der Python-Wert None zu einem JSON NULL-Wert führen soll.

Diese Logik würde fehlschlagen und ist nun korrigiert unter folgenden Umständen:

1. Wenn die Spalte auch einen Standard- oder Server-Standardwert enthielt, würde ein positiver Wert von None für das abgebildete Attribut, das persistentes JSON "null" erwartet, dennoch dazu führen, dass der Standardwert der Spalte ausgelöst wird und den None-Wert ersetzt.

class MyObject(Base):
    # ...

    json_value = Column(JSON(none_as_null=False), default="some default")


# would insert "some default" instead of "'null'",
# now will insert "'null'"
obj = MyObject(json_value=None)
session.add(obj)
session.commit()

2. Wenn die Spalte keinen Standard- oder Server-Standardwert enthielt, würde ein fehlender Wert für eine JSON-Spalte, die mit none_as_null=False konfiguriert ist, dennoch JSON NULL rendern, anstatt darauf zurückzufallen, keinen Wert einzufügen, und inkonsistent im Vergleich zu allen anderen Datentypen verhalten.

class MyObject(Base):
    # ...

    some_other_value = Column(String(50))
    json_value = Column(JSON(none_as_null=False))


# would result in NULL for some_other_value,
# but json "'null'" for json_value.  Now results in NULL for both
# (the json_value is omitted from the INSERT)
obj = MyObject()
session.add(obj)
session.commit()

Dies ist eine Verhaltensänderung, die für Anwendungen, die sich darauf verlassen, dass ein fehlender Wert standardmäßig JSON null ist, rückwärts inkompatibel ist. Dies stellt im Wesentlichen fest, dass ein fehlender Wert von einem vorhandenen Wert von None unterschieden wird. Siehe JSON-Spalten fügen kein JSON NULL ein, wenn kein Wert angegeben und kein Standard festgelegt ist für weitere Details.

3. Wenn die Methode Session.bulk_insert_mappings() verwendet wurde, wurde None in allen Fällen ignoriert.

# would insert SQL NULL and/or trigger defaults,
# now inserts "'null'"
session.bulk_insert_mappings(MyObject, [{"json_value": None}])

Der JSON-Typ implementiert nun das Flag TypeEngine.should_evaluate_none, was anzeigt, dass None hier nicht ignoriert werden soll; es wird automatisch basierend auf dem Wert von JSON.none_as_null konfiguriert. Dank #3061 können wir unterscheiden, ob der Wert None aktiv vom Benutzer gesetzt wurde oder ob er nie gesetzt wurde.

Die Funktion gilt auch für den neuen Basis- JSON-Typ und seine abgeleiteten Typen.

#3514

Neue Konstante JSON.NULL hinzugefügt

Um sicherzustellen, dass eine Anwendung jederzeit die volle Kontrolle auf Wert-Ebene darüber hat, ob eine JSON-, JSON-, JSON- oder JSONB-Spalte einen SQL NULL- oder JSON "null"-Wert erhalten soll, wurde die Konstante JSON.NULL hinzugefügt. Diese kann in Verbindung mit null() verwendet werden, um vollständig zwischen SQL NULL und JSON "null" zu unterscheiden, unabhängig davon, wie JSON.none_as_null gesetzt ist.

from sqlalchemy import null
from sqlalchemy.dialects.postgresql import JSON

obj1 = MyObject(json_value=null())  # will *always* insert SQL NULL
obj2 = MyObject(json_value=JSON.NULL)  # will *always* insert JSON string "null"

session.add_all([obj1, obj2])
session.commit()

Die Funktion gilt auch für den neuen Basis- JSON-Typ und seine abgeleiteten Typen.

#3514

Array-Unterstützung zu Core hinzugefügt; neue ANY- und ALL-Operatoren

Zusammen mit den Verbesserungen am PostgreSQL ARRAY-Typ, die in Korrekte SQL-Typen werden aus indizierten Zugriffen auf ARRAY, JSON, HSTORE ermittelt beschrieben sind, wurde die Basisklasse des ARRAY selbst nach Core in eine neue Klasse ARRAY verschoben.

Arrays sind Teil des SQL-Standards, ebenso wie verschiedene Array-orientierte Funktionen wie array_agg() und unnest(). Zur Unterstützung dieser Konstrukte, nicht nur für PostgreSQL, sondern potenziell auch für andere Array-fähige Backends in der Zukunft wie DB2, befindet sich der Großteil der Array-Logik für SQL-Ausdrücke nun in Core. Der ARRAY-Typ funktioniert weiterhin nur unter PostgreSQL, kann aber direkt verwendet werden und unterstützt spezielle Array-Anwendungsfälle wie indizierten Zugriff sowie die Unterstützung für ANY und ALL.

mytable = Table("mytable", metadata, Column("data", ARRAY(Integer, dimensions=2)))

expr = mytable.c.data[5][6]

expr = mytable.c.data[5].any(12)

Zur Unterstützung von ANY und ALL behält der ARRAY-Typ dieselben Methoden Comparator.any() und Comparator.all() wie der PostgreSQL-Typ bei, exportiert diese Operationen aber auch an neue eigenständige Operatorfunktionen any_() und all_(). Diese beiden Funktionen funktionieren eher im traditionellen SQL-Sinne und erlauben eine Ausdrucksform auf der rechten Seite wie

from sqlalchemy import any_, all_

select([mytable]).where(12 == any_(mytable.c.data[5]))

Für die PostgreSQL-spezifischen Operatoren "contains", "contained_by" und "overlaps" sollte weiterhin direkt der ARRAY-Typ verwendet werden, der die gesamte Funktionalität des ARRAY-Typs ebenfalls bietet.

Die Operatoren any_() und all_() sind auf Core-Ebene offen, ihre Interpretation durch Backend-Datenbanken ist jedoch begrenzt. Auf dem PostgreSQL-Backend akzeptieren die beiden Operatoren nur Array-Werte. Auf dem MySQL-Backend akzeptieren sie nur Subquery-Werte. Auf MySQL kann ein Ausdruck wie

from sqlalchemy import any_, all_

subq = select([mytable.c.value])
select([mytable]).where(12 > any_(subq))

#3516

Neue Funktionsmerkmale, "WITHIN GROUP", array_agg und set aggregate Funktionen

Mit dem neuen ARRAY-Typ können wir auch eine vordefinierte Funktion für die SQL-Funktion array_agg() implementieren, die ein Array zurückgibt, welches nun über array_agg verfügbar ist.

from sqlalchemy import func

stmt = select([func.array_agg(table.c.value)])

Ein PostgreSQL-Element für eine aggregierte ORDER BY-Klausel wird ebenfalls über aggregate_order_by hinzugefügt.

from sqlalchemy.dialects.postgresql import aggregate_order_by

expr = func.array_agg(aggregate_order_by(table.c.a, table.c.b.desc()))
stmt = select([expr])

Erzeugt

SELECT array_agg(table1.a ORDER BY table1.b DESC) AS array_agg_1 FROM table1

Das PG-Dialekt selbst bietet auch einen array_agg()-Wrapper, um sicherzustellen, dass der ARRAY-Typ verwendet wird.

from sqlalchemy.dialects.postgresql import array_agg

stmt = select([array_agg(table.c.value).contains("foo")])

Zusätzlich sind Funktionen wie percentile_cont(), percentile_disc(), rank(), dense_rank() und andere, die eine Ordnung über WITHIN GROUP (ORDER BY <expr>) erfordern, nun über den Modifikator FunctionElement.within_group() verfügbar.

from sqlalchemy import func

stmt = select(
    [
        department.c.id,
        func.percentile_cont(0.5).within_group(department.c.salary.desc()),
    ]
)

Die obige Anweisung würde einen SQL-Code ähnlich dem folgenden erzeugen:

SELECT department.id, percentile_cont(0.5)
WITHIN GROUP (ORDER BY department.salary DESC)

Platzhalter mit korrekten Rückgabetypen werden nun für diese Funktionen bereitgestellt und umfassen percentile_cont, percentile_disc, rank, dense_rank, mode, percent_rank und cume_dist.

#3132 #1370

TypeDecorator funktioniert jetzt automatisch mit Enum, Boolean und "schema"-Typen

Die SchemaType-Typen umfassen Typen wie Enum und Boolean, die nicht nur einem Datenbanktyp entsprechen, sondern auch entweder eine CHECK-Beschränkung oder im Falle von PostgreSQL ENUM eine neue CREATE TYPE-Anweisung generieren. Diese funktionieren nun automatisch mit TypeDecorator-Rezepten. Zuvor musste ein TypeDecorator für ein ENUM wie folgt aussehen:

# old way
class MyEnum(TypeDecorator, SchemaType):
    impl = postgresql.ENUM("one", "two", "three", name="myenum")

    def _set_table(self, table):
        self.impl._set_table(table)

Der TypeDecorator leitet diese zusätzlichen Ereignisse nun weiter, sodass er wie jeder andere Typ gehandhabt werden kann.

# new way
class MyEnum(TypeDecorator):
    impl = postgresql.ENUM("one", "two", "three", name="myenum")

#2919

Multi-Tenancy Schema-Übersetzung für Table-Objekte

Zur Unterstützung des Anwendungsfalls einer Anwendung, die denselben Satz von Table-Objekten in vielen Schemas verwendet, z. B. Schema-pro-Benutzer, wird eine neue Ausführungsoption Connection.execution_options.schema_translate_map hinzugefügt. Mit dieser Zuordnung können eine Reihe von Table-Objekten pro Verbindung auf jede beliebige Schemasammlung verweisen, anstatt auf das Table.schema, dem sie zugewiesen wurden. Die Übersetzung funktioniert für DDL und SQL-Generierung sowie mit dem ORM.

Wenn beispielsweise die Klasse User dem Schema "per_user" zugewiesen wäre

class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)

    __table_args__ = {"schema": "per_user"}

Bei jeder Anfrage kann die Session so konfiguriert werden, dass sie jedes Mal auf ein anderes Schema verweist.

session = Session()
session.connection(
    execution_options={"schema_translate_map": {"per_user": "account_one"}}
)

# will query from the ``account_one.user`` table
session.query(User).get(5)

#2685

„Freundliche“ Stringifizierung von Core SQL-Konstrukten ohne Dialekt

Der Aufruf von str() für ein Core SQL-Konstrukt erzeugt nun in mehr Fällen als zuvor einen String, der verschiedene SQL-Konstrukte unterstützt, die normalerweise nicht in Standard-SQL vorhanden sind, wie RETURNING, Array-Indizes und nicht-standardmäßige Datentypen.

>>> from sqlalchemy import table, column
t>>> t = table('x', column('a'), column('b'))
>>> print(t.insert().returning(t.c.a, t.c.b))
INSERT INTO x (a, b) VALUES (:a, :b) RETURNING x.a, x.b

Die Funktion str() ruft nun einen völlig separaten Dialekt/Compiler auf, der nur für die einfache String-Ausgabe ohne einen spezifischen Dialekt konfiguriert ist. So können, wenn mehr "nur zeig mir einen String!"-Fälle auftreten, diese diesem Dialekt/Compiler hinzugefügt werden, ohne das Verhalten auf echten Dialekten zu beeinträchtigen.

#3631

Die Funktion type_coerce ist nun ein persistentes SQL-Element

Die Funktion type_coerce() gab zuvor je nach Eingabe ein Objekt vom Typ BindParameter oder Label zurück. Ein Nebeneffekt davon war, dass bei Ausdruckstransformationen, wie der Konvertierung eines Elements von einer Column in einen BindParameter, der für das Lazy Loading auf ORM-Ebene kritisch ist, die Typ-Kooperationsinformationen nicht verwendet wurden, da sie bereits verloren gegangen waren.

Um dieses Verhalten zu verbessern, gibt die Funktion jetzt einen persistenten TypeCoerce-Container um den gegebenen Ausdruck zurück, der selbst unverändert bleibt; dieses Konstrukt wird explizit vom SQL-Compiler ausgewertet. Dies ermöglicht es, dass die Kooperation des inneren Ausdrucks erhalten bleibt, unabhängig davon, wie die Anweisung modifiziert wird, einschließlich der Ersetzung des enthaltenen Elements durch ein anderes, wie es häufig bei der Lazy Loading-Funktion des ORM der Fall ist.

Der Testfall, der die Auswirkung veranschaulicht, verwendet eine heterogene primaryjoin-Bedingung in Verbindung mit benutzerdefinierten Typen und Lazy Loading. Gegeben sei ein benutzerdefinierter Typ, der ein CAST als „Bind Expression“ anwendet

class StringAsInt(TypeDecorator):
    impl = String

    def column_expression(self, col):
        return cast(col, Integer)

    def bind_expression(self, value):
        return cast(value, String)

Dann eine Zuordnung, bei der wir eine String-„id“-Spalte in einer Tabelle mit einer Integer-„id“-Spalte in der anderen gleichsetzen

class Person(Base):
    __tablename__ = "person"
    id = Column(StringAsInt, primary_key=True)

    pets = relationship(
        "Pets",
        primaryjoin=(
            "foreign(Pets.person_id)==cast(type_coerce(Person.id, Integer), Integer)"
        ),
    )


class Pets(Base):
    __tablename__ = "pets"
    id = Column("id", Integer, primary_key=True)
    person_id = Column("person_id", Integer)

Oben, im relationship.primaryjoin-Ausdruck, verwenden wir type_coerce(), um gebundene Parameter zu behandeln, die per Lazy Loading als Integer übergeben werden, da wir bereits wissen, dass diese von unserem StringAsInt-Typ stammen, der den Wert in Python als Integer beibehält. Wir verwenden dann cast(), damit die VARCHAR-„id“-Spalte als SQL-Ausdruck für einen regulären, nicht konvertierten Join, wie bei Query.join() oder joinedload(), zu einem Integer umgewandelt wird. Das heißt, ein joinedload von .pets sieht so aus

SELECT person.id AS person_id, pets_1.id AS pets_1_id,
       pets_1.person_id AS pets_1_person_id
FROM person
LEFT OUTER JOIN pets AS pets_1
ON pets_1.person_id = CAST(person.id AS INTEGER)

Ohne das CAST in der ON-Klausel des Joins werden stark typisierte Datenbanken wie PostgreSQL die implizite Vergleiche von Integer und Fehler verweigern.

Der Lazyload-Fall von .pets beruht auf dem Ersetzen der Person.id-Spalte zum Ladezeitpunkt durch einen gebundenen Parameter, der einen per Python geladenen Wert erhält. Dieses Ersetzen ist genau dort, wo die Absicht unserer type_coerce()-Funktion verloren gehen würde. Vor der Änderung kommt dieser Lazy Load wie folgt heraus

SELECT pets.id AS pets_id, pets.person_id AS pets_person_id
FROM pets
WHERE pets.person_id = CAST(CAST(%(param_1)s AS VARCHAR) AS INTEGER)
-- {'param_1': 5}

Wo oben zu sehen ist, dass unser In-Python-Wert 5 zuerst in VARCHAR und dann wieder in INTEGER in SQL umgewandelt wird; ein doppelter CAST, der funktioniert, aber dennoch nicht das ist, was wir angefordert haben.

Mit der Änderung behält die Funktion type_coerce() einen Wrapper bei, auch nachdem die Spalte durch einen gebundenen Parameter ersetzt wurde, und die Abfrage sieht nun wie folgt aus

SELECT pets.id AS pets_id, pets.person_id AS pets_person_id
FROM pets
WHERE pets.person_id = CAST(%(param_1)s AS INTEGER)
-- {'param_1': 5}

Wo unser äußerer CAST, der sich in unserem primaryjoin befindet, weiterhin greift, aber der unnötige CAST, der Teil des StringAsInt-Benutzertyps ist, wie von der type_coerce()-Funktion beabsichtigt, entfernt wird.

#3531

Wichtige Verhaltensänderungen – ORM

JSON-Spalten werden kein JSON NULL einfügen, wenn kein Wert angegeben wird und kein Standard festgelegt ist

Wie in JSON „null“ wird wie erwartet mit ORM-Operationen eingefügt, weggelassen, wenn nicht vorhanden erläutert, wird JSON keinen JSON-„null“-Wert rendern, wenn der Wert vollständig fehlt. Um SQL NULL zu verhindern, sollte ein Standardwert festgelegt werden. Gegeben sei die folgende Zuordnung

class MyObject(Base):
    # ...

    json_value = Column(JSON(none_as_null=False), nullable=False)

Die folgende Flush-Operation schlägt mit einem Integritätsfehler fehl

obj = MyObject()  # note no json_value
session.add(obj)
session.commit()  # will fail with integrity error

Wenn der Standardwert für die Spalte JSON NULL sein soll, legen Sie dies auf der Spalte fest

class MyObject(Base):
    # ...

    json_value = Column(JSON(none_as_null=False), nullable=False, default=JSON.NULL)

Oder stellen Sie sicher, dass der Wert auf dem Objekt vorhanden ist

obj = MyObject(json_value=None)
session.add(obj)
session.commit()  # will insert JSON NULL

Beachten Sie, dass das Festlegen von None als Standardwert dasselbe ist wie das vollständige Weglassen; das Flag JSON.none_as_null hat keinen Einfluss auf den Wert von None, der an Column.default oder Column.server_default übergeben wird

# default=None is the same as omitting it entirely, does not apply JSON NULL
json_value = Column(JSON(none_as_null=False), nullable=False, default=None)

Spalten werden nicht mehr redundant mit DISTINCT + ORDER BY hinzugefügt

Eine Abfrage wie die folgende wird nun nur noch die Spalten erweitern, die in der SELECT-Liste fehlen, ohne Duplikate

q = (
    session.query(User.id, User.name.label("name"))
    .distinct()
    .order_by(User.id, User.name, User.fullname)
)

Erzeugt

SELECT DISTINCT user.id AS a_id, user.name AS name,
 user.fullname AS a_fullname
FROM a ORDER BY user.id, user.name, user.fullname

Zuvor würde sie Folgendes erzeugen

SELECT DISTINCT user.id AS a_id, user.name AS name, user.name AS a_name,
  user.fullname AS a_fullname
FROM a ORDER BY user.id, user.name, user.fullname

Wo oben die user.name-Spalte unnötigerweise hinzugefügt wird. Die Ergebnisse wären nicht betroffen, da die zusätzlichen Spalten ohnehin nicht im Ergebnis enthalten sind, aber die Spalten sind unnötig.

Zusätzlich, wenn das PostgreSQL DISTINCT ON-Format verwendet wird, indem Ausdrücke an Query.distinct() übergeben werden, ist die obige „Spaltenhinzufügungs“-Logik vollständig deaktiviert.

Wenn die Abfrage als Subquery für das Joined Eager Loading gebündelt wird, sind die Regeln für die „Spaltenliste erweitern“ notwendigerweise aggressiver, damit ORDER BY weiterhin erfüllt werden kann. Dieser Fall bleibt also unverändert.

#3641

Gleichnamige @validates-Dekoratoren lösen nun eine Ausnahme aus

Der validates()-Dekorator ist nur dafür vorgesehen, einmal pro Klasse für einen bestimmten Attributnamen erstellt zu werden. Die Erstellung von mehr als einem löst nun einen Fehler aus, während es zuvor stillschweigend nur den zuletzt definierten Validator auswählte

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)

    data = Column(String)

    @validates("data")
    def _validate_data_one(self):
        assert "x" in data

    @validates("data")
    def _validate_data_two(self):
        assert "y" in data


configure_mappers()

Löst aus

sqlalchemy.exc.InvalidRequestError: A validation function for mapped attribute 'data'
on mapper Mapper|A|a already exists.

#3776

Wichtige Verhaltensänderungen – Core

TextClause.columns() passt sich positionsweise an Spalten an, nicht nach Namen, wenn positionsweise übergeben

Das neue Verhalten der Methode TextClause.columns(), die selbst erst kürzlich in der 0.9er-Serie hinzugefügt wurde, ist, dass, wenn Spalten positionsweise ohne zusätzliche Schlüsselwortargumente übergeben werden, sie positionsweise mit den letztendlichen Ergebnismengenspalten verknüpft werden und nicht mehr nach Namen. Es wird gehofft, dass die Auswirkungen dieser Änderung gering sein werden, da die Methode immer so dokumentiert wurde, dass die Spalten in der gleichen Reihenfolge übergeben werden, wie sie in der Text-SQL-Anweisung stehen, was intuitiv erscheinen würde, obwohl die Interna dies nicht überprüften.

Eine Anwendung, die diese Methode verwendet, indem sie Column-Objekte positionsweise übergibt, muss sicherstellen, dass die Position dieser Column-Objekte mit der Position übereinstimmt, an der diese Spalten in der Text-SQL-Anweisung aufgeführt sind.

Z. B. Code wie der folgende

stmt = text("SELECT id, name, description FROM table")

# no longer matches by name
stmt = stmt.columns(my_table.c.name, my_table.c.description, my_table.c.id)

würde nicht mehr wie erwartet funktionieren; die Reihenfolge der angegebenen Spalten ist nun wichtig

# correct version
stmt = stmt.columns(my_table.c.id, my_table.c.name, my_table.c.description)

Wahrscheinlicher ist, dass eine Anweisung, die wie folgt funktionierte

stmt = text("SELECT * FROM table")
stmt = stmt.columns(my_table.c.id, my_table.c.name, my_table.c.description)

ist nun leicht riskant, da die „*“-Spezifikation im Allgemeinen Spalten in der Reihenfolge liefert, in der sie in der Tabelle selbst vorhanden sind. Wenn sich die Struktur der Tabelle aufgrund von Schemaänderungen ändert, kann diese Reihenfolge nicht mehr dieselbe sein. Daher wird bei der Verwendung von TextClause.columns() empfohlen, die gewünschten Spalten explizit in der Text-SQL-Anweisung aufzulisten, obwohl es nicht mehr notwendig ist, sich um die Namen selbst in der Text-SQL-Anweisung zu kümmern.

String server_default jetzt literarisch zitiert

Ein Server-Standardwert, der Column.server_default als einfacher Python-String mit eingebetteten Anführungszeichen übergeben wird, wird nun über das literarische Quoting-System geleitet

>>> from sqlalchemy.schema import MetaData, Table, Column, CreateTable
>>> from sqlalchemy.types import String
>>> t = Table("t", MetaData(), Column("x", String(), server_default="hi ' there"))
>>> print(CreateTable(t))
CREATE TABLE t ( x VARCHAR DEFAULT 'hi '' there' )

Zuvor würde das Anführungszeichen direkt gerendert. Diese Änderung kann für Anwendungen mit einem solchen Anwendungsfall, die das Problem umgangen haben, abwärtskompatibel sein.

#3809

Ein UNION oder ähnliches von SELECTs mit LIMIT/OFFSET/ORDER BY parenthesisiert nun die eingebetteten SELECTs

Ein Problem, das, wie andere auch, lange Zeit von den Fähigkeiten von SQLite angetrieben wurde, wurde nun verbessert, um auf allen unterstützenden Backends zu funktionieren. Wir beziehen uns auf eine Abfrage, die ein UNION von SELECT-Anweisungen ist, die ihrerseits Zeilenbegrenzungs- oder Sortierungsfunktionen enthalten, die LIMIT, OFFSET und/oder ORDER BY umfassen

(SELECT x FROM table1 ORDER BY y LIMIT 1) UNION
(SELECT x FROM table2 ORDER BY y LIMIT 2)

Die obige Abfrage erfordert Klammern innerhalb jedes Unterauswahl, um die Unterergebnisse korrekt zu gruppieren. Die Erzeugung der obigen Anweisung in SQLAlchemy Core sieht wie folgt aus

stmt1 = select([table1.c.x]).order_by(table1.c.y).limit(1)
stmt2 = select([table1.c.x]).order_by(table2.c.y).limit(2)

stmt = union(stmt1, stmt2)

Zuvor würde die obige Konstruktion keine Klammern für die inneren SELECT-Anweisungen erzeugen, was zu einer Abfrage führen würde, die auf allen Backends fehlschlägt.

Die obigen Formate werden **weiterhin auf SQLite fehlschlagen**; zusätzlich wird das Format, das ORDER BY, aber kein LIMIT/SELECT enthält, **weiterhin auf Oracle fehlschlagen**. Dies ist keine abwärtsinkompatible Änderung, da die Abfragen auch ohne Klammern fehlschlagen; mit der Korrektur funktionieren die Abfragen zumindest auf allen anderen Datenbanken.

In allen Fällen, um ein UNION von begrenzten SELECTs zu erzeugen, das auch auf SQLite und in allen Fällen auf Oracle funktioniert, müssen die Subqueries ein SELECT eines ALIAS sein

stmt1 = select([table1.c.x]).order_by(table1.c.y).limit(1).alias().select()
stmt2 = select([table2.c.x]).order_by(table2.c.y).limit(2).alias().select()

stmt = union(stmt1, stmt2)

Diese Workaround funktioniert auf allen SQLAlchemy-Versionen. Im ORM sieht es so aus

stmt1 = session.query(Model1).order_by(Model1.y).limit(1).subquery().select()
stmt2 = session.query(Model2).order_by(Model2.y).limit(1).subquery().select()

stmt = session.query(Model1).from_statement(stmt1.union(stmt2))

Das Verhalten hier hat viele Parallelen zum „Join Rewriting“-Verhalten, das in SQLAlchemy 0.9 in Viele JOIN- und LEFT OUTER JOIN-Ausdrücke werden nicht mehr in (SELECT * FROM ..) AS ANON_1 verpackt eingeführt wurde; in diesem Fall haben wir jedoch darauf verzichtet, neues Rewriting-Verhalten hinzuzufügen, um diesen Fall für SQLite zu berücksichtigen. Das bestehende Rewriting-Verhalten ist bereits sehr kompliziert, und der Fall von UNIONs mit geklammerten SELECT-Anweisungen ist weitaus seltener als der „Right-Nested-Join“-Anwendungsfall dieser Funktion.

#2528

Dialektverbesserungen und Änderungen - PostgreSQL

Unterstützung für INSERT..ON CONFLICT (DO UPDATE | DO NOTHING)

Die ON CONFLICT-Klausel von INSERT, die seit Version 9.5 in PostgreSQL hinzugefügt wurde, wird nun über eine PostgreSQL-spezifische Version des Insert-Objekts, über sqlalchemy.dialects.postgresql.dml.insert(), unterstützt. Diese Insert-Unterklasse fügt zwei neue Methoden hinzu: Insert.on_conflict_do_update() und Insert.on_conflict_do_nothing(), die die vollständige Syntax implementieren, die PostgreSQL 9.5 in diesem Bereich unterstützt

from sqlalchemy.dialects.postgresql import insert

insert_stmt = insert(my_table).values(id="some_id", data="some data to insert")

do_update_stmt = insert_stmt.on_conflict_do_update(
    index_elements=[my_table.c.id], set_=dict(data="some data to update")
)

conn.execute(do_update_stmt)

Das obige wird rendern

INSERT INTO my_table (id, data)
VALUES (:id, :data)
ON CONFLICT id DO UPDATE SET data=:data_2

#3529

ARRAY- und JSON-Typen geben nun korrekt „unhashable“ an

Wie in Änderungen bezüglich „unhashable“ Typen, Auswirkungen auf die Entduplizierung von ORM-Zeilen beschrieben, ist die ORM auf die Erzeugung einer Hash-Funktion für Spaltenwerte angewiesen, wenn die ausgewählten Entitäten einer Abfrage vollständige ORM-Entitäten mit Spaltenausdrücken mischen. Das Flag hashable=False ist nun korrekt für alle „Datenstruktur“-Typen von PG gesetzt, einschließlich ARRAY und JSON. Die Typen JSONB und HSTORE enthielten dieses Flag bereits. Für ARRAY ist dies bedingt durch das Flag ARRAY.as_tuple, jedoch sollte es nicht mehr notwendig sein, dieses Flag zu setzen, um einen Array-Wert in einer zusammengesetzten ORM-Zeile zu haben.

#3499

Korrekte SQL-Typen werden aus dem indizierten Zugriff auf ARRAY, JSON, HSTORE etabliert

Für alle drei Typen ARRAY, JSON und HSTORE sollte der SQL-Typ, der dem durch indizierten Zugriff zurückgegebenen Ausdruck zugewiesen wird, z. B. col[someindex], in allen Fällen korrekt sein.

Dies schließt ein

  • Der SQL-Typ, der dem indizierten Zugriff auf ein ARRAY zugewiesen wird, berücksichtigt die Anzahl der konfigurierten Dimensionen. Ein ARRAY mit drei Dimensionen gibt einen SQL-Ausdruck vom Typ ARRAY mit einer Dimension weniger zurück. Angenommen, eine Spalte vom Typ ARRAY(Integer, dimensions=3), können wir nun diesen Ausdruck ausführen

    int_expr = col[5][6][7]  # returns an Integer expression object

    Zuvor würde der indizierte Zugriff auf col[5] einen Ausdruck vom Typ Integer zurückgeben, bei dem wir keinen indizierten Zugriff mehr für die verbleibenden Dimensionen durchführen konnten, es sei denn, wir verwendeten cast() oder type_coerce().

  • Die Typen JSON und JSONB spiegeln nun wider, was PostgreSQL selbst für den indizierten Zugriff tut. Das bedeutet, dass jeder indizierte Zugriff auf einen JSON- oder JSONB-Typ einen Ausdruck zurückgibt, der selbst *immer* JSON oder JSONB ist, es sei denn, der Modifikator Comparator.astext wird verwendet. Das bedeutet, dass PostgreSQL, unabhängig davon, ob der indizierte Zugriff auf die JSON-Struktur letztendlich auf einen String, eine Liste, eine Zahl oder eine andere JSON-Struktur verweist, ihn immer als JSON selbst betrachtet, es sei denn, er wird explizit anders umgewandelt. Wie beim ARRAY-Typ bedeutet dies, dass die Erzeugung von JSON-Ausdrücken mit mehreren Ebenen von indiziertem Zugriff nun unkompliziert ist

    json_expr = json_col["key1"]["attr1"][5]
  • Der „textuelle“ Typ, der durch indizierten Zugriff auf HSTORE zurückgegeben wird, sowie der „textuelle“ Typ, der durch indizierten Zugriff auf JSON und JSONB in Verbindung mit dem Modifikator Comparator.astext zurückgegeben wird, ist nun konfigurierbar; er ist standardmäßig TextClause in beiden Fällen, kann aber mit den Parametern JSON.astext_type oder HSTORE.text_type auf einen benutzerdefinierten Typ gesetzt werden.

#3499 #3487

Der JSON cast()-Vorgang erfordert nun die explizite Angabe von .astext

Als Teil der Änderungen in Korrekte SQL-Typen werden aus dem indizierten Zugriff auf ARRAY, JSON, HSTORE etabliert, rufen die Vorgänge des ColumnElement.cast() Operators auf JSON und JSONB nicht mehr implizit den Modifikator Comparator.astext auf; die JSON/JSONB-Typen von PostgreSQL unterstützen CAST-Operationen untereinander ohne den „astext“-Aspekt.

Das bedeutet, dass in den meisten Fällen eine Anwendung, die Folgendes tat

expr = json_col["somekey"].cast(Integer)

muss nun geändert werden zu

expr = json_col["somekey"].astext.cast(Integer)

ARRAY mit ENUM emittiert nun CREATE TYPE für das ENUM

Eine Tabellendefinition wie die folgende gibt nun wie erwartet CREATE TYPE aus

enum = Enum(
    "manager",
    "place_admin",
    "carwash_admin",
    "parking_admin",
    "service_admin",
    "tire_admin",
    "mechanic",
    "carwasher",
    "tire_mechanic",
    name="work_place_roles",
)


class WorkPlacement(Base):
    __tablename__ = "work_placement"
    id = Column(Integer, primary_key=True)
    roles = Column(ARRAY(enum))


e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
Base.metadata.create_all(e)

emittiert

CREATE TYPE work_place_roles AS ENUM (
    'manager', 'place_admin', 'carwash_admin', 'parking_admin',
    'service_admin', 'tire_admin', 'mechanic', 'carwasher',
    'tire_mechanic')

CREATE TABLE work_placement (
    id SERIAL NOT NULL,
    roles work_place_roles[],
    PRIMARY KEY (id)
)

#2729

CHECK-Constraints werden nun reflektiert

Der PostgreSQL-Dialekt unterstützt nun die Reflexion von CHECK-Constraints sowohl in der Methode Inspector.get_check_constraints() als auch in der Table-Reflexion innerhalb der Table.constraints-Sammlung.

„Plain“- und „Materialized“-Views können separat inspiziert werden

Das neue Argument PGInspector.get_view_names.include ermöglicht die Angabe, welche Untertypen von Views zurückgegeben werden sollen

from sqlalchemy import inspect

insp = inspect(engine)

plain_views = insp.get_view_names(include="plain")
all_views = insp.get_view_names(include=("plain", "materialized"))

#3588

Tablespace-Option zu Index hinzugefügt

Das Index-Objekt akzeptiert nun das Argument postgresql_tablespace, um TABLESPACE anzugeben, auf die gleiche Weise, wie es vom Table-Objekt akzeptiert wird.

#3720

Unterstützung für PyGreSQL

Das DBAPI PyGreSQL wird nun unterstützt.

Das Modul „postgres“ wird entfernt

Das lange deprecate Modul sqlalchemy.dialects.postgres wurde entfernt; dies hat seit vielen Jahren eine Warnung ausgegeben und Projekte sollten sqlalchemy.dialects.postgresql verwenden. Engine-URLs der Form postgres:// funktionieren jedoch weiterhin.

Unterstützung für FOR UPDATE SKIP LOCKED / FOR NO KEY UPDATE / FOR KEY SHARE

Die neuen Parameter GenerativeSelect.with_for_update.skip_locked und GenerativeSelect.with_for_update.key_share in Core und ORM wenden eine Modifikation auf eine „SELECT…FOR UPDATE“- oder „SELECT…FOR SHARE“-Abfrage auf dem PostgreSQL-Backend an

  • SELECT FOR NO KEY UPDATE

    stmt = select([table]).with_for_update(key_share=True)
  • SELECT FOR UPDATE SKIP LOCKED

    stmt = select([table]).with_for_update(skip_locked=True)
  • SELECT FOR KEY SHARE

    stmt = select([table]).with_for_update(read=True, key_share=True)

Dialektverbesserungen und Änderungen - MySQL

MySQL JSON-Unterstützung

Ein neuer Typ JSON wird dem MySQL-Dialekt hinzugefügt, der den neu in MySQL 5.7 eingeführten JSON-Typ unterstützt. Dieser Typ ermöglicht sowohl die Persistenz von JSON als auch rudimentären indizierten Zugriff unter interner Verwendung der JSON_EXTRACT-Funktion. Eine indizierbare JSON-Spalte, die sowohl unter MySQL als auch unter PostgreSQL funktioniert, kann durch Verwendung des JSON-Datentyps, der sowohl für MySQL als auch für PostgreSQL gemeinsam ist, erreicht werden.

#3547

Unterstützung für AUTOCOMMIT „Isolationslevel“ hinzugefügt

Der MySQL-Dialekt akzeptiert nun den Wert „AUTOCOMMIT“ für die Parameter create_engine.isolation_level und Connection.execution_options.isolation_level

connection = engine.connect()
connection = connection.execution_options(isolation_level="AUTOCOMMIT")

Der Isolationslevel nutzt die verschiedenen „Autocommit“-Attribute, die von den meisten MySQL DBAPIs bereitgestellt werden.

#3332

Keine automatische Erzeugung eines impliziten KEYs mehr für zusammengesetzte Primärschlüssel mit AUTO_INCREMENT

Der MySQL-Dialekt hatte das Verhalten, dass, wenn ein zusammengesetzter Primärschlüssel auf einer InnoDB-Tabelle AUTO_INCREMENT auf einer seiner Spalten enthielt, die nicht die erste Spalte war, z. B.

t = Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True, autoincrement=False),
    Column("y", Integer, primary_key=True, autoincrement=True),
    mysql_engine="InnoDB",
)

DDL wie die folgende würde generiert werden

CREATE TABLE some_table (
    x INTEGER NOT NULL,
    y INTEGER NOT NULL AUTO_INCREMENT,
    PRIMARY KEY (x, y),
    KEY idx_autoinc_y (y)
)ENGINE=InnoDB

Beachten Sie oben „KEY“ mit einem automatisch generierten Namen; dies ist eine Änderung, die vor vielen Jahren in den Dialekt aufgenommen wurde, als Reaktion auf das Problem, dass AUTO_INCREMENT andernfalls auf InnoDB ohne diesen zusätzlichen KEY fehlschlagen würde.

Diese Problemumgehung wurde entfernt und durch das wesentlich bessere System ersetzt, die AUTO_INCREMENT-Spalte einfach *zuerst* innerhalb des Primärschlüssels anzugeben

CREATE TABLE some_table (
    x INTEGER NOT NULL,
    y INTEGER NOT NULL AUTO_INCREMENT,
    PRIMARY KEY (y, x)
)ENGINE=InnoDB

Um die explizite Kontrolle über die Reihenfolge von Primärschlüsselspalten zu behalten, verwenden Sie die PrimaryKeyConstraint-Konstruktion explizit (1.1.0b2) (zusammen mit einem KEY für die Autoincrement-Spalte, wie von MySQL benötigt), z. B.

t = Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True, autoincrement=True),
    PrimaryKeyConstraint("x", "y"),
    UniqueConstraint("y"),
    mysql_engine="InnoDB",
)

Zusammen mit der Änderung Die Direktive .autoincrement wird für eine zusammengesetzte Primärschlüsselspalte nicht mehr implizit aktiviert sind zusammengesetzte Primärschlüssel mit oder ohne Auto-Inkrement nun einfacher zu spezifizieren; Column.autoincrement ist nun standardmäßig auf den Wert "auto" gesetzt und die Direktiven autoincrement=False sind nicht mehr erforderlich

t = Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True, autoincrement=True),
    mysql_engine="InnoDB",
)

Dialektverbesserungen und Änderungen – SQLite

Rechts verschachtelte Join-Workaround für SQLite-Version 3.7.16 aufgehoben

In Version 0.9 durchlief die von Viele JOIN- und LEFT OUTER JOIN-Ausdrücke werden nicht mehr in (SELECT * FROM ..) AS ANON_1 verpackt eingeführte Funktion viele Anstrengungen, um Joins auf SQLite so umzuschreiben, dass sie immer Subqueries verwenden, um einen „Right-Nested-Join“-Effekt zu erzielen, da SQLite diese Syntax seit vielen Jahren nicht unterstützt hat. Ironischerweise war die in dieser Migration erwähnte SQLite-Version, 3.7.15.2, die *letzte* SQLite-Version, die diese Einschränkung hatte! Die nächste Version war 3.7.16 und die Unterstützung für Right-Nested-Joins wurde stillschweigend hinzugefügt. In 1.1 wurde die Arbeit zur Identifizierung der spezifischen SQLite-Version und des Quell-Commits, wo diese Änderung vorgenommen wurde, durchgeführt (die SQLite-Änderungsprotokolle verweisen darauf mit der kryptischen Phrase „Enhance the query optimizer to exploit transitive join constraints“ ohne Link zu einer Issue- oder Changenummer oder weiterer Erklärung), und die in dieser Änderung vorhandenen Workarounds werden nun für SQLite aufgehoben, wenn DBAPI die Version 3.7.16 oder höher meldet.

#3634

Workaround für gepunktete Spaltennamen für SQLite-Version 3.10.0 aufgehoben

Der SQLite-Dialekt verfügt seit langem über eine Umgehungslösung für ein Problem, bei dem der Datenbanktreiber für einige SQL-Ergebnisdatensätze, insbesondere bei Verwendung von UNION, nicht die korrekten Spaltennamen meldet. Die Umgehungslösung ist detailliert beschrieben unter Gepunktete Spaltennamen und erfordert, dass SQLAlchemy annimmt, dass jeder Spaltenname mit einem Punkt darin tatsächlich eine Kombination aus tabellenname.spaltenname ist, die über dieses fehlerhafte Verhalten geliefert wird, mit der Option, dies über die Ausführungsoption sqlite_raw_colnames zu deaktivieren.

Ab SQLite-Version 3.10.0 wurde der Fehler in UNION und anderen Abfragen behoben. Ähnlich wie bei der in Rechts verschachtelte Join-Workaround für SQLite-Version 3.7.16 aufgehoben beschriebenen Änderung identifiziert die SQLite-Änderungsprotokoll sie nur kryptisch als „Hinzugefügt das Feld colUsed zu sqlite3_index_info zur Verwendung durch die Methode sqlite3_module.xBestIndex“. Die Übersetzung dieser gepunkteten Spaltennamen durch SQLAlchemy ist jedoch mit dieser Version nicht mehr erforderlich und wird daher deaktiviert, wenn Version 3.10.0 oder höher erkannt wird.

Insgesamt verlässt sich das SQLAlchemy ResultProxy ab der 1.0-Serie viel weniger auf Spaltennamen in Ergebnisdatensätzen, wenn Ergebnisse für Core- und ORM-SQL-Konstrukte geliefert werden, sodass die Bedeutung dieses Problems ohnehin bereits verringert war.

#3633

Verbesserte Unterstützung für Remote-Schemas

Der SQLite-Dialekt implementiert nun Inspector.get_schema_names() und bietet zusätzlich eine verbesserte Unterstützung für Tabellen und Indizes, die aus einem Remote-Schema erstellt und reflektiert werden. Dies ist bei SQLite eine Datenbank, der über die Anweisung ATTACH ein Name zugewiesen wird. Zuvor funktionierte die ``CREATE INDEX`` DDL für eine schema-gebundene Tabelle nicht korrekt, und die Methode Inspector.get_foreign_keys() gibt nun das angegebene Schema in den Ergebnissen an. Schemap übergreifende Fremdschlüssel werden nicht unterstützt.

Reflektion des Namens von PRIMARY KEY-Constraints

Das SQLite-Backend nutzt nun die „sqlite_master“-Ansicht von SQLite, um den Namen des Primärschlüssel-Constraints einer Tabelle aus der ursprünglichen DDL zu extrahieren, genauso wie dies bei Fremdschlüssel-Constraints in neueren SQLAlchemy-Versionen geschieht.

#3629

Check-Constraints werden jetzt reflektiert

Der SQLite-Dialekt unterstützt nun die Reflektion von CHECK-Constraints sowohl innerhalb der Methode Inspector.get_check_constraints() als auch bei der Table-Reflektion innerhalb der Sammlung Table.constraints.

ON DELETE und ON UPDATE Klauseln für Fremdschlüssel werden nun reflektiert

Der Inspector wird nun die ON DELETE- und ON UPDATE-Klauseln von Fremdschlüssel-Constraints für den SQLite-Dialekt einschließen, und das ForeignKeyConstraint-Objekt, das als Teil einer Table reflektiert wird, wird diese Klauseln ebenfalls angeben.

Dialektverbesserungen und Änderungen - SQL Server

Unterstützung für Transaktionsisolationslevel für SQL Server hinzugefügt

Alle SQL Server-Dialekte unterstützen Transaktionsisolationslevel-Einstellungen über die Parameter create_engine.isolation_level und Connection.execution_options.isolation_level. Die vier Standardlevel werden unterstützt, ebenso wie SNAPSHOT

engine = create_engine(
    "mssql+pyodbc://scott:tiger@ms_2008", isolation_level="REPEATABLE READ"
)

#3534

String / Var-Length-Typen repräsentieren „max“ bei der Reflektion nicht mehr explizit

Beim Reflektieren eines Typs wie String, TextClause usw., der eine Länge enthält, kopierte ein „un-längter“ Typ unter SQL Server den Parameter „length“ als Wert "max"

>>> from sqlalchemy import create_engine, inspect
>>> engine = create_engine("mssql+pyodbc://scott:tiger@ms_2008", echo=True)
>>> engine.execute("create table s (x varchar(max), y varbinary(max))")
>>> insp = inspect(engine)
>>> for col in insp.get_columns("s"):
...     print(col["type"].__class__, col["type"].length)
<class 'sqlalchemy.sql.sqltypes.VARCHAR'> max
<class 'sqlalchemy.dialects.mssql.base.VARBINARY'> max

Der Parameter „length“ in den Basistypen wird als Integer-Wert oder nur als None erwartet. None bedeutet unbegrenzte Länge, die der SQL Server-Dialekt als „max“ interpretiert. Die Korrektur besteht also darin, dass diese Längen als None ausgegeben werden, damit die Typobjekte auch in Nicht-SQL-Server-Kontexten funktionieren.

>>> for col in insp.get_columns("s"):
...     print(col["type"].__class__, col["type"].length)
<class 'sqlalchemy.sql.sqltypes.VARCHAR'> None
<class 'sqlalchemy.dialects.mssql.base.VARBINARY'> None

Anwendungen, die sich möglicherweise auf einen direkten Vergleich des „length“-Wertes mit dem String „max“ verlassen haben, sollten den Wert von None als gleichbedeutend betrachten.

#3504

Unterstützung für „non clustered“ für Primärschlüssel, um ein Clustering an anderer Stelle zu ermöglichen

Das Flag mssql_clustered, das auf UniqueConstraint, PrimaryKeyConstraint, Index verfügbar ist, hat nun standardmäßig den Wert None und kann auf False gesetzt werden, was insbesondere für einen Primärschlüssel das Schlüsselwort NONCLUSTERED rendert und somit die Verwendung eines anderen Index als „clustered“ ermöglicht.

Das Flag legacy_schema_aliasing ist nun auf False gesetzt

SQLAlchemy 1.0.5 führte das Flag legacy_schema_aliasing für den MSSQL-Dialekt ein, mit dem das sogenannte „Legacy Mode“-Aliasing deaktiviert werden kann. Dieses Aliasing versucht, schem qualifizierte Tabellen in Aliase umzuwandeln. Bei einer Tabelle wie

account_table = Table(
    "account",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("info", String(100)),
    schema="customer_schema",
)

Der Legacy-Modus wird versuchen, einen schema-qualifizierten Tabellennamen in einen Alias umzuwandeln

>>> eng = create_engine("mssql+pymssql://mydsn", legacy_schema_aliasing=True)
>>> print(account_table.select().compile(eng))
SELECT account_1.id, account_1.info FROM customer_schema.account AS account_1

Dieses Aliasing hat sich jedoch als unnötig erwiesen und erzeugt in vielen Fällen fehlerhafte SQL.

In SQLAlchemy 1.1 hat das Flag legacy_schema_aliasing nun standardmäßig den Wert False, wodurch dieser Modus deaktiviert und der MSSQL-Dialekt mit schema-qualifizierten Tabellen normal funktionieren kann. Für Anwendungen, die sich auf dieses Verhalten verlassen, sollte das Flag auf True zurückgesetzt werden.

#3434

Dialektverbesserungen und Änderungen - Oracle

Unterstützung für SKIP LOCKED

Der neue Parameter GenerativeSelect.with_for_update.skip_locked in Core und ORM generiert den Suffix „SKIP LOCKED“ für eine „SELECT…FOR UPDATE“- oder „SELECT.. FOR SHARE“-Abfrage.