Was ist neu in SQLAlchemy 1.2?

Über dieses Dokument

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

Einleitung

Diese Anleitung beschreibt, was es in SQLAlchemy Version 1.2 Neues gibt, und dokumentiert auch Änderungen, die Benutzer beim Migrieren ihrer Anwendungen von der 1.1er-Serie von SQLAlchemy zu 1.2 betreffen.

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

Plattformunterstützung

Zielgruppe: Python 2.7 und neuer

SQLAlchemy 1.2 verschiebt die minimale Python-Version auf 2.7 und unterstützt 2.6 nicht mehr. Neue Sprachfunktionen, die in Python 2.6 nicht unterstützt wurden, werden voraussichtlich in die 1.2-Serie integriert. Für Python 3-Unterstützung wird SQLAlchemy derzeit auf den Versionen 3.5 und 3.6 getestet.

Neue Features und Verbesserungen - ORM

„Baked“ Loading ist jetzt Standard für Lazy Loads

Die Erweiterung sqlalchemy.ext.baked, die erstmals in der 1.0-Serie eingeführt wurde, ermöglicht die Konstruktion eines sogenannten BakedQuery-Objekts. Dieses Objekt generiert ein Query-Objekt in Verbindung mit einem Cache-Schlüssel, der die Struktur der Abfrage darstellt. Dieser Cache-Schlüssel wird dann mit der resultierenden SQL-Anweisung verknüpft, sodass eine spätere Verwendung eines weiteren BakedQuery mit derselben Struktur den gesamten Overhead des Aufbaus des Query-Objekts, des Aufbaus des Kern-select()-Objekts und der Kompilierung des select() zu einer Zeichenkette umgeht und somit den Großteil des Funktionsaufruf-Overheads reduziert, der normalerweise mit der Erstellung und Ausgabe eines ORM Query-Objekts verbunden ist.

Das BakedQuery wird jetzt standardmäßig vom ORM verwendet, wenn es eine „lazy“ Abfrage für das lazy Laden einer relationship()-Konstruktion generiert, z. B. die des Standard lazy="select" Relationship-Loader-Strategie. Dies ermöglicht eine signifikante Reduzierung der Funktionsaufrufe im Kontext der Verwendung von Lazy-Load-Abfragen durch eine Anwendung, um Collections und verwandte Objekte zu laden. Zuvor war diese Funktion in 1.0 und 1.1 über die Verwendung einer globalen API-Methode oder über die baked_select-Strategie verfügbar, jetzt ist es die einzige Implementierung für dieses Verhalten. Die Funktion wurde auch verbessert, sodass das Caching auch für Objekte erfolgen kann, die nach dem Lazy Load zusätzliche Loader-Optionen aktiv haben.

Das Caching-Verhalten kann pro Beziehung über das Flag relationship.bake_queries deaktiviert werden. Dies ist für sehr ungewöhnliche Fälle gedacht, wie z. B. eine Beziehung, die eine benutzerdefinierte Query-Implementierung verwendet, die nicht mit dem Caching kompatibel ist.

#3954

Neues „selectin“ Eager Loading, lädt alle Collections auf einmal mittels IN

Ein neuer Eager-Loader namens „selectin“-Loading wurde hinzugefügt. Dieser ähnelt in vielerlei Hinsicht dem „subquery“-Loading, erzeugt jedoch eine einfachere SQL-Anweisung, die cachbar und effizienter ist.

Gegeben sei die folgende Abfrage

q = (
    session.query(User)
    .filter(User.name.like("%ed%"))
    .options(subqueryload(User.addresses))
)

Die erzeugte SQL wäre die Abfrage gegen User, gefolgt von der Subqueryload für User.addresses (beachten Sie, dass auch die Parameter aufgeführt sind)

SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)

SELECT addresses.id AS addresses_id,
       addresses.user_id AS addresses_user_id,
       addresses.email_address AS addresses_email_address,
       anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id
FROM users
WHERE users.name LIKE ?) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
('%ed%',)

Mit „selectin“-Loading erhalten wir stattdessen ein SELECT, das sich auf die tatsächlichen Primärschlüsselwerte bezieht, die in der übergeordneten Abfrage geladen wurden

q = (
    session.query(User)
    .filter(User.name.like("%ed%"))
    .options(selectinload(User.addresses))
)

Erzeugt

SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)

SELECT users_1.id AS users_1_id,
       addresses.id AS addresses_id,
       addresses.user_id AS addresses_user_id,
       addresses.email_address AS addresses_email_address
FROM users AS users_1
JOIN addresses ON users_1.id = addresses.user_id
WHERE users_1.id IN (?, ?)
ORDER BY users_1.id
(1, 3)

Die obige SELECT-Anweisung bietet folgende Vorteile

  • Sie verwendet keine Subquery, sondern nur einen INNER JOIN. Das bedeutet, dass sie bei Datenbanken wie MySQL, die keine Subqueries mögen, viel besser funktioniert.

  • Ihre Struktur ist unabhängig von der ursprünglichen Abfrage. In Verbindung mit dem neuen erweiterten IN-Parameter-System können wir in den meisten Fällen die „baked“-Abfrage verwenden, um die SQL-Zeichenkette zu cachen, was den Aufwand pro Abfrage erheblich reduziert.

  • Da die Abfrage nur für eine gegebene Liste von Primärschlüssel-IDs abfragt, ist das „selectin“-Loading potenziell mit Query.yield_per() kompatibel, um die Ergebnisse eines SELECTs stückweise zu verarbeiten, vorausgesetzt, der Datenbanktreiber erlaubt mehrere gleichzeitige Cursors (SQLite, PostgreSQL; **nicht** MySQL-Treiber oder SQL Server ODBC-Treiber). Weder „joined eager loading“ noch „subquery eager loading“ sind mit Query.yield_per() kompatibel.

Nachteile des selectin eager loading sind potenziell große SQL-Abfragen mit großen Listen von IN-Parametern. Die Listen von IN-Parametern selbst werden in Gruppen von 500 aufgeteilt, sodass ein Ergebnis-Set von mehr als 500 führenden Objekten zusätzliche „SELECT IN“-Abfragen nach sich zieht. Außerdem hängt die Unterstützung für zusammengesetzte Primärschlüssel von der Fähigkeit der Datenbank ab, Tupel mit IN zu verwenden, z. B. (table.column_one, table_column_two) IN ((?, ?), (?, ?) (?, ?)). Derzeit sind PostgreSQL und MySQL mit dieser Syntax kompatibel, SQLite nicht.

Siehe auch

Select IN Loading

#3944

„selectin“ Polomorphes Laden, lädt Unterklassen mittels separater IN-Abfragen

Ähnlich wie die gerade beschriebene „selectin“-Beziehungs-Ladefunktion unter Neues „selectin“ Eager Loading, lädt alle Collections auf einmal mittels IN ist das „selectin“-polomorphe Laden. Dies ist eine polomorphe Ladefunktion, die hauptsächlich für „joined eager loading“ zugeschnitten ist und es ermöglicht, die Basis-Entität mit einer einfachen SELECT-Anweisung zu laden, aber dann werden die Attribute der zusätzlichen Unterklassen mit zusätzlichen SELECT-Anweisungen geladen.

>>> from sqlalchemy.orm import selectin_polymorphic

>>> query = session.query(Employee).options(
...     selectin_polymorphic(Employee, [Manager, Engineer])
... )

>>> query.all()
SELECT employee.id AS employee_id, employee.name AS employee_name, employee.type AS employee_type FROM employee () SELECT engineer.id AS engineer_id, employee.id AS employee_id, employee.type AS employee_type, engineer.engineer_name AS engineer_engineer_name FROM employee JOIN engineer ON employee.id = engineer.id WHERE employee.id IN (?, ?) ORDER BY employee.id (1, 2) SELECT manager.id AS manager_id, employee.id AS employee_id, employee.type AS employee_type, manager.manager_name AS manager_manager_name FROM employee JOIN manager ON employee.id = manager.id WHERE employee.id IN (?) ORDER BY employee.id (3,)

#3948

ORM-Attribute, die Ad-hoc SQL-Ausdrücke empfangen können

Ein neuer ORM-Attributtyp query_expression() wird hinzugefügt, der deferred() ähnlich ist, außer dass sein SQL-Ausdruck zur Abfragezeit mithilfe einer neuen Option with_expression() bestimmt wird; falls nicht angegeben, ist das Attribut standardmäßig None

from sqlalchemy.orm import query_expression
from sqlalchemy.orm import with_expression


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

    # will be None normally...
    expr = query_expression()


# but let's give it x + y
a1 = session.query(A).options(with_expression(A.expr, A.x + A.y)).first()
print(a1.expr)

#3058

ORM-Unterstützung für Mehr-Tabellen-Löschungen

Die ORM-Methode Query.delete() unterstützt Mehr-Tabellen-Kriterien für DELETE, wie unter Mehr-Tabellen-Kriterienunterstützung für DELETE eingeführt. Die Funktion funktioniert genauso wie Mehr-Tabellen-Kriterien für UPDATE, die erstmals in 0.8 eingeführt und unter Query.update() unterstützt UPDATE..FROM beschrieben wurde.

Unten geben wir eine DELETE-Anweisung gegen SomeEntity aus und fügen eine FROM-Klausel (oder Äquivalent, je nach Backend) gegen SomeOtherEntity hinzu.

query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
    SomeOtherEntity.foo == "bar"
).delete()

#959

Unterstützung für Bulk-Updates von Hybriden, Composites

Sowohl Hybrid-Attribute (z. B. sqlalchemy.ext.hybrid) als auch zusammengesetzte Attribute (Zusammengesetzte Spaltentypen) unterstützen jetzt die Verwendung in der SET-Klausel einer UPDATE-Anweisung bei Verwendung von Query.update().

Für Hybride können einfache Ausdrücke direkt verwendet werden, oder der neue Dekorator hybrid_property.update_expression() kann verwendet werden, um einen Wert in mehrere Spalten/Ausdrücke aufzuteilen.

class Person(Base):
    # ...

    first_name = Column(String(10))
    last_name = Column(String(10))

    @hybrid.hybrid_property
    def name(self):
        return self.first_name + " " + self.last_name

    @name.expression
    def name(cls):
        return func.concat(cls.first_name, " ", cls.last_name)

    @name.update_expression
    def name(cls, value):
        f, l = value.split(" ", 1)
        return [(cls.first_name, f), (cls.last_name, l)]

Oben kann ein UPDATE gerendert werden mittels

session.query(Person).filter(Person.id == 5).update({Person.name: "Dr. No"})

Ähnliche Funktionalität ist für Composites verfügbar, bei denen zusammengesetzte Werte für Bulk-UPDATE in ihre einzelnen Spalten zerlegt werden.

session.query(Vertex).update({Edge.start: Point(3, 4)})

Hybrid-Attribute unterstützen die Wiederverwendung über Unterklassen hinweg, Neudefinition von @getter

Die Klasse sqlalchemy.ext.hybrid.hybrid_property unterstützt jetzt den Aufruf von Mutatoren wie @setter, @expression etc. mehrmals über Unterklassen hinweg und bietet jetzt einen @getter-Mutator, sodass eine bestimmte Hybrid-Eigenschaft über Unterklassen oder andere Klassen wiederverwendet werden kann. Dies ähnelt nun dem Verhalten von @property in Standard-Python.

class FirstNameOnly(Base):
    # ...

    first_name = Column(String)

    @hybrid_property
    def name(self):
        return self.first_name

    @name.setter
    def name(self, value):
        self.first_name = value


class FirstNameLastName(FirstNameOnly):
    # ...

    last_name = Column(String)

    @FirstNameOnly.name.getter
    def name(self):
        return self.first_name + " " + self.last_name

    @name.setter
    def name(self, value):
        self.first_name, self.last_name = value.split(" ", maxsplit=1)

    @name.expression
    def name(cls):
        return func.concat(cls.first_name, " ", cls.last_name)

Oben wird die Hybrid-Eigenschaft FirstNameOnly.name von der Unterklasse FirstNameLastName referenziert, um sie speziell für die neue Unterklasse wiederzuverwenden. Dies geschieht durch Kopieren des Hybrid-Objekts in ein neues Objekt innerhalb jedes Aufrufs von @getter, @setter sowie in allen anderen Mutatormethoden wie @expression, wobei die Definition der vorherigen Hybrid-Eigenschaft intakt bleibt. Zuvor modifizierten Methoden wie @setter die vorhandene Hybrid-Eigenschaft direkt und beeinträchtigten die Definition in der Oberklasse.

Hinweis

Lesen Sie die Dokumentation unter Wiederverwendung von Hybrid-Eigenschaften über Unterklassen hinweg für wichtige Hinweise zur Überschreibung von hybrid_property.expression() und hybrid_property.comparator(), da ein spezieller Qualifier hybrid_property.overrides in einigen Fällen notwendig sein kann, um Namenskonflikte mit QueryableAttribute zu vermeiden.

Hinweis

Diese Änderung in @hybrid_property bedeutet, dass bei Hinzufügen von Settern und anderem Zustand zu einer @hybrid_property die **Methoden den Namen der ursprünglichen Hybrid-Eigenschaft beibehalten müssen**, andernfalls wird die neue Hybrid-Eigenschaft mit dem zusätzlichen Zustand unter dem nicht übereinstimmenden Namen in der Klasse vorhanden sein. Dies ist dasselbe Verhalten wie bei der @property-Konstruktion, die Teil des Standard-Pythons ist.

class FirstNameOnly(Base):
    @hybrid_property
    def name(self):
        return self.first_name

    # WRONG - will raise AttributeError: can't set attribute when
    # assigning to .name
    @name.setter
    def _set_name(self, value):
        self.first_name = value


class FirstNameOnly(Base):
    @hybrid_property
    def name(self):
        return self.first_name

    # CORRECT - note regular Python @property works the same way
    @name.setter
    def name(self, value):
        self.first_name = value

#3911

#3912

Neues bulk_replace Event

Um den unter Eine @validates-Methode empfängt alle Werte bei Bulk-Collection-Set vor dem Vergleich beschriebenen Validierungsfall zu unterstützen, wird eine neue Methode AttributeEvents.bulk_replace() hinzugefügt. Diese wird in Verbindung mit den Events AttributeEvents.append() und AttributeEvents.remove() aufgerufen. „bulk_replace“ wird vor „append“ und „remove“ aufgerufen, damit die Collection vor dem Vergleich mit der bestehenden Collection modifiziert werden kann. Danach werden einzelne Elemente an eine neue Ziel-Collection angehängt, wodurch das „append“-Event für neue Elemente in der Collection ausgelöst wird, wie es bisher der Fall war. Unten wird sowohl „bulk_replace“ als auch „append“ gleichzeitig veranschaulicht, einschließlich der Tatsache, dass „append“ ein Objekt erhält, das bereits von „bulk_replace“ behandelt wurde, wenn eine Collection-Zuweisung verwendet wird. Ein neues Symbol attributes.OP_BULK_REPLACE kann verwendet werden, um festzustellen, ob dieses „append“-Event der zweite Teil eines Bulk-Replaces ist.

from sqlalchemy.orm.attributes import OP_BULK_REPLACE


@event.listens_for(SomeObject.collection, "bulk_replace")
def process_collection(target, values, initiator):
    values[:] = [_make_value(value) for value in values]


@event.listens_for(SomeObject.collection, "append", retval=True)
def process_collection(target, value, initiator):
    # make sure bulk_replace didn't already do it
    if initiator is None or initiator.op is not OP_BULK_REPLACE:
        return _make_value(value)
    else:
        return value

#3896

Neuer „modified“-Event-Handler für sqlalchemy.ext.mutable

Ein neuer Event-Handler AttributeEvents.modified() wird hinzugefügt. Dieser wird als Reaktion auf Aufrufe der Methode flag_modified() ausgelöst, die normalerweise von der Erweiterung sqlalchemy.ext.mutable aufgerufen wird.

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy import event

Base = declarative_base()


class MyDataClass(Base):
    __tablename__ = "my_data"
    id = Column(Integer, primary_key=True)
    data = Column(MutableDict.as_mutable(JSONEncodedDict))


@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance):
    print("json value modified:", instance.data)

Oben wird der Event-Handler ausgelöst, wenn eine In-Place-Änderung am .data-Dictionary vorgenommen wird.

#3303

„for update“-Argumente zu Session.refresh hinzugefügt

Zu der Methode Session.refresh() wurde das neue Argument Session.refresh.with_for_update hinzugefügt. Als die Methode Query.with_lockmode() zugunsten von Query.with_for_update() deprecated wurde, wurde die Methode Session.refresh() nie aktualisiert, um die neue Option widerzuspiegeln.

session.refresh(some_object, with_for_update=True)

Das Argument Session.refresh.with_for_update akzeptiert ein Wörterbuch mit Optionen, die als dieselben Argumente übergeben werden, die auch an Query.with_for_update() gesendet werden.

session.refresh(some_objects, with_for_update={"read": True})

Der neue Parameter überschreibt den Parameter Session.refresh.lockmode.

#3991

In-place Mutationsoperatoren funktionieren für MutableSet, MutableList

Implementiert wurden die In-place Mutationsoperatoren __ior__, __iand__, __ixor__ und __isub__ für MutableSet und __iadd__ für MutableList. Während diese Methoden die Collection zuvor erfolgreich aktualisiert hätten, hätten sie keine Änderungsereignisse korrekt ausgelöst. Die Operatoren mutieren die Collection wie zuvor, lösen aber zusätzlich das korrekte Änderungsereignis aus, sodass die Änderung Teil des nächsten Flush-Prozesses wird.

model = session.query(MyModel).first()
model.json_set &= {1, 3}

#3853

AssociationProxy any(), has(), contains() funktionieren mit verketteten Association Proxies

Die Vergleichsmethoden AssociationProxy.any(), AssociationProxy.has() und AssociationProxy.contains() unterstützen jetzt die Verknüpfung mit einem Attribut, das selbst ebenfalls ein AssociationProxy ist, und zwar rekursiv. Unten ist A.b_values ein Association Proxy, der zu AtoB.bvalue verknüpft ist, welches selbst ein Association Proxy zu B ist.

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

    b_values = association_proxy("atob", "b_value")
    c_values = association_proxy("atob", "c_value")


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

    c = relationship("C")


class C(Base):
    __tablename__ = "c"
    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey("b.id"))
    value = Column(String)


class AtoB(Base):
    __tablename__ = "atob"

    a_id = Column(ForeignKey("a.id"), primary_key=True)
    b_id = Column(ForeignKey("b.id"), primary_key=True)

    a = relationship("A", backref="atob")
    b = relationship("B", backref="atob")

    b_value = association_proxy("b", "value")
    c_value = association_proxy("b", "c")

Wir können auf A.b_values mit AssociationProxy.contains() abfragen, um über die beiden Proxies A.b_values und AtoB.b_value abzufragen.

>>> s.query(A).filter(A.b_values.contains("hi")).all()
SELECT a.id AS a_id FROM a WHERE EXISTS (SELECT 1 FROM atob WHERE a.id = atob.a_id AND (EXISTS (SELECT 1 FROM b WHERE b.id = atob.b_id AND b.value = :value_1)))

Ähnlich können wir auf A.c_values mit AssociationProxy.any() abfragen, um über die beiden Proxies A.c_values und AtoB.c_value abzufragen.

>>> s.query(A).filter(A.c_values.any(value="x")).all()
SELECT a.id AS a_id FROM a WHERE EXISTS (SELECT 1 FROM atob WHERE a.id = atob.a_id AND (EXISTS (SELECT 1 FROM b WHERE b.id = atob.b_id AND (EXISTS (SELECT 1 FROM c WHERE b.id = c.b_id AND c.value = :value_1)))))

#3769

Identitäts-Schlüssel-Verbesserungen zur Unterstützung von Sharding

Die vom ORM verwendete Identitäts-Schlüssel-Struktur enthält nun ein zusätzliches Mitglied, sodass zwei identische Primärschlüssel, die aus verschiedenen Kontexten stammen, innerhalb derselben Identitätsmap koexistieren können.

Das Beispiel unter Horizontales Sharding wurde aktualisiert, um dieses Verhalten zu veranschaulichen. Das Beispiel zeigt eine geshardete Klasse WeatherLocation, die auf ein abhängiges WeatherReport-Objekt verweist, wobei die Klasse WeatherReport einer Tabelle zugeordnet ist, die einen einfachen ganzzahligen Primärschlüssel speichert. Zwei WeatherReport-Objekte aus verschiedenen Datenbanken können denselben Primärschlüsselwert haben. Das Beispiel zeigt nun, dass ein neues Feld identity_token diesen Unterschied verfolgt, sodass die beiden Objekte in derselben Identitätsmap koexistieren können.

tokyo = WeatherLocation("Asia", "Tokyo")
newyork = WeatherLocation("North America", "New York")

tokyo.reports.append(Report(80.0))
newyork.reports.append(Report(75))

sess = create_session()

sess.add_all([tokyo, newyork, quito])

sess.commit()

# the Report class uses a simple integer primary key.  So across two
# databases, a primary key will be repeated.  The "identity_token" tracks
# in memory that these two identical primary keys are local to different
# databases.

newyork_report = newyork.reports[0]
tokyo_report = tokyo.reports[0]

assert inspect(newyork_report).identity_key == (Report, (1,), "north_america")
assert inspect(tokyo_report).identity_key == (Report, (1,), "asia")

# the token representing the originating shard is also available directly

assert inspect(newyork_report).identity_token == "north_america"
assert inspect(tokyo_report).identity_token == "asia"

#4137

Neue Features und Verbesserungen - Core

Boolean-Datentyp erzwingt jetzt strikte True/False/None-Werte

In Version 1.1 führte die Änderung, die unter Nicht-native boolesche Ganzzahlwerte werden in allen Fällen auf null/eins/None abgebildet beschrieben wurde, zu einem unbeabsichtigten Nebeneffekt, nämlich einer Änderung des Verhaltens von Boolean bei der Übergabe von Nicht-Ganzzahlwerten, z. B. Zeichenketten. Insbesondere der Zeichenkettenwert "0", der zuvor den Wert False erzeugte, erzeugte jetzt True. Verschlimmert wurde die Situation durch die Tatsache, dass die Verhaltensänderung nur für einige Backends und nicht für andere galt, was bedeutete, dass Code, der Zeichenketten "0"-Werte an Boolean sendete, inkonsistent über Backends hinweg ausfiel.

Die endgültige Lösung für dieses Problem ist, dass **Zeichenkettenwerte für Boolean nicht unterstützt werden**. Daher wird in 1.2 ein harter TypeError ausgelöst, wenn ein Nicht-Ganzzahl-/True/False/None-Wert übergeben wird. Außerdem werden nur die Ganzzahlwerte 0 und 1 akzeptiert.

Um Anwendungen zu unterstützen, die eine liberalere Interpretation von booleschen Werten wünschen, sollte TypeDecorator verwendet werden. Unten ist ein Rezept dargestellt, das das „liberale“ Verhalten des Boolean-Datentyps vor 1.1 ermöglicht.

from sqlalchemy import Boolean
from sqlalchemy import TypeDecorator


class LiberalBoolean(TypeDecorator):
    impl = Boolean

    def process_bind_param(self, value, dialect):
        if value is not None:
            value = bool(int(value))
        return value

#4102

Pessimistische Verbindungsunterbrechungserkennung dem Connection Pool hinzugefügt

Die Dokumentation des Connection Pools enthielt lange Zeit ein Rezept zur Verwendung des ConnectionEvents.engine_connect()-Engine-Events, um eine einfache Anweisung an eine ausgecheckte Verbindung zu senden und sie auf Lebensfähigkeit zu testen. Die Funktionalität dieses Rezepts wurde nun in den Connection Pool selbst integriert, wenn er mit einem geeigneten Dialekt verwendet wird. Durch den neuen Parameter create_engine.pool_pre_ping wird jede ausgecheckte Verbindung vor der Rückgabe auf Aktualität geprüft.

engine = create_engine("mysql+pymysql://", pool_pre_ping=True)

Während der „pre-ping“-Ansatz eine geringe Latenz beim Checkout des Connection Pools hinzufügt, ist dieser Overhead für eine typische, transaktional orientierte Anwendung (was die meisten ORM-Anwendungen einschließt) minimal und eliminiert das Problem des Erhaltens einer veralteten Verbindung, die einen Fehler auslösen würde und die Anwendung dazu zwingt, den Vorgang abzubrechen oder zu wiederholen.

Die Funktion unterstützt **nicht** Verbindungen, die während einer laufenden Transaktion oder SQL-Operation verloren gehen. Wenn eine Anwendung auch diese abfangen muss, muss sie eigene Retry-Logik für Vorgänge implementieren, um diese Fehler zu antizipieren.

#3919

Das Verhalten des leeren Collections für IN / NOT IN Operatoren ist jetzt konfigurierbar; Standardausdruck vereinfacht

Ein Ausdruck wie column.in_([]), der als falsch angenommen wird, erzeugt nun standardmäßig den Ausdruck 1 != 1 anstelle von column != column. Dies **ändert das Ergebnis** einer Abfrage, die einen SQL-Ausdruck oder eine Spalte vergleicht, die NULL ergibt, wenn sie mit einer leeren Menge verglichen wird, und erzeugt einen booleschen Wert falsch oder wahr (für NOT IN) anstelle von NULL. Die unter dieser Bedingung auftretende Warnung wird ebenfalls entfernt. Das alte Verhalten ist über den Parameter create_engine.empty_in_strategy für create_engine() verfügbar.

In SQL unterstützen die Operatoren IN und NOT IN keinen Vergleich mit einer explizit leeren Collection von Werten; das heißt, diese Syntax ist illegal.

mycolumn IN ()

Um dies zu umgehen, erkennen SQLAlchemy und andere Datenbankbibliotheken diese Bedingung und rendern einen alternativen Ausdruck, der falsch oder im Fall von NOT IN wahr ergibt, basierend auf der Theorie, dass „col IN ()“ immer falsch ist, da nichts „in der leeren Menge“ ist. Typischerweise wird verwendet, um eine boolesche Konstante zu erzeugen, die über verschiedene Datenbanken hinweg portabel ist und im Kontext der WHERE-Klausel funktioniert, eine einfache Tautologie wie 1 != 1 verwendet, um falsch auszuwerten, und 1 = 1, um wahr auszuwerten (eine einfache Konstante „0“ oder „1“ funktioniert oft nicht als Ziel einer WHERE-Klausel).

SQLAlchemy begann in seinen frühen Tagen ebenfalls mit diesem Ansatz, aber bald wurde die Theorie aufgestellt, dass der SQL-Ausdruck column IN () nicht falsch ergibt, wenn die „column“ NULL ist; stattdessen ergibt der Ausdruck NULL, da „NULL“ „unbekannt“ bedeutet und Vergleiche mit NULL in SQL normalerweise NULL ergeben.

Um dieses Ergebnis zu simulieren, wechselte SQLAlchemy von der Verwendung von 1 != 1 zur Verwendung des Ausdrucks expr != expr für leeres „IN“ und expr = expr für leeres „NOT IN“; das heißt, anstatt eines festen Werts verwenden wir die tatsächliche linke Seite des Ausdrucks. Wenn die linke Seite des Ausdrucks NULL ergibt, erhält der Vergleich insgesamt ebenfalls das NULL-Ergebnis anstelle von falsch oder wahr.

Leider beschwerten sich Benutzer schließlich, dass dieser Ausdruck einen sehr schweren Leistungseffekt auf einige Query Planner hatte. Zu diesem Zeitpunkt wurde eine Warnung hinzugefügt, wenn ein leerer IN-Ausdruck angetroffen wurde, was SQLAlchemy weiterhin „korrekt“ hält und Benutzer auffordert, Code zu vermeiden, der leere IN-Prädikate generiert, da diese im Allgemeinen sicher weggelassen werden können. Dies ist jedoch natürlich bei Abfragen, die dynamisch aus Eingabevariablen aufgebaut werden und bei denen ein eingehendes Set von Werten leer sein kann, aufwendig.

In den letzten Monaten wurden die ursprünglichen Annahmen dieser Entscheidung in Frage gestellt. Die Annahme, dass der Ausdruck „NULL IN ()“ NULL zurückgeben sollte, war nur theoretisch und konnte nicht getestet werden, da Datenbanken diese Syntax nicht unterstützen. Wie sich jedoch herausstellt, kann man eine relationale Datenbank tatsächlich fragen, welchen Wert sie für „NULL IN ()“ zurückgeben würde, indem man die leere Menge wie folgt simuliert:

SELECT NULL IN (SELECT 1 WHERE 1 != 1)

Mit dem obigen Test sehen wir, dass die Datenbanken selbst keine Einigkeit über die Antwort erzielen können. PostgreSQL, das von den meisten als die „korrekteste“ Datenbank angesehen wird, gibt False zurück; denn obwohl „NULL“ für „unbekannt“ steht, bedeutet die „leere Menge“, dass nichts vorhanden ist, einschließlich aller unbekannten Werte. MySQL und MariaDB geben hingegen für den obigen Ausdruck NULL zurück und greifen auf das üblichere Verhalten zurück, dass „alle Vergleiche mit NULL ergeben NULL“.

Die SQL-Architektur von SQLAlchemy ist ausgefeilter als zum Zeitpunkt der ersten Designentscheidung, sodass wir nun entweder ein Verhalten zur Kompilierungszeit der SQL-Zeichenfolge aufrufen können. Zuvor erfolgte die Umwandlung in einen Vergleichsausdruck zur Konstruktionszeit, d. h. in dem Moment, in dem die Operatoren ColumnOperators.in_() oder ColumnOperators.notin_() aufgerufen wurden. Mit dem kompilierungszeitlichen Verhalten kann dem Dialekt selbst angewiesen werden, entweder einen Ansatz aufzurufen, d. h. den „statischen“ Vergleich 1 != 1 oder den „dynamischen“ Vergleich expr != expr. Der Standard wurde auf den „statischen“ Vergleich **geändert**, da dies mit dem Verhalten übereinstimmt, das PostgreSQL ohnehin hätte, und dies auch dem bevorzugten Verhalten der überwiegenden Mehrheit der Benutzer entspricht. Dies wird **das Ergebnis** einer Abfrage ändern, die einen Null-Ausdruck mit der leeren Menge vergleicht, insbesondere einer Abfrage nach der Negation where(~null_expr.in_([])), da diese nun als wahr und nicht als NULL ausgewertet wird.

Das Verhalten kann nun mit dem Flag create_engine.empty_in_strategy gesteuert werden, das standardmäßig auf die Einstellung "static" gesetzt ist, aber auch auf "dynamic" oder "dynamic_warn" gesetzt werden kann, wobei die Einstellung "dynamic_warn" dem früheren Verhalten entspricht, das expr != expr sowie eine Leistungswarnung ausgibt. Es wird jedoch erwartet, dass die meisten Benutzer den „statischen“ Standard begrüßen werden.

#3907

Spät erweiterte IN-Parametersätze ermöglichen IN-Ausdrücke mit zwischengespeicherten Anweisungen

Es wurde eine neue Art von bindparam() namens „expanding“ hinzugefügt. Diese ist für die Verwendung in IN-Ausdrücken vorgesehen, bei denen die Liste der Elemente zur Ausführungszeit der Anweisung und nicht zur Kompilierungszeit der Anweisung in einzelne gebundene Parameter gerendert wird. Dies ermöglicht sowohl die Verknüpfung eines einzelnen gebundenen Parameternamens mit einem IN-Ausdruck aus mehreren Elementen als auch die Verwendung von Abfrage-Caching mit IN-Ausdrücken. Das neue Feature ermöglicht es den zugehörigen Features „select in“ loading und „polymorphic in“ loading, die gepackte Query-Erweiterung zu nutzen, um den Overhead bei Aufrufen zu reduzieren.

stmt = select([table]).where(table.c.col.in_(bindparam("foo", expanding=True)))
conn.execute(stmt, {"foo": [1, 2, 3]})

Das Feature sollte innerhalb der 1.2er-Serie als **experimentell** betrachtet werden.

#3953

Vereinheitlichte Operator-Präzedenz für Vergleichsoperatoren

Die Operator-Präzedenz für Operatoren wie IN, LIKE, gleich, IS, MATCH und andere Vergleichsoperatoren wurde auf eine Ebene reduziert. Dies hat zur Folge, dass mehr Klammern generiert werden, wenn Vergleichsoperatoren kombiniert werden, wie z. B.

(column("q") == null()) != (column("y") == null())

wird nun (q IS NULL) != (y IS NULL) statt q IS NULL != y IS NULL generieren.

#3999

Unterstützung für SQL-Kommentare in Tabellen, Spalten, DDL, Reflexion

Core erhält Unterstützung für Zeichenketten-Kommentare, die mit Tabellen und Spalten verknüpft sind. Diese werden über die Argumente Table.comment und Column.comment angegeben werden.

Table(
    "my_table",
    metadata,
    Column("q", Integer, comment="the Q value"),
    comment="my Q table",
)

Wie oben erwähnt, wird DDL beim Erstellen von Tabellen entsprechend gerendert, um die obigen Kommentare mit der Tabelle/Spalte innerhalb des Schemas zu verknüpfen. Wenn die obige Tabelle automatisch geladen oder mit Inspector.get_columns() inspiziert wird, sind die Kommentare enthalten. Der Tabellenkommentar ist auch unabhängig über die Methode Inspector.get_table_comment() verfügbar.

Aktuelle Backend-Unterstützung umfasst MySQL, PostgreSQL und Oracle.

#1546

Unterstützung für Mehrfachkriterien bei DELETE

Das Delete-Konstrukt unterstützt nun Mehrfachkriterien, implementiert für die Backends, die dies unterstützen, derzeit PostgreSQL, MySQL und Microsoft SQL Server (Unterstützung wurde auch zum derzeit nicht funktionierenden Sybase-Dialekt hinzugefügt). Das Feature funktioniert genauso wie bei Mehrfachkriterien für UPDATE, die erstmals in den Serien 0.7 und 0.8 eingeführt wurden.

Gegeben eine Anweisung wie

stmt = (
    users.delete()
    .where(users.c.id == addresses.c.id)
    .where(addresses.c.email_address.startswith("ed%"))
)
conn.execute(stmt)

Die resultierende SQL aus der obigen Anweisung auf einem PostgreSQL-Backend würde wie folgt gerendert werden:

DELETE FROM users USING addresses
WHERE users.id = addresses.id
AND (addresses.email_address LIKE %(email_address_1)s || '%%')

#959

Neue Option „autoescape“ für startswith(), endswith()

Der Parameter „autoescape“ wird zu ColumnOperators.startswith(), ColumnOperators.endswith(), ColumnOperators.contains() hinzugefügt. Dieser Parameter, wenn er auf True gesetzt ist, maskiert automatisch alle Vorkommen von %, _ mit einem Escape-Zeichen, das standardmäßig ein Schrägstrich / ist; Vorkommen des Escape-Zeichens selbst werden ebenfalls maskiert. Der Schrägstrich wird verwendet, um Konflikte mit Einstellungen wie PostgreSQLs standard_confirming_strings, dessen Standardwert sich ab PostgreSQL 9.1 geändert hat, und MySQLs NO_BACKSLASH_ESCAPES-Einstellungen zu vermeiden. Der bestehende Parameter „escape“ kann nun verwendet werden, um das Autoescape-Zeichen bei Bedarf zu ändern.

Hinweis

Dieses Feature wurde seit 1.2.0 von seiner ursprünglichen Implementierung in 1.2.0b2 geändert, sodass autoescape nun als boolescher Wert übergeben wird, anstatt eines bestimmten Zeichens, das als Escape-Zeichen verwendet werden soll.

Ein Ausdruck wie

>>> column("x").startswith("total%score", autoescape=True)

Wird gerendert als

x LIKE :x_1 || '%' ESCAPE '/'

wobei der Wert des Parameters „x_1“ 'total/%score' ist.

Ähnlich wird ein Ausdruck, der Backslashes enthält,

>>> column("x").startswith("total/score", autoescape=True)

wird auf die gleiche Weise gerendert, mit dem Wert des Parameters „x_1“ als 'total//score'.

#2694

Stärkere Typisierung für „float“-Datentypen

Eine Reihe von Änderungen ermöglicht die Verwendung des Float-Datentyps, um ihn stärker an Python-Gleitkommawerte zu binden, anstatt an den allgemeineren Numeric. Die Änderungen beziehen sich hauptsächlich darauf, sicherzustellen, dass Python-Gleitkommawerte nicht fälschlicherweise in Decimal() umgewandelt werden und bei Bedarf auf float umgewandelt werden, auf der Ergebnis-Seite, wenn die Anwendung mit reinen Gleitkommazahlen arbeitet.

  • Ein reiner Python „float“-Wert, der an einen SQL-Ausdruck übergeben wird, wird nun mit dem Typ Float in einen Literalparameter gezogen; zuvor war der Typ Numeric mit dem Standardflag „asdecimal=True“, was bedeutete, dass der Ergebnistyp in Decimal() umgewandelt wurde. Insbesondere würde dies eine verwirrende Warnung auf SQLite ausgeben.

    float_value = connection.scalar(
        select([literal(4.56)])  # the "BindParameter" will now be
        # Float, not Numeric(asdecimal=True)
    )
  • Mathematische Operationen zwischen Numeric, Float und Integer behalten nun den Numeric- oder Float-Typ im resultierenden Ausdruckstyp bei, einschließlich des asdecimal-Flags und ob der Typ Float sein soll.

    # asdecimal flag is maintained
    expr = column("a", Integer) * column("b", Numeric(asdecimal=False))
    assert expr.type.asdecimal == False
    
    # Float subclass of Numeric is maintained
    expr = column("a", Integer) * column("b", Float())
    assert isinstance(expr.type, Float)
  • Der Datentyp Float wendet den float()-Prozessor bedingungslos auf Ergebniswerte an, wenn bekannt ist, dass der DBAPI den nativen Decimal()-Modus unterstützt. Einige Backends garantieren nicht immer, dass eine Gleitkommazahl als reines Float und nicht als Präzisions-Numerik zurückgegeben wird, wie z. B. MySQL.

#4017

#4018

#4020

Unterstützung für GROUPING SETS, CUBE, ROLLUP

Alle drei, GROUPING SETS, CUBE und ROLLUP, sind über den func-Namespace verfügbar. Bei CUBE und ROLLUP funktionieren diese Funktionen bereits in früheren Versionen, für GROUPING SETS wird jedoch ein Platzhalter im Compiler hinzugefügt, um den Platz zu ermöglichen. Alle drei Funktionen sind nun in der Dokumentation benannt.

>>> from sqlalchemy import select, table, column, func, tuple_
>>> t = table("t", column("value"), column("x"), column("y"), column("z"), column("q"))
>>> stmt = select([func.sum(t.c.value)]).group_by(
...     func.grouping_sets(
...         tuple_(t.c.x, t.c.y),
...         tuple_(t.c.z, t.c.q),
...     )
... )
>>> print(stmt)
SELECT sum(t.value) AS sum_1 FROM t GROUP BY GROUPING SETS((t.x, t.y), (t.z, t.q))

#3429

Parameter-Hilfe für Mehrfach-INSERT mit kontextuellem Standard-Generator

Eine Standardgenerierungsfunktion, z. B. die unter Kontextsensitive Standardfunktionen beschriebene, kann die aktuellen Parameter, die für die Anweisung relevant sind, über das Attribut DefaultExecutionContext.current_parameters abrufen. Im Falle eines Insert-Konstrukts, das mehrere VALUES-Klauseln über die Methode Insert.values() spezifiziert, wird die benutzerdefinierte Funktion mehrmals aufgerufen, einmal für jeden Parametersatz. Es gab jedoch keine Möglichkeit zu wissen, welcher Teil der Schlüssel in DefaultExecutionContext.current_parameters für diese Spalte gilt. Eine neue Funktion DefaultExecutionContext.get_current_parameters() wird hinzugefügt, die ein Schlüsselwortargument DefaultExecutionContext.get_current_parameters.isolate_multiinsert_groups mit dem Standardwert True enthält. Diese führt die zusätzliche Arbeit durch, ein Unterverzeichnis von DefaultExecutionContext.current_parameters zu liefern, das die Namen auf die aktuell verarbeitete VALUES-Klausel lokalisiert.

def mydefault(context):
    return context.get_current_parameters()["counter"] + 12


mytable = Table(
    "mytable",
    metadata_obj,
    Column("counter", Integer),
    Column("counter_plus_twelve", Integer, default=mydefault, onupdate=mydefault),
)

stmt = mytable.insert().values([{"counter": 5}, {"counter": 18}, {"counter": 20}])

conn.execute(stmt)

#4075

Wichtige Verhaltensänderungen – ORM

Das after_rollback()-Session-Event wird nun vor dem Ablaufen von Objekten ausgelöst

Das SessionEvents.after_rollback()-Event hat nun Zugriff auf den Attributzustand von Objekten, bevor deren Zustand abgelaufen ist (z. B. die „Snapshot-Entfernung“). Dies ermöglicht es dem Event, mit dem Verhalten des SessionEvents.after_commit()-Events konsistent zu sein, das ebenfalls vor der Entfernung des „Snapshots“ ausgelöst wird.

sess = Session()

user = sess.query(User).filter_by(name="x").first()


@event.listens_for(sess, "after_rollback")
def after_rollback(session):
    # 'user.name' is now present, assuming it was already
    # loaded.  previously this would raise upon trying
    # to emit a lazy load.
    print("user name: %s" % user.name)


@event.listens_for(sess, "after_commit")
def after_commit(session):
    # 'user.name' is present, assuming it was already
    # loaded.  this is the existing behavior.
    print("user name: %s" % user.name)


if should_rollback:
    sess.rollback()
else:
    sess.commit()

Beachten Sie, dass die Session innerhalb dieses Events immer noch keine SQL-Ausgabe zulässt; das bedeutet, dass nicht geladene Attribute immer noch nicht geladen werden können, wenn das Event ausgeführt wird.

#3934

Problem mit Single-Table-Inheritance mit select_from() behoben

Die Methode Query.select_from() berücksichtigt nun den Diskriminatorspalten für Single-Table-Inheritance bei der SQL-Generierung; zuvor wurden nur die Ausdrücke in der Spaltenliste der Abfrage berücksichtigt.

Angenommen, Manager ist eine Unterklasse von Employee. Eine Abfrage wie die folgende

sess.query(Manager.id)

würde SQL wie folgt generieren:

SELECT employee.id FROM employee WHERE employee.type IN ('manager')

Wenn jedoch Manager nur durch Query.select_from() und nicht in der Spaltenliste angegeben würde, würde der Diskriminator nicht hinzugefügt werden.

sess.query(func.count(1)).select_from(Manager)

würde erzeugen:

SELECT count(1) FROM employee

Mit der Korrektur funktioniert Query.select_from() nun korrekt und wir erhalten

SELECT count(1) FROM employee WHERE employee.type IN ('manager')

Anwendungen, die dies möglicherweise durch manuelle Angabe der WHERE-Klausel umgangen haben, müssen möglicherweise angepasst werden.

#3891

Die vorherige Sammlung wird bei Ersetzung nicht mehr mutiert

Das ORM löst Events aus, wann immer sich die Elemente einer zugeordneten Sammlung ändern. Im Falle der Zuweisung einer Sammlung zu einem Attribut, das die vorherige Sammlung ersetzen würde, war eine Nebeneffekt, dass die ersetzte Sammlung ebenfalls mutiert wurde, was irreführend und unnötig ist.

>>> a1, a2, a3 = Address("a1"), Address("a2"), Address("a3")
>>> user.addresses = [a1, a2]

>>> previous_collection = user.addresses

# replace the collection with a new one
>>> user.addresses = [a2, a3]

>>> previous_collection
[Address('a1'), Address('a2')]

Wie oben erwähnt, würde vor der Änderung die previous_collection das Mitglied „a1“ entfernt haben, entsprechend dem Mitglied, das nicht mehr in der neuen Sammlung enthalten ist.

#3913

Eine @validates-Methode empfängt alle Werte bei der Massen-Sammlungseinstellung vor dem Vergleich

Eine Methode, die @validates verwendet, empfängt nun alle Mitglieder einer Sammlung während einer „Massen-Einstellungs“-Operation, bevor der Vergleich mit der vorhandenen Sammlung angewendet wird.

Gegeben eine Zuordnung als

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

    @validates("bs")
    def convert_dict_to_b(self, key, value):
        return B(data=value["data"])


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

Oben könnten wir den Validator wie folgt verwenden, um von einem eingehenden Wörterbuch in eine Instanz von B bei der Sammlungsanhäufung zu konvertieren:

a1 = A()
a1.bs.append({"data": "b1"})

Eine Sammlungszuweisung würde jedoch fehlschlagen, da das ORM davon ausgeht, dass eingehende Objekte bereits Instanzen von B sind, während es versucht, sie mit den vorhandenen Mitgliedern der Sammlung zu vergleichen, bevor Sammlungsanhäufungen erfolgen, die tatsächlich den Validator aufrufen. Dies würde es Massen-Einstellungsoperationen unmöglich machen, nicht-ORM-Objekte wie Wörterbücher zu unterstützen, die eine Vorkorrektur benötigen.

a1 = A()
a1.bs = [{"data": "b1"}]

Die neue Logik verwendet das neue AttributeEvents.bulk_replace()-Event, um sicherzustellen, dass alle Werte vorab an die @validates-Funktion gesendet werden.

Als Teil dieser Änderung bedeutet dies, dass Validatoren nun **alle** Mitglieder einer Sammlung bei der Massen-Einstellung erhalten, nicht nur die neuen Mitglieder. Angenommen ein einfacher Validator wie

class A(Base):
    # ...

    @validates("bs")
    def validate_b(self, key, value):
        assert value.data is not None
        return value

Oben, wenn wir mit einer Sammlung beginnen als

a1 = A()

b1, b2 = B(data="one"), B(data="two")
a1.bs = [b1, b2]

Und dann die Sammlung durch eine ersetzen, die sich mit der ersten überschneidet:

b3 = B(data="three")
a1.bs = [b2, b3]

Zuvor hätte die zweite Zuweisung die Methode A.validate_b nur einmal für das Objekt b3 ausgelöst. Das Objekt b2 wäre als bereits in der Sammlung vorhanden angesehen und nicht validiert worden. Mit dem neuen Verhalten werden sowohl b2 als auch b3 vor der Weitergabe an die Sammlung an A.validate_b übergeben. Es ist daher wichtig, dass Validierungsmethoden idempotentes Verhalten aufweisen, um einen solchen Fall zu berücksichtigen.

#3896

Verwenden Sie flag_dirty(), um ein Objekt als „dirty“ zu markieren, ohne dass sich Attribute ändern

Es wird nun eine Ausnahme ausgelöst, wenn die Funktion flag_modified() verwendet wird, um ein Attribut als geändert zu markieren, das nicht tatsächlich geladen ist.

a1 = A(data="adf")
s.add(a1)

s.flush()

# expire, similarly as though we said s.commit()
s.expire(a1, "data")

# will raise InvalidRequestError
attributes.flag_modified(a1, "data")

Dies liegt daran, dass der Flush-Prozess wahrscheinlich sowieso fehlschlägt, wenn das Attribut zum Zeitpunkt des Flushs noch nicht vorhanden ist. Um ein Objekt als „geändert“ zu markieren, ohne sich auf ein bestimmtes Attribut zu beziehen, damit es für benutzerdefinierte Event-Handler wie SessionEvents.before_flush() im Flush-Prozess berücksichtigt wird, verwenden Sie die neue Funktion flag_dirty().

from sqlalchemy.orm import attributes

attributes.flag_dirty(a1)

#3753

Das Schlüsselwort „scope“ wurde aus scoped_session entfernt

Ein sehr altes und undokumentiertes Schlüsselwortargument scope wurde entfernt.

from sqlalchemy.orm import scoped_session

Session = scoped_session(sessionmaker())

session = Session(scope=None)

Der Zweck dieses Schlüsselworts war der Versuch, variable „Scopes“ zu ermöglichen, wobei None für „keine Scope“ stand und somit eine neue Session zurückgab. Das Schlüsselwort wurde nie dokumentiert und löst nun TypeError aus, wenn es angetroffen wird. Es wird nicht erwartet, dass dieses Schlüsselwort verwendet wird, aber wenn Benutzer während der Beta-Tests Probleme damit melden, kann es mit einer Deprecation wiederhergestellt werden.

#3796

Verbesserungen an post_update in Verbindung mit onupdate

Eine Beziehung, die die relationship.post_update-Funktion verwendet, interagiert nun besser mit einer Spalte, die einen Column.onupdate-Wert gesetzt hat. Wenn ein Objekt mit einem expliziten Wert für die Spalte eingefügt wird, wird dieser während des UPDATEs erneut angegeben, damit die „onupdate“-Regel ihn nicht überschreibt.

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    favorite_b_id = Column(ForeignKey("b.id", name="favorite_b_fk"))
    bs = relationship("B", primaryjoin="A.id == B.a_id")
    favorite_b = relationship(
        "B", primaryjoin="A.favorite_b_id == B.id", post_update=True
    )
    updated = Column(Integer, onupdate=my_onupdate_function)


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


a1 = A()
b1 = B()

a1.bs.append(b1)
a1.favorite_b = b1
a1.updated = 5
s.add(a1)
s.flush()

Oben wäre das frühere Verhalten, dass ein UPDATE nach dem INSERT gesendet wird, wodurch „onupdate“ ausgelöst und der Wert „5“ überschrieben wird. Die SQL sieht nun wie folgt aus:

INSERT INTO a (favorite_b_id, updated) VALUES (?, ?)
(None, 5)
INSERT INTO b (a_id) VALUES (?)
(1,)
UPDATE a SET favorite_b_id=?, updated=? WHERE a.id = ?
(1, 5, 1)

Zusätzlich, wenn der Wert von „updated“ *nicht* gesetzt ist, erhalten wir korrekt den neu generierten Wert für a1.updated zurück; zuvor würde die Logik, die das Attribut aktualisiert oder abläuft, um den generierten Wert präsent zu machen, für ein post-update nicht ausgelöst. Das InstanceEvents.refresh_flush()-Event wird ebenfalls ausgelöst, wenn in diesem Fall eine Aktualisierung innerhalb des Flushs erfolgt.

#3471

#3472

post_update integriert sich mit ORM-Versioning

Das unter Zeilen, die auf sich selbst zeigen / Gegenseitig abhängige Zeilen dokumentierte post_update-Feature beinhaltet, dass zusätzlich zu den normalerweise für die Zielzeile ausgegebenen INSERT/UPDATE/DELETE eine UPDATE-Anweisung als Reaktion auf Änderungen an einem bestimmten beziehungsgebundenen Fremdschlüssel ausgegeben wird. Diese UPDATE-Anweisung nimmt nun am Versionierungs-Feature teil, das unter Konfigurieren eines Versionszählers dokumentiert ist.

Gegeben eine Zuordnung

class Node(Base):
    __tablename__ = "node"
    id = Column(Integer, primary_key=True)
    version_id = Column(Integer, default=0)
    parent_id = Column(ForeignKey("node.id"))
    favorite_node_id = Column(ForeignKey("node.id"))

    nodes = relationship("Node", primaryjoin=remote(parent_id) == id)
    favorite_node = relationship(
        "Node", primaryjoin=favorite_node_id == remote(id), post_update=True
    )

    __mapper_args__ = {"version_id_col": version_id}

Ein UPDATE eines Knotens, der einen anderen Knoten als „Favorit“ zuordnet, erhöht nun den Versionszähler und stimmt mit der aktuellen Version überein.

node = Node()
session.add(node)
session.commit()  # node is now version #1

node = session.query(Node).get(node.id)
node.favorite_node = Node()
session.commit()  # node is now version #2

Beachten Sie, dass dies bedeutet, dass ein Objekt, das aufgrund anderer Attribute ein UPDATE erhält, und ein zweites UPDATE aufgrund einer post_update-Beziehungsänderung, nun **zwei Versionszähleraktualisierungen für einen Flush** erhält. Wenn das Objekt jedoch von einem INSERT im aktuellen Flush betroffen ist, wird der Versionszähler **nicht** zusätzlich erhöht, es sei denn, es ist ein serverseitiges Versionierungsschema vorhanden.

Der Grund, warum post_update ein UPDATE auslöst, selbst wenn ein UPDATE vorliegt, wird nun unter Warum löst post_update zusätzlich zum ersten UPDATE ein UPDATE aus? besprochen.

#3496

Wichtige Verhaltensänderungen – Core

Das Tippverhalten benutzerdefinierter Operatoren wurde konsistent gestaltet

Benutzerdefinierte Operatoren können auf die Schnelle mit der Funktion Operators.op() erstellt werden. Zuvor war das Tippverhalten eines Ausdrucks gegen einen solchen Operator inkonsistent und auch nicht steuerbar.

Während in 1.1 ein Ausdruck wie der folgende ein Ergebnis ohne Rückgabetyp ergab (angenommen, -%> ist ein spezieller Operator, der von der Datenbank unterstützt wird):

>>> column("x", types.DateTime).op("-%>")(None).type
NullType()

Andere Typen nutzten das Standardverhalten, den linken Typ als Rückgabetyp zu verwenden.

>>> column("x", types.String(50)).op("-%>")(None).type
String(length=50)

Diese Verhaltensweisen waren meist zufällig, daher wurde das Verhalten konsistent auf die zweite Form gebracht, d. h. der Standard-Rückgabetyp ist derselbe wie der linke Ausdruck.

>>> column("x", types.DateTime).op("-%>")(None).type
DateTime()

Da die meisten benutzerdefinierten Operatoren „Vergleichsoperatoren“ sind, oft einer der vielen speziellen Operatoren, die von PostgreSQL definiert werden, wurde das Flag Operators.op.is_comparison repariert, um seinem dokumentierten Verhalten zu folgen, das es ermöglicht, den Rückgabetyp in allen Fällen auf Boolean zu setzen, einschließlich für ARRAY und JSON.

>>> column("x", types.String(50)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.ARRAY(types.Integer)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.JSON()).op("-%>", is_comparison=True)(None).type
Boolean()

Zur Unterstützung von booleschen Vergleichsoperatoren wurde eine neue Kurzmethode Operators.bool_op() hinzugefügt. Diese Methode sollte für Ad-hoc-Boolesche Operatoren bevorzugt werden.

>>> print(column("x", types.Integer).bool_op("-%>")(5))
x -%> :x_1

Prozentzeichen in literal_column() werden nun bedingt maskiert

Das literal_column-Konstrukt maskiert nun Prozentzeichen bedingt, abhängig davon, ob der verwendete DBAPI einen Prozentzeichen-sensitiven Paramstyle verwendet oder nicht (z. B. „format“ oder „pyformat“).

Zuvor war es nicht möglich, ein literal_column-Konstrukt zu erzeugen, das ein einzelnes Prozentzeichen angibt:

>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%%symbol

Das Prozentzeichen bleibt nun für Dialekte unberührt, die nicht auf die Paramstyles „format“ oder „pyformat“ eingestellt sind; Dialekte wie die meisten MySQL-Dialekte, die einen dieser Paramstyles angeben, maskieren weiterhin wie erwartet.

>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%symbol
>>> from sqlalchemy.dialects import mysql >>> print(literal_column("some%symbol").compile(dialect=mysql.dialect()))
some%%symbol

Als Teil dieser Änderung wird die Verdopplung, die bei der Verwendung von Operatoren wie ColumnOperators.contains(), ColumnOperators.startswith() und ColumnOperators.endswith() vorhanden war, ebenfalls verfeinert, sodass sie nur dann auftritt, wenn dies angemessen ist.

#3740

Das COLLATE-Schlüsselwort auf Spaltenebene umschließt nun den Namen der Kollation

Ein Fehler in den Funktionen collate() und ColumnOperators.collate(), die verwendet wurden, um ad-hoc Spaltenkollationen auf Anweisungsebene zu liefern, wurde behoben, wobei ein groß-/kleinschreibungsempfindlicher Name nicht umschlossen wurde.

stmt = select([mytable.c.x, mytable.c.y]).order_by(
    mytable.c.somecolumn.collate("fr_FR")
)

wird nun gerendert

SELECT mytable.x, mytable.y,
FROM mytable ORDER BY mytable.somecolumn COLLATE "fr_FR"

Zuvor wurde der groß-/kleinschreibungsempfindliche Name „fr_FR“ nicht umschlossen. Derzeit wird die manuelle Umschließung des Namens „fr_FR“ **nicht** erkannt, sodass Anwendungen, die den Bezeichner manuell umschließen, angepasst werden sollten. Beachten Sie, dass diese Änderung die Verwendung von Kollationen auf Typenebene (z. B. angegeben für den Datentyp wie String auf Tabellenebene) nicht beeinträchtigt, wo bereits eine Umschließung angewendet wird.

#3785

Dialektverbesserungen und Änderungen - PostgreSQL

Unterstützung für Batch-Modus / Fast Execution Helpers

Die Methode cursor.executemany() von psycopg2 wurde als leistungsschwach identifiziert, insbesondere bei INSERT-Anweisungen. Um dies zu mildern, hat psycopg2 Fast Execution Helpers hinzugefügt, die Anweisungen in weniger Server-Roundtrips umwandeln, indem mehrere DML-Anweisungen in einem Batch gesendet werden. SQLAlchemy 1.2 enthält nun Unterstützung für diese Helfer, die transparent verwendet werden können, wann immer die Engine cursor.executemany() verwendet, um eine Anweisung gegen mehrere Parametersätze aufzurufen. Die Funktion ist standardmäßig deaktiviert und kann über das Argument use_batch_mode bei create_engine() aktiviert werden.

engine = create_engine(
    "postgresql+psycopg2://scott:tiger@host/dbname", use_batch_mode=True
)

Die Funktion wird derzeit als experimentell betrachtet, könnte aber in einer zukünftigen Version standardmäßig aktiviert werden.

#4109

Unterstützung für Feldangaben in INTERVAL, einschließlich vollständiger Reflexion

Der „fields“-Spezifizierer im INTERVAL-Datentyp von PostgreSQL erlaubt die Angabe, welche Felder des Intervalls gespeichert werden sollen, einschließlich solcher Werte wie „YEAR“, „MONTH“, „YEAR TO MONTH“ usw. Der Datentyp INTERVAL erlaubt nun die Angabe dieser Werte.

from sqlalchemy.dialects.postgresql import INTERVAL

Table("my_table", metadata, Column("some_interval", INTERVAL(fields="DAY TO SECOND")))

Zusätzlich können nun alle INTERVAL-Datentypen unabhängig vom vorhandenen „fields“-Spezifizierer reflektiert werden; der „fields“-Parameter im Datentyp selbst wird ebenfalls vorhanden sein.

>>> inspect(engine).get_columns("my_table")
[{'comment': None,
  'name': u'some_interval', 'nullable': True,
  'default': None, 'autoincrement': False,
  'type': INTERVAL(fields=u'day to second')}]

#3959

Dialektverbesserungen und Änderungen - MySQL

Unterstützung für INSERT..ON DUPLICATE KEY UPDATE

Die Klausel ON DUPLICATE KEY UPDATE von INSERT, die von MySQL unterstützt wird, wird nun mit einer MySQL-spezifischen Version des Insert-Objekts über sqlalchemy.dialects.mysql.dml.insert() unterstützt. Diese Insert-Unterklasse fügt eine neue Methode Insert.on_duplicate_key_update() hinzu, die die MySQL-Syntax implementiert.

from sqlalchemy.dialects.mysql import insert

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

on_conflict_stmt = insert_stmt.on_duplicate_key_update(
    data=insert_stmt.inserted.data, status="U"
)

conn.execute(on_conflict_stmt)

Das Obige wird gerendert

INSERT INTO my_table (id, data)
VALUES (:id, :data)
ON DUPLICATE KEY UPDATE data=VALUES(data), status=:status_1

#4009

Dialektverbesserungen und Änderungen - Oracle

Umfangreiche Überarbeitung des cx_Oracle-Dialekts, Typsystem

Mit der Einführung der cx_Oracle DBAPI-Serie 6.x wurde der cx_Oracle-Dialekt von SQLAlchemy überarbeitet und vereinfacht, um die jüngsten Verbesserungen in cx_Oracle zu nutzen und Muster zu entfernen, die für die cx_Oracle-Serie vor 5.x relevanter waren.

  • Die minimale unterstützte cx_Oracle-Version ist nun 5.1.3; 5.3 oder die neueste 6.x-Serie werden empfohlen.

  • Die Handhabung von Datentypen wurde überarbeitet. Die Methode cursor.setinputsizes() wird nicht mehr für Datentypen außer LOB-Typen verwendet, gemäß dem Rat der Entwickler von cx_Oracle. Infolgedessen sind die Parameter auto_setinputsizes und exclude_setinputsizes veraltet und haben keine Auswirkungen mehr.

  • Das Flag coerce_to_decimal, wenn auf False gesetzt, um anzuzeigen, dass die Umwandlung von numerischen Typen mit Präzision und Skala in Decimal nicht erfolgen soll, wirkt sich nur auf nicht typisierte (z. B. reine Zeichenfolgen ohne TypeEngine-Objekte) Anweisungen aus. Ein Core-Ausdruck, der einen Numeric-Typ oder -Subtyp enthält, folgt nun den Dezimalumwandlungsregeln dieses Typs.

  • Die „Two-Phase“-Transaktionsunterstützung im Dialekt, die für die cx_Oracle-Serie 6.x bereits eingestellt wurde, wurde nun vollständig entfernt, da diese Funktion nie korrekt funktioniert hat und unwahrscheinlich ist, dass sie in der Produktion verwendet wurde. Infolgedessen ist das Dialektflag allow_twophase veraltet und hat ebenfalls keine Auswirkungen.

  • Behebung eines Fehlers im Zusammenhang mit den Spalten-Keys bei RETURNING. Gegeben sei eine Anweisung wie folgt

    result = conn.execute(table.insert().values(x=5).returning(table.c.a, table.c.b))

    Zuvor waren die Keys in jeder Ergebniszeile ret_0 und ret_1, was interne Bezeichner der cx_Oracle RETURNING-Implementierung sind. Die Keys sind nun a und b, wie es für andere Dialekte erwartet wird.

  • Der LOB-Datentyp von cx_Oracle gibt Rückgabewerte als cx_Oracle.LOB-Objekt zurück, ein Cursor-assoziierter Proxy, der den endgültigen Datenwert über eine .read()-Methode zurückgibt. Historisch gesehen, wenn mehr Zeilen gelesen wurden, bevor diese LOB-Objekte verbraucht wurden (insbesondere mehr Zeilen als der Wert von cursor.arraysize, was das Lesen eines neuen Batches von Zeilen verursacht), gaben diese LOB-Objekte den Fehler „LOB variable no longer valid after subsequent fetch“ aus. SQLAlchemy umging dies, indem es sowohl automatisch .read() für diese LOBs innerhalb seines Typsystems aufrief als auch ein spezielles BufferedColumnResultSet verwendete, das sicherstellte, dass diese Daten gepuffert wurden, falls ein Aufruf wie cursor.fetchmany() oder cursor.fetchall() verwendet wurde.

    Der Dialekt verwendet nun einen cx_Oracle outputtypehandler, um diese .read()-Aufrufe zu verarbeiten, sodass diese immer im Voraus aufgerufen werden, unabhängig davon, wie viele Zeilen abgerufen werden, sodass dieser Fehler nicht mehr auftreten kann. Infolgedessen wurde die Verwendung von BufferedColumnResultSet sowie einige andere interne Bestandteile von Core ResultSet, die für diesen Anwendungsfall spezifisch waren, entfernt. Die Typobjekte sind ebenfalls vereinfacht, da sie kein binäres Spaltenergebnis mehr verarbeiten müssen.

    Zusätzlich hat cx_Oracle 6.x die Bedingungen, unter denen dieser Fehler auftritt, in jedem Fall entfernt, sodass der Fehler nicht mehr möglich ist. Der Fehler kann in SQLAlchemy auftreten, wenn die selten (wenn überhaupt) verwendete Option auto_convert_lobs=False in Verbindung mit der vorherigen 5.x-Serie von cx_Oracle verwendet wird und mehr Zeilen gelesen werden, bevor die LOB-Objekte verarbeitet werden können. Ein Upgrade auf cx_Oracle 6.x wird dieses Problem beheben.

Oracle UNIQUE-, CHECK-Constraints werden nun reflektiert

UNIQUE- und CHECK-Constraints werden nun über Inspector.get_unique_constraints() und Inspector.get_check_constraints() reflektiert. Ein Table-Objekt, das reflektiert wird, enthält nun auch CheckConstraint-Objekte. Siehe die Hinweise unter Constraint Reflection für Informationen über Verhaltensweisen hier, einschließlich der Tatsache, dass die meisten Table-Objekte weiterhin keine UniqueConstraint-Objekte enthalten, da diese normalerweise über Index dargestellt werden.

#4003

Oracle Fremdschlüssel-Constraint-Namen sind nun „namensnormalisiert“

Die Namen von Fremdschlüssel-Constraints, die an ein ForeignKeyConstraint-Objekt während der Tabellenreflexion sowie innerhalb der Methode Inspector.get_foreign_keys() übergeben werden, werden nun „namensnormalisiert“, d. h. in Kleinbuchstaben für einen groß-/kleinschreibungsunempfindlichen Namen ausgedrückt, anstatt im rohen UPPERCASE-Format, das Oracle verwendet.

>>> insp.get_indexes("addresses")
[{'unique': False, 'column_names': [u'user_id'],
  'name': u'address_idx', 'dialect_options': {}}]

>>> insp.get_pk_constraint("addresses")
{'name': u'pk_cons', 'constrained_columns': [u'id']}

>>> insp.get_foreign_keys("addresses")
[{'referred_table': u'users', 'referred_columns': [u'id'],
  'referred_schema': None, 'name': u'user_id_fk',
  'constrained_columns': [u'user_id']}]

Zuvor sah das Ergebnis der Fremdschlüssel wie folgt aus

[
    {
        "referred_table": "users",
        "referred_columns": ["id"],
        "referred_schema": None,
        "name": "USER_ID_FK",
        "constrained_columns": ["user_id"],
    }
]

Was insbesondere bei der automatischen Generierung von Alembic Probleme verursachen könnte.

#3276

Dialektverbesserungen und Änderungen - SQL Server

SQL Server-Schemanamen mit eingebetteten Punkten werden unterstützt

Der SQL Server-Dialekt hat ein Verhalten, bei dem ein Schemaname mit einem Punkt darin als „database“.„owner“-Identifier-Paar angenommen wird, das bei der Tabellen- und Komponentenreflexion sowie beim Rendern der Umschließung für den Schemanamen notwendigerweise in diese getrennten Komponenten aufgeteilt wird, sodass die beiden Symbole getrennt umschlossen werden. Das `schema`-Argument kann nun mithilfe von Klammern übergeben werden, um manuell anzugeben, wo diese Aufteilung erfolgt, was Datenbank- und/oder Eignernamen ermöglicht, die selbst einen oder mehrere Punkte enthalten.

Table("some_table", metadata, Column("q", String(50)), schema="[MyDataBase.dbo]")

Die obige Tabelle betrachtet den „owner“ als MyDataBase.dbo, der auch bei der Wiedergabe umschlossen wird, und die „database“ als None. Um individuell auf Datenbankname und Besitzer zu verweisen, verwenden Sie zwei Klammerpaare.

Table(
    "some_table",
    metadata,
    Column("q", String(50)),
    schema="[MyDataBase.SomeDB].[MyDB.owner]",
)

Zusätzlich wird die quoted_name-Konstruktion nun vom SQL Server-Dialekt honoriert, wenn sie an „schema“ übergeben wird; das gegebene Symbol wird nicht am Punkt geteilt, wenn das Quote-Flag True ist, und als „owner“ interpretiert.

#2626

Unterstützung für den AUTOCOMMIT-Isolationsgrad

Sowohl die PyODBC- als auch die pymssql-Dialekte unterstützen nun den „AUTOCOMMIT“-Isolationsgrad, wie er von Connection.execution_options() festgelegt wird, was die korrekten Flags auf dem DBAPI-Verbindungsobjekt setzt.