Was gibt es Neues in SQLAlchemy 0.9?

Über dieses Dokument

Dieses Dokument beschreibt die Änderungen zwischen SQLAlchemy Version 0.8, die im Mai 2013 Wartungs-Releases erhielt, und SQLAlchemy Version 0.9, deren erstes Produktionsrelease am 30. Dezember 2013 stattfand.

Dokument zuletzt aktualisiert: 10. Juni 2015

Einleitung

Diese Anleitung stellt vor, was es Neues in SQLAlchemy Version 0.9 gibt, und dokumentiert auch Änderungen, die Benutzer betrafen, die ihre Anwendungen von der 0.8er-Serie von SQLAlchemy auf 0.9 migrieren.

Bitte überprüfen Sie sorgfältig Verhaltensänderungen - ORM und Verhaltensänderungen - Core auf potenziell abwärtskompatible Änderungen.

Plattformunterstützung

Ziel Python 2.6 und höher, Python 3 ohne 2to3

Die erste Errungenschaft des 0.9-Releases ist die Entfernung der Abhängigkeit vom 2to3-Tool für die Python 3-Kompatibilität. Um dies einfacher zu gestalten, ist die niedrigste Ziel-Python-Version jetzt 2.6, die eine breite Cross-Kompatibilität mit Python 3 aufweist. Alle SQLAlchemy-Module und Unit-Tests werden nun gleichermaßen gut von jedem Python-Interpreter ab 2.6 an interpretiert, einschließlich der Interpreter 3.1 und 3.2.

#2671

C-Erweiterungen unterstützt unter Python 3

Die C-Erweiterungen wurden für die Unterstützung von Python 3 portiert und werden nun in Python 2- und Python 3-Umgebungen erstellt.

#2161

Verhaltensänderungen - ORM

Zusammengesetzte Attribute werden jetzt in ihrer Objektform zurückgegeben, wenn sie pro Attribut abgefragt werden

Die Verwendung einer Query in Verbindung mit einem zusammengesetzten Attribut gibt nun den von diesem Verbund aufrechterhaltenen Objekttyp zurück, anstatt ihn in einzelne Spalten aufzuteilen. Verwendung des Mappings unter Composite Column Types

>>> session.query(Vertex.start, Vertex.end).filter(Vertex.start == Point(3, 4)).all()
[(Point(x=3, y=4), Point(x=5, y=6))]

Diese Änderung ist abwärtsinkompatibel mit Code, der erwartet, dass das einzelne Attribut in einzelne Spalten expandiert wird. Um dieses Verhalten zu erhalten, verwenden Sie den Accessor .clauses

>>> session.query(Vertex.start.clauses, Vertex.end.clauses).filter(
...     Vertex.start == Point(3, 4)
... ).all()
[(3, 4, 5, 6)]

#2824

Query.select_from() wendet die Klausel nicht mehr auf entsprechende Entitäten an

Die Methode Query.select_from() wurde in neueren Versionen popularisiert, um zu steuern, von was eine Query-Objekt zuerst „auswählt“, typischerweise um zu steuern, wie ein JOIN gerendert wird.

Betrachten Sie das folgende Beispiel gegen das übliche User-Mapping

select_stmt = select([User]).where(User.id == 7).alias()

q = (
    session.query(User)
    .join(select_stmt, User.id == select_stmt.c.id)
    .filter(User.name == "ed")
)

Die obige Anweisung rendert vorhersagbar SQL wie folgt:

SELECT "user".id AS user_id, "user".name AS user_name
FROM "user" JOIN (SELECT "user".id AS id, "user".name AS name
FROM "user"
WHERE "user".id = :id_1) AS anon_1 ON "user".id = anon_1.id
WHERE "user".name = :name_1

Wenn wir die Reihenfolge der linken und rechten Elemente des JOIN umkehren wollten, würde uns die Dokumentation glauben machen, dass wir Query.select_from() verwenden können, um dies zu tun

q = (
    session.query(User)
    .select_from(select_stmt)
    .join(User, User.id == select_stmt.c.id)
    .filter(User.name == "ed")
)

Allerdings würde die obige Verwendung von Query.select_from() in Version 0.8 und früher die select_stmt anwenden, um die User-Entität zu **ersetzen**, da sie von der user-Tabelle auswählt, die mit User kompatibel ist.

-- SQLAlchemy 0.8 and earlier...
SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name
FROM (SELECT "user".id AS id, "user".name AS name
FROM "user"
WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON anon_1.id = anon_1.id
WHERE anon_1.name = :name_1

Die obige Anweisung ist ein Chaos, die ON-Klausel bezieht sich auf anon_1.id = anon_1.id, unsere WHERE-Klausel wurde durch anon_1 ersetzt.

Dieses Verhalten ist durchaus beabsichtigt, hat aber einen anderen Anwendungsfall als der, der für Query.select_from() populär geworden ist. Das obige Verhalten ist nun durch eine neue Methode namens Query.select_entity_from() verfügbar. Dies ist ein weniger genutztes Verhalten, das im modernen SQLAlchemy ungefähr einem Auswahl aus einem angepassten aliased()-Konstrukt entspricht.

select_stmt = select([User]).where(User.id == 7)
user_from_stmt = aliased(User, select_stmt.alias())

q = session.query(user_from_stmt).filter(user_from_stmt.name == "ed")

Mit SQLAlchemy 0.9 produziert unsere Abfrage, die von select_stmt auswählt, die erwartete SQL-Ausgabe.

-- SQLAlchemy 0.9
SELECT "user".id AS user_id, "user".name AS user_name
FROM (SELECT "user".id AS id, "user".name AS name
FROM "user"
WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON "user".id = id
WHERE "user".name = :name_1

Die Methode Query.select_entity_from() wird in SQLAlchemy **0.8.2** verfügbar sein. Anwendungen, die sich auf das alte Verhalten verlassen, können also zuerst zu dieser Methode wechseln, sicherstellen, dass alle Tests weiterhin funktionieren, und dann problemlos auf 0.9 aktualisieren.

#2736

viewonly=True bei relationship() verhindert, dass sich die Historie auswirkt

Das Flag viewonly bei relationship() wird angewendet, um zu verhindern, dass Änderungen am Zielattribut während des Flush-Prozesses Auswirkungen haben. Dies geschieht, indem das Attribut bei der Flush nicht berücksichtigt wird. Bis jetzt registrierten Änderungen am Attribut das Elternobjekt jedoch immer noch als „dirty“ und lösten einen potenziellen Flush aus. Die Änderung besteht darin, dass das Flag viewonly nun auch verhindert, dass eine Historie für das Zielattribut gesetzt wird. Attributereignisse wie Backrefs und benutzerspezifische Ereignisse funktionieren weiterhin normal.

Die Änderung wird wie folgt veranschaulicht:

from sqlalchemy import Column, Integer, ForeignKey, create_engine
from sqlalchemy.orm import backref, relationship, Session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import inspect

Base = declarative_base()


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


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))
    a = relationship("A", backref=backref("bs", viewonly=True))


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

a = A()
b = B()

sess = Session(e)
sess.add_all([a, b])
sess.commit()

b.a = a

assert b in sess.dirty

# before 0.9.0
# assert a in sess.dirty
# assert inspect(a).attrs.bs.history.has_changes()

# after 0.9.0
assert a not in sess.dirty
assert not inspect(a).attrs.bs.history.has_changes()

#2833

Verbesserungen und Korrekturen an SQL-Ausdrücken für Association Proxies

Die Operatoren == und !=, wie sie von einem Association Proxy implementiert werden, der sich auf einen Skalarwert in einer Skalarbeziehung bezieht, erzeugen nun einen vollständigeren SQL-Ausdruck, der dazu bestimmt ist, die An- oder Abwesenheit der „Assoziationszeile“ zu berücksichtigen, wenn der Vergleich mit None erfolgt.

Betrachten Sie dieses Mapping:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    b_id = Column(Integer, ForeignKey("b.id"), primary_key=True)
    b = relationship("B")
    b_value = association_proxy("b", "value")


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

Bis 0.8 würde eine Abfrage wie die folgende:

s.query(A).filter(A.b_value == None).all()

würde ergeben

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NULL)

In 0.9 erzeugt sie nun:

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE (EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NULL)) OR a.b_id IS NULL

Der Unterschied besteht darin, dass sie nicht nur b.value überprüft, sondern auch, ob a überhaupt keine b-Zeile referenziert. Dies liefert andere Ergebnisse als in früheren Versionen für ein System, das diese Art von Vergleich verwendet, bei der einige Elternzeilen keine Assoziationszeile haben.

Wichtiger ist, dass für A.b_value != None ein korrekter Ausdruck erzeugt wird. In 0.8 würde dies für A-Zeilen, die kein b hatten, True zurückgeben.

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE NOT (EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NULL))

Jetzt in 0.9 wurde die Prüfung überarbeitet, um sicherzustellen, dass die A.b_id-Zeile vorhanden ist und zusätzlich B.value nicht NULL ist.

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NOT NULL)

Zusätzlich wird der Operator has() erweitert, so dass Sie ihn gegen einen Skalarspaltenwert ohne Kriterium aufrufen können und er Kriterien erzeugt, die die Anwesenheit der Assoziationszeile prüfen.

s.query(A).filter(A.b_value.has()).all()

Ausgabe

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id)

Dies entspricht A.b.has(), ermöglicht es Ihnen aber, direkt gegen b_value abzufragen.

#2751

Fehlender Skalarwert bei Association Proxy gibt None zurück

Ein Association Proxy von einem Skalarattribut zu einem Skalar gibt nun None zurück, wenn das proxy-Objekt nicht vorhanden ist. Dies entspricht der Tatsache, dass fehlende Many-to-Ones in SQLAlchemy None zurückgeben, und dies sollte auch für den proxy-Wert gelten. Z.B.

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    b = relationship("B", uselist=False)

    bname = association_proxy("b", "name")


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))
    name = Column(String)


a1 = A()

# this is how m2o's always have worked
assert a1.b is None

# but prior to 0.9, this would raise AttributeError,
# now returns None just like the proxied value.
assert a1.bname is None

#2810

attributes.get_history() fragt standardmäßig die DB ab, wenn der Wert nicht vorhanden ist

Ein Bugfix bezüglich get_history() ermöglicht es einem spaltenbasierten Attribut, eine nicht geladene Spalte aus der Datenbank abzurufen, vorausgesetzt, das Flag passive bleibt auf seinem Standardwert PASSIVE_OFF. Zuvor wurde dieses Flag nicht beachtet. Zusätzlich wird eine neue Methode AttributeState.load_history() hinzugefügt, um das Attribut AttributeState.history zu ergänzen, welche Loader-Aufrufe für ein nicht geladenes Attribut ausgibt.

Dies ist eine kleine Änderung, die wie folgt demonstriert wird:

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

Base = declarative_base()


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


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

sess = Session(e)

a1 = A(data="a1")
sess.add(a1)
sess.commit()  # a1 is now expired

# history doesn't emit loader callables
assert inspect(a1).attrs.data.history == (None, None, None)

# in 0.8, this would fail to load the unloaded state.
assert attributes.get_history(a1, "data") == (
    (),
    [
        "a1",
    ],
    (),
)

# load_history() is now equivalent to get_history() with
# passive=PASSIVE_OFF ^ INIT_OK
assert inspect(a1).attrs.data.load_history() == (
    (),
    [
        "a1",
    ],
    (),
)

#2787

Verhaltensänderungen - Core

Typobjekte akzeptieren keine ignorierten Schlüsselwortargumente mehr

Bis zur 0.8-Serie akzeptierten die meisten Typobjekte beliebige Schlüsselwortargumente, die stillschweigend ignoriert wurden.

from sqlalchemy import Date, Integer

# storage_format argument here has no effect on any backend;
# it needs to be on the SQLite-specific type
d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d")

# display_width argument here has no effect on any backend;
# it needs to be on the MySQL-specific type
i = Integer(display_width=5)

Dies war ein sehr alter Fehler, für den in der 0.8-Serie eine Deprecation-Warnung hinzugefügt wurde, aber da niemand Python mit dem Flag „-W“ ausführt, wurde er meist nie gesehen.

$ python -W always::DeprecationWarning ~/dev/sqlalchemy/test.py
/Users/classic/dev/sqlalchemy/test.py:5: SADeprecationWarning: Passing arguments to
type object constructor <class 'sqlalchemy.types.Date'> is deprecated
  d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d")
/Users/classic/dev/sqlalchemy/test.py:9: SADeprecationWarning: Passing arguments to
type object constructor <class 'sqlalchemy.types.Integer'> is deprecated
  i = Integer(display_width=5)

Ab der 0.9-Serie ist der „Catch-all“-Konstruktor von TypeEngine entfernt worden, und diese bedeutungslosen Argumente werden nicht mehr akzeptiert.

Der richtige Weg, dialektspezifische Argumente wie storage_format und display_width zu verwenden, ist die Verwendung der entsprechenden dialektspezifischen Typen.

from sqlalchemy.dialects.sqlite import DATE
from sqlalchemy.dialects.mysql import INTEGER

d = DATE(storage_format="%(day)02d.%(month)02d.%(year)04d")

i = INTEGER(display_width=5)

Was ist mit dem Fall, wo wir auch den dialektagnostischen Typ wollen? Wir verwenden die Methode TypeEngine.with_variant().

from sqlalchemy import Date, Integer
from sqlalchemy.dialects.sqlite import DATE
from sqlalchemy.dialects.mysql import INTEGER

d = Date().with_variant(
    DATE(storage_format="%(day)02d.%(month)02d.%(year)04d"), "sqlite"
)

i = Integer().with_variant(INTEGER(display_width=5), "mysql")

TypeEngine.with_variant() ist nicht neu, es wurde in SQLAlchemy 0.7.2 hinzugefügt. Code, der auf der 0.8-Serie läuft, kann also korrigiert werden, um diesen Ansatz zu verwenden und vor dem Upgrade auf 0.9 getestet zu werden.

None kann nicht mehr als „teilweiser AND“-Konstruktor verwendet werden

None kann nicht mehr als „Fallback“ verwendet werden, um eine AND-Bedingung stückweise zu bilden. Dieses Muster war nicht dokumentiert, obwohl einige SQLAlchemy-Interna es nutzten.

condition = None

for cond in conditions:
    condition = condition & cond

if condition is not None:
    stmt = stmt.where(condition)

Die obige Sequenz, wenn conditions nicht leer ist, erzeugt unter 0.9 SELECT .. WHERE <condition> AND NULL. Das None wird nicht mehr implizit ignoriert und ist stattdessen konsistent mit der Interpretation von None in anderen Kontexten als der einer Konjunktion.

Der korrekte Code für 0.8 und 0.9 sollte lauten:

from sqlalchemy.sql import and_

if conditions:
    stmt = stmt.where(and_(*conditions))

Eine weitere Variante, die auf allen Backends unter 0.9 funktioniert, unter 0.8 aber nur auf Backends, die boolesche Konstanten unterstützen:

from sqlalchemy.sql import true

condition = true()

for cond in conditions:
    condition = cond & condition

stmt = stmt.where(condition)

Unter 0.8 erzeugt dies eine SELECT-Anweisung, die immer AND true in der WHERE-Klausel hat, was von Backends, die keine booleschen Konstanten unterstützen (MySQL, MSSQL), nicht akzeptiert wird. Unter 0.9 wird die Konstante true innerhalb einer and_()-Konjunktion verworfen.

Der „password“-Teil von create_engine() betrachtet das Zeichen „+“ nicht mehr als kodiertes Leerzeichen

Aus welchem Grund auch immer wurde die Python-Funktion unquote_plus() auf das „password“-Feld einer URL angewendet, was eine falsche Anwendung der in RFC 1738 beschriebenen Kodierungsregeln ist, da sie Leerzeichen als Pluszeichen kodierte. Die Stringifizierung einer URL kodiert nun nur noch „:“, „@“ oder „/“ und nichts anderes, und wird nun sowohl auf das username- als auch auf das password-Feld angewendet (zuvor nur auf das Passwort). Beim Parsen werden kodierte Zeichen konvertiert, aber Pluszeichen und Leerzeichen werden unverändert übernommen.

# password: "pass word + other:words"
dbtype://user:pass word + other%3Awords@host/dbname

# password: "apples/oranges"
dbtype://username:apples%2Foranges@hostspec/database

# password: "apples@oranges@@"
dbtype://username:apples%40oranges%40%40@hostspec/database

# password: '', username is "username@"
dbtype://username%40:@hostspec/database

#2873

Die Vorrangregeln für COLLATE wurden geändert

Zuvor würde ein Ausdruck wie der folgende:

print((column("x") == "somevalue").collate("en_EN"))

einen Ausdruck wie diesen erzeugen:

-- 0.8 behavior
(x = :x_1) COLLATE en_EN

Dies wird von MSSQL missverstanden und ist generell nicht die Syntax, die für jede Datenbank vorgeschlagen wird. Der Ausdruck wird nun die durch die Dokumentation der meisten Datenbanken illustrierte Syntax erzeugen.

-- 0.9 behavior
x = :x_1 COLLATE en_EN

Die potenziell abwärtsinkompatible Änderung ergibt sich, wenn der Operator ColumnOperators.collate() auf die rechte Spalte angewendet wird, wie folgt:

print(column("x") == literal("somevalue").collate("en_EN"))

Unter 0.8 erzeugt dies:

x = :param_1 COLLATE en_EN

Unter 0.9 erzeugt es nun die genauere, aber wahrscheinlich nicht gewünschte Form von:

x = (:param_1 COLLATE en_EN)

Der Operator ColumnOperators.collate() funktioniert nun auch angemessener innerhalb eines ORDER BY-Ausdrucks, da den Operatoren ASC und DESC eine spezifische Priorität eingeräumt wurde, die wiederum sicherstellt, dass keine Klammern generiert werden.

>>> # 0.8
>>> print(column("x").collate("en_EN").desc())
(x COLLATE en_EN) DESC
>>> # 0.9 >>> print(column("x").collate("en_EN").desc())
x COLLATE en_EN DESC

#2879

PostgreSQL CREATE TYPE <x> AS ENUM wendet jetzt Anführungszeichen auf Werte an

Der Typ ENUM wendet nun eine Maskierung auf einfache Anführungszeichen innerhalb der aufgezählten Werte an.

>>> from sqlalchemy.dialects import postgresql
>>> type = postgresql.ENUM("one", "two", "three's", name="myenum")
>>> from sqlalchemy.dialects.postgresql import base
>>> print(base.CreateEnumType(type).compile(dialect=postgresql.dialect()))
CREATE TYPE myenum AS ENUM ('one','two','three''s')

Bestehende Workarounds, die bereits einfache Anführungszeichen maskieren, müssen modifiziert werden, da sie sonst doppelt maskiert werden.

#2878

Neue Funktionen

API zur Ereignisentfernung

Ereignisse, die mit listen() oder listens_for() eingerichtet wurden, können nun mit der neuen Funktion remove() entfernt werden. Die an remove() übergebenen Argumente target, identifier und fn müssen exakt mit denen übereinstimmen, die zum Zuhören verwendet wurden, und das Ereignis wird von allen Orten entfernt, an denen es eingerichtet wurde.

@event.listens_for(MyClass, "before_insert", propagate=True)
def my_before_insert(mapper, connection, target):
    """listen for before_insert"""
    # ...


event.remove(MyClass, "before_insert", my_before_insert)

Im obigen Beispiel wird das Flag propagate=True gesetzt. Das bedeutet, dass my_before_insert() als Listener für MyClass sowie für alle Unterklassen von MyClass eingerichtet wird. Das System verfolgt, wohin die Listener-Funktion my_before_insert() als Ergebnis dieses Aufrufs platziert wurde und entfernt sie als Ergebnis des Aufrufs von remove().

Das Entfernungssystem verwendet eine Registrierung, um Argumente, die an listen() übergeben wurden, mit Sammlungen von Ereignis-Listenern zu verknüpfen, welche in vielen Fällen wrappte Versionen der ursprünglichen, vom Benutzer bereitgestellten Funktion sind. Diese Registrierung nutzt intensiv schwache Referenzen, um es allen enthaltenen Inhalten, wie Listener-Zielen, zu ermöglichen, garbage collected zu werden, wenn sie aus dem Gültigkeitsbereich fallen.

#2268

Neue Query Options API; load_only() Option

Das System von Loader-Optionen wie joinedload(), subqueryload(), lazyload(), defer() usw. bauen alle auf einem neuen System auf, das als Load bekannt ist. Load bietet einen „method chained“ (auch bekannt als generativ) Ansatz für Loader-Optionen, so dass anstatt langer Pfade mit Punkten oder mehreren Attributnamen ein expliziter Loader-Stil für jeden Pfad angegeben wird.

Obwohl der neue Weg etwas wortreicher ist, ist er einfacher zu verstehen, da keine Mehrdeutigkeit besteht, welche Optionen auf welche Pfade angewendet werden; er vereinfacht die Methodensignaturen der Optionen und bietet größere Flexibilität, insbesondere für spaltenbasierte Optionen. Die alten Systeme bleiben ebenfalls auf unbestimmte Zeit funktionsfähig und alle Stile können gemischt werden.

Alter Weg

Um einen bestimmten Ladestil entlang jedes Links in einem mehrteiligen Pfad festzulegen, muss die Option _all() verwendet werden.

query(User).options(joinedload_all("orders.items.keywords"))

Neuer Weg

Loader-Optionen sind jetzt verkettbar, sodass dieselbe Methode joinedload(x) auf jeden Link angewendet wird, ohne dass zwischen joinedload() und joinedload_all() unterschieden werden muss.

query(User).options(joinedload("orders").joinedload("items").joinedload("keywords"))

Alter Weg

Das Setzen einer Option auf einem Pfad, der auf einer Unterklasse basiert, erfordert, dass alle Links im Pfad als klassengebundene Attribute ausgeschrieben werden, da die Methode PropComparator.of_type() aufgerufen werden muss.

session.query(Company).options(
    subqueryload_all(Company.employees.of_type(Engineer), Engineer.machines)
)

Neuer Weg

Nur die Elemente im Pfad, die PropComparator.of_type() tatsächlich benötigen, müssen als klassengebundenes Attribut festgelegt werden; zeichenbasierte Namen können danach wieder aufgenommen werden.

session.query(Company).options(
    subqueryload(Company.employees.of_type(Engineer)).subqueryload("machines")
)

Alter Weg

Das Setzen der Loader-Option am letzten Link in einem langen Pfad verwendet eine Syntax, die stark danach aussieht, als würde sie die Option für alle Links im Pfad setzen, was zu Verwirrung führt.

query(User).options(subqueryload("orders.items.keywords"))

Neuer Weg

Ein Pfad kann nun mit defaultload() für Einträge im Pfad, bei denen der bestehende Loader-Stil unverändert bleiben soll, ausgeschrieben werden. Wortreicher, aber die Absicht ist klarer.

query(User).options(defaultload("orders").defaultload("items").subqueryload("keywords"))

Der Punktstil kann weiterhin genutzt werden, insbesondere um mehrere Pfadelemente zu überspringen.

query(User).options(defaultload("orders.items").subqueryload("keywords"))

Alter Weg

Die Option defer() auf einem Pfad musste für jede Spalte mit dem vollständigen Pfad ausgeschrieben werden.

query(User).options(defer("orders.description"), defer("orders.isopen"))

Neuer Weg

Ein einzelnes Load-Objekt, das den Zielpfad erreicht, kann wiederholt Load.defer() aufgerufen werden.

query(User).options(defaultload("orders").defer("description").defer("isopen"))

Die Load-Klasse

Die Klasse Load kann direkt verwendet werden, um ein „gebundenes“ Ziel bereitzustellen, insbesondere wenn mehrere Elternentitäten vorhanden sind.

from sqlalchemy.orm import Load

query(User, Address).options(Load(Address).joinedload("entries"))

Nur laden

Eine neue Option load_only() erreicht einen „ defer everything but“-Ladetyp, der nur die angegebenen Spalten lädt und den Rest verzögert.

from sqlalchemy.orm import load_only

query(User).options(load_only("name", "fullname"))

# specify explicit parent entity
query(User, Address).options(Load(User).load_only("name", "fullname"))

# specify path
query(User).options(joinedload(User.addresses).load_only("email_address"))

Klassenspezifische Wildcards

Mit Load kann ein Wildcard verwendet werden, um das Laden für alle Beziehungen (oder vielleicht Spalten) einer bestimmten Entität festzulegen, ohne andere zu beeinflussen.

# lazyload all User relationships
query(User).options(Load(User).lazyload("*"))

# undefer all User columns
query(User).options(Load(User).undefer("*"))

# lazyload all Address relationships
query(User).options(defaultload(User.addresses).lazyload("*"))

# undefer all Address columns
query(User).options(defaultload(User.addresses).undefer("*"))

#1418

Neue text()-Funktionen

Der text()-Konstrukt erhält neue Methoden.

  • TextClause.bindparams() ermöglicht die flexible Festlegung von Bindparameter-Typen und -Werten.

    # setup values
    stmt = text(
        "SELECT id, name FROM user WHERE name=:name AND timestamp=:timestamp"
    ).bindparams(name="ed", timestamp=datetime(2012, 11, 10, 15, 12, 35))
    
    # setup types and/or values
    stmt = (
        text("SELECT id, name FROM user WHERE name=:name AND timestamp=:timestamp")
        .bindparams(bindparam("name", value="ed"), bindparam("timestamp", type_=DateTime()))
        .bindparam(timestamp=datetime(2012, 11, 10, 15, 12, 35))
    )
  • TextClause.columns() überschreibt die Option typemap von text() und gibt ein neues Konstrukt zurück, TextAsFrom

    # turn a text() into an alias(), with a .c. collection:
    stmt = text("SELECT id, name FROM user").columns(id=Integer, name=String)
    stmt = stmt.alias()
    
    stmt = select([addresses]).select_from(
        addresses.join(stmt), addresses.c.user_id == stmt.c.id
    )
    
    
    # or into a cte():
    stmt = text("SELECT id, name FROM user").columns(id=Integer, name=String)
    stmt = stmt.cte("x")
    
    stmt = select([addresses]).select_from(
        addresses.join(stmt), addresses.c.user_id == stmt.c.id
    )

#2877

INSERT aus SELECT

Nach buchstäblich jahrelanger sinnloser Prokrastination wurde dieses relativ geringfügige syntaktische Merkmal hinzugefügt und auch auf 0.8.3 zurückportiert, ist also technisch gesehen in 0.9 nicht „neu“. Ein select()-Konstrukt oder ein anderes kompatibles Konstrukt kann an die neue Methode Insert.from_select() übergeben werden, wo es zum Rendern eines INSERT .. SELECT-Konstrukts verwendet wird

>>> from sqlalchemy.sql import table, column
>>> t1 = table("t1", column("a"), column("b"))
>>> t2 = table("t2", column("x"), column("y"))
>>> print(t1.insert().from_select(["a", "b"], t2.select().where(t2.c.y == 5)))
INSERT INTO t1 (a, b) SELECT t2.x, t2.y FROM t2 WHERE t2.y = :y_1

Das Konstrukt ist intelligent genug, um auch ORM-Objekte wie Klassen und Query-Objekte zu verarbeiten

s = Session()
q = s.query(User.id, User.name).filter_by(name="ed")
ins = insert(Address).from_select((Address.id, Address.email_address), q)

Rendert

INSERT INTO addresses (id, email_address)
SELECT users.id AS users_id, users.name AS users_name
FROM users WHERE users.name = :name_1

#722

Neue FOR UPDATE-Unterstützung für select(), Query()

Es wird versucht, die Spezifikation der FOR UPDATE-Klausel in SELECT-Anweisungen im Core und ORM zu vereinfachen, und es wird Unterstützung für FOR UPDATE OF SQL hinzugefügt, das von PostgreSQL und Oracle unterstützt wird.

Mit dem Core GenerativeSelect.with_for_update() können Optionen wie FOR SHARE und NOWAIT einzeln angegeben werden, anstatt auf willkürliche Zeichenketten-Codes zu verweisen

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

Unter PostgreSQL kann die obige Anweisung wie folgt gerendert werden

SELECT table.a, table.b FROM table FOR SHARE OF table NOWAIT

Das Query-Objekt erhält eine ähnliche Methode Query.with_for_update(), die sich auf die gleiche Weise verhält. Diese Methode überschreibt die bestehende Methode Query.with_lockmode(), die FOR UPDATE-Klauseln über ein anderes System übersetzte. Derzeit wird das Zeichenkettenargument „lockmode“ weiterhin von der Methode Session.refresh() akzeptiert.

Genauigkeit der Fließkomma-String-Konvertierung für native Fließkomma-Datentypen konfigurierbar

Die Konvertierung, die SQLAlchemy durchführt, wenn ein DBAPI einen Python-Fließkomma-Typ zurückgibt, der in einen Python Decimal() umgewandelt werden soll, beinhaltet notwendigerweise einen Zwischenschritt, der den Fließkommawert in einen String umwandelt. Die für diese String-Konvertierung verwendete Skala war bisher auf 10 fest codiert und ist nun konfigurierbar. Die Einstellung ist sowohl für den Numeric- als auch für den Float-Typ sowie für alle SQL- und dialektspezifischen Nachfolgetypen über den Parameter decimal_return_scale verfügbar. Wenn der Typ einen .scale-Parameter unterstützt, wie es bei Numeric und einigen Float-Typen wie DOUBLE der Fall ist, wird der Wert von .scale als Standardwert für .decimal_return_scale verwendet, wenn dieser nicht anderweitig angegeben ist. Wenn sowohl .scale als auch .decimal_return_scale fehlen, gilt der Standardwert 10. Z.B.:

from sqlalchemy.dialects.mysql import DOUBLE
import decimal

data = Table(
    "data",
    metadata,
    Column("double_value", mysql.DOUBLE(decimal_return_scale=12, asdecimal=True)),
)

conn.execute(
    data.insert(),
    double_value=45.768392065789,
)
result = conn.scalar(select([data.c.double_value]))

# previously, this would typically be Decimal("45.7683920658"),
# e.g. trimmed to 10 decimal places

# now we get 12, as requested, as MySQL can support this
# much precision for DOUBLE
assert result == decimal.Decimal("45.768392065789")

#2867

Spaltenbündel für ORM-Abfragen

Das Bundle ermöglicht das Abfragen von Spaltensätzen, die dann unter einem Namen im von der Abfrage zurückgegebenen Tupel zusammengefasst werden. Die ursprünglichen Zwecke von Bundle sind 1. um „zusammengesetzte“ ORM-Spalten als einzelnen Wert in einem spaltenbasierten Ergebnis zurückzugeben, anstatt sie auf einzelne Spalten zu erweitern, und 2. um die Erstellung benutzerdefinierter Ergebnis-Set-Konstrukte innerhalb des ORM zu ermöglichen, unter Verwendung von Ad-hoc-Spalten und Rückgabetypen, ohne die schwerfälligeren Mechanismen von zugeordneten Klassen zu involvieren.

#2824

Serverseitige Versionszählung

Die Versionierungsfunktion des ORM (jetzt auch dokumentiert unter Konfiguration eines Versionszählers) kann nun serverseitige Versionszählungsmechanismen nutzen, wie z. B. solche, die durch Trigger oder Datenbank-Systemspalten erzeugt werden, sowie bedingte programmatische Schemata außerhalb der version_id_counter-Funktion selbst. Durch Angabe des Wertes False für den Parameter version_id_generator verwendet das ORM den bereits gesetzten Versionsbezeichner oder ruft alternativ den Versionsbezeichner aus jeder Zeile ab, wenn INSERT oder UPDATE gesendet wird. Bei Verwendung eines serverseitig generierten Versionsbezeichners wird dringend empfohlen, diese Funktion nur auf einem Backend mit starker RETURNING-Unterstützung (PostgreSQL, SQL Server; Oracle unterstützt ebenfalls RETURNING, aber der cx_oracle-Treiber hat nur begrenzte Unterstützung) zu verwenden, da sonst die zusätzlichen SELECT-Anweisungen erhebliche Leistungseinbußen verursachen. Das Beispiel unter Serverseitige Versionszähler veranschaulicht die Verwendung der PostgreSQL xmin-Systemspalte, um sie in die Versionsfunktion des ORM zu integrieren.

#2793

Option include_backrefs=False für @validates

Die Funktion validates() akzeptiert nun eine Option include_backrefs=True, die das Auslösen des Validators für den Fall überspringt, dass das Ereignis von einem Backref initiiert wurde

from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship, validates
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

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

    @validates("bs")
    def validate_bs(self, key, item):
        print("A.bs validator")
        return item


class B(Base):
    __tablename__ = "b"

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

    @validates("a", include_backrefs=False)
    def validate_a(self, key, item):
        print("B.a validator")
        return item


a1 = A()
a1.bs.append(B())  # prints only "A.bs validator"

#1535

PostgreSQL JSON-Typ

Der PostgreSQL-Dialekt verfügt nun über einen JSON-Typ als Ergänzung zum HSTORE-Typ.

Siehe auch

JSON

#2581

Automap-Erweiterung

Eine neue Erweiterung wird in **0.9.1** hinzugefügt, bekannt als sqlalchemy.ext.automap. Dies ist eine **experimentelle** Erweiterung, die die Funktionalität von Declarative sowie die DeferredReflection-Klasse erweitert. Im Wesentlichen stellt die Erweiterung eine Basisklasse AutomapBase bereit, die automatisch zugeordnete Klassen und Beziehungen zwischen ihnen auf Basis gegebener Tabellenmetadaten generiert.

Die normalerweise verwendete MetaData kann durch Reflexion erzeugt werden, aber es besteht keine Anforderung, dass Reflexion verwendet wird. Der grundlegendste Anwendungsfall zeigt, wie sqlalchemy.ext.automap zugeordnete Klassen, einschließlich Beziehungen, basierend auf einem gespiegelten Schema liefern kann

from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from sqlalchemy import create_engine

Base = automap_base()

# engine, suppose it has two tables 'user' and 'address' set up
engine = create_engine("sqlite:///mydatabase.db")

# reflect the tables
Base.prepare(engine, reflect=True)

# mapped classes are now created with names matching that of the table
# name.
User = Base.classes.user
Address = Base.classes.address

session = Session(engine)

# rudimentary relationships are produced
session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
session.commit()

# collection-based relationships are by default named "<classname>_collection"
print(u1.address_collection)

Darüber hinaus ist die AutomapBase-Klasse eine deklarative Basis und unterstützt alle Funktionen, die Declarative unterstützt. Die „Automapping“-Funktion kann mit einem vorhandenen, explizit deklarierten Schema verwendet werden, um nur Beziehungen und fehlende Klassen zu generieren. Benennungsschemata und Beziehungsgenerierungsroutinen können mit aufrufbaren Funktionen hinzugefügt werden.

Es wird gehofft, dass das AutomapBase-System eine schnelle und modernisierte Lösung für das Problem bietet, das auch das sehr berühmte SQLSoup zu lösen versucht, nämlich die schnelle und rudimentäre Erzeugung eines Objektmodells aus einer bestehenden Datenbank im laufenden Betrieb. Indem das Problem strikt auf der Ebene der Mapper-Konfiguration behandelt und vollständig mit bestehenden Declarative-Klassentechniken integriert wird, versucht AutomapBase, einen gut integrierten Ansatz für die zügige automatische Generierung von Ad-hoc-Zuordnungen zu bieten.

Siehe auch

Automap

Verhaltensverbesserungen

Verbesserungen, die keine Kompatibilitätsprobleme verursachen sollten, außer in extrem seltenen und ungewöhnlichen hypothetischen Fällen, aber gut zu wissen sind, falls unerwartete Probleme auftreten.

Viele JOIN- und LEFT OUTER JOIN-Ausdrücke werden nicht mehr in (SELECT * FROM ..) AS ANON_1 verpackt

Seit vielen Jahren war das SQLAlchemy ORM daran gehindert, einen JOIN in die rechte Seite eines bestehenden JOINs zu verschachteln (typischerweise einen LEFT OUTER JOIN, da INNER JOINs immer abgeflacht werden konnten)

SELECT a.*, b.*, c.* FROM a LEFT OUTER JOIN (b JOIN c ON b.id = c.id) ON a.id

Dies lag daran, dass SQLite bis Version **3.7.16** eine Anweisung dieses Formats nicht parsen konnte.

SQLite version 3.7.15.2 2013-01-09 11:53:05
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> create table a(id integer);
sqlite> create table b(id integer);
sqlite> create table c(id integer);
sqlite> select a.id, b.id, c.id from a left outer join (b join c on b.id=c.id) on b.id=a.id;
Error: no such column: b.id

Right-outer-joins sind natürlich eine weitere Möglichkeit, die rechte Seite zu umgehen; dies wäre erheblich kompliziert und visuell unangenehm zu implementieren, aber glücklicherweise unterstützt SQLite auch kein RIGHT OUTER JOIN :)

sqlite> select a.id, b.id, c.id from b join c on b.id=c.id
   ...> right outer join a on b.id=a.id;
Error: RIGHT and FULL OUTER JOINs are not currently supported

Schon 2005 war nicht klar, ob andere Datenbanken Probleme mit dieser Form hatten, aber heute scheint klar zu sein, dass jede getestete Datenbank außer SQLite sie unterstützt (Oracle 8, eine sehr alte Datenbank, unterstützt das JOIN-Schlüsselwort überhaupt nicht, aber SQLAlchemy hatte schon immer ein einfaches Umschreibungsverfahren für die Oracle-Syntax implementiert). Um die Sache noch schlimmer zu machen, verschlechtert die übliche Umgehung von SQLAlchemy, ein SELECT anzuwenden, oft die Leistung auf Plattformen wie PostgreSQL und MySQL

SELECT a.*, anon_1.* FROM a LEFT OUTER JOIN (
                SELECT b.id AS b_id, c.id AS c_id
                FROM b JOIN c ON b.id = c.id
            ) AS anon_1 ON a.id=anon_1.b_id

Ein JOIN der obigen Form ist üblich bei der Arbeit mit Joined-Table-Vererbung; jedes Mal, wenn Query.join() verwendet wird, um von einem Elternteil zu einer Joined-Table-Unterklasse zu verbinden, oder wenn joinedload() ähnlich verwendet wird, stellte SQLAlchemy ORM immer sicher, dass kein verschachtelter JOIN gerendert wurde, um nicht zu riskieren, dass die Abfrage auf SQLite nicht ausgeführt werden konnte. Obwohl Core immer einen JOIN der kompakteren Form unterstützt hat, musste ORM ihn vermeiden.

Ein zusätzliches Problem trat bei der Erzeugung von Joins über Many-to-Many-Beziehungen auf, bei denen spezielle Kriterien in der ON-Klausel vorhanden sind. Betrachten Sie einen Eager-Load-Join wie den folgenden

session.query(Order).outerjoin(Order.items)

Angenommen, ein Many-to-Many von Order zu Item, das sich tatsächlich auf eine Unterklasse wie Subitem bezieht, würde der SQL für das obige wie folgt aussehen:

SELECT order.id, order.name
FROM order LEFT OUTER JOIN order_item ON order.id = order_item.order_id
LEFT OUTER JOIN item ON order_item.item_id = item.id AND item.type = 'subitem'

Was ist falsch an der obigen Abfrage? Im Grunde, dass sie viele order / order_item Zeilen lädt, bei denen die Kriterien von item.type == 'subitem' nicht zutreffen.

Ab SQLAlchemy 0.9 wurde ein völlig neuer Ansatz gewählt. Das ORM kümmert sich nicht mehr um das Verschachteln von JOINs auf der rechten Seite eines umschließenden JOINs und wird diese nun so oft wie möglich rendern, während immer noch die korrekten Ergebnisse zurückgegeben werden. Wenn die SQL-Anweisung zur Kompilierung übergeben wird, **schreibt der Dialektcompiler den Join** so um, dass er für das Ziel-Backend geeignet ist, falls dieses Backend keinen recht-verschachtelten JOIN unterstützt (was derzeit nur SQLite ist - wenn andere Backends dieses Problem haben, lassen Sie es uns bitte wissen!).

Ein reguläres query(Parent).join(Subclass) wird nun normalerweise einen einfacheren Ausdruck erzeugen

SELECT parent.id AS parent_id
FROM parent JOIN (
        base_table JOIN subclass_table
        ON base_table.id = subclass_table.id) ON parent.id = base_table.parent_id

Joined Eager Loads wie query(Parent).options(joinedload(Parent.subclasses)) werden die einzelnen Tabellen aliasen, anstatt sie in ein ANON_1 zu verpacken

SELECT parent.*, base_table_1.*, subclass_table_1.* FROM parent
    LEFT OUTER JOIN (
        base_table AS base_table_1 JOIN subclass_table AS subclass_table_1
        ON base_table_1.id = subclass_table_1.id)
        ON parent.id = base_table_1.parent_id

Many-to-Many Joins und Eagerloads werden die „sekundären“ und „rechten“ Tabellen rechts verschachteln

SELECT order.id, order.name
FROM order LEFT OUTER JOIN
(order_item JOIN item ON order_item.item_id = item.id AND item.type = 'subitem')
ON order_item.order_id = order.id

Alle diese Joins, wenn sie mit einer Select-Anweisung gerendert werden, die explizit use_labels=True angibt, was für alle Abfragen gilt, die das ORM erzeugt, sind Kandidaten für das „Join-Rewriting“, d. h. das Umschreiben all dieser rechts verschachtelten Joins in verschachtelte SELECT-Anweisungen, während die identische Beschriftung beibehalten wird, die von der Select verwendet wird. SQLite, die einzige Datenbank, die diese sehr gängige SQL-Syntax selbst im Jahr 2013 nicht unterstützt, schultert die zusätzliche Komplexität selbst, wobei die obigen Abfragen wie folgt umgeschrieben werden:

-- sqlite only!
SELECT parent.id AS parent_id
    FROM parent JOIN (
        SELECT base_table.id AS base_table_id,
                base_table.parent_id AS base_table_parent_id,
                subclass_table.id AS subclass_table_id
        FROM base_table JOIN subclass_table ON base_table.id = subclass_table.id
    ) AS anon_1 ON parent.id = anon_1.base_table_parent_id

-- sqlite only!
SELECT parent.id AS parent_id, anon_1.subclass_table_1_id AS subclass_table_1_id,
        anon_1.base_table_1_id AS base_table_1_id,
        anon_1.base_table_1_parent_id AS base_table_1_parent_id
FROM parent LEFT OUTER JOIN (
    SELECT base_table_1.id AS base_table_1_id,
        base_table_1.parent_id AS base_table_1_parent_id,
        subclass_table_1.id AS subclass_table_1_id
    FROM base_table AS base_table_1
    JOIN subclass_table AS subclass_table_1 ON base_table_1.id = subclass_table_1.id
) AS anon_1 ON parent.id = anon_1.base_table_1_parent_id

-- sqlite only!
SELECT "order".id AS order_id
FROM "order" LEFT OUTER JOIN (
        SELECT order_item_1.order_id AS order_item_1_order_id,
            order_item_1.item_id AS order_item_1_item_id,
            item.id AS item_id, item.type AS item_type
FROM order_item AS order_item_1
    JOIN item ON item.id = order_item_1.item_id AND item.type IN (?)
) AS anon_1 ON "order".id = anon_1.order_item_1_order_id

Hinweis

Ab SQLAlchemy 1.1 deaktivieren sich die Workarounds für SQLite in dieser Funktion automatisch, wenn SQLite-Version **3.7.16** oder höher erkannt wird, da SQLite die Unterstützung für rechts verschachtelte Joins repariert hat.

Die Funktionen Join.alias(), aliased() und with_polymorphic() unterstützen jetzt ein neues Argument, flat=True, das zum Konstruieren von Aliassen von Joined-Table-Entitäten verwendet wird, ohne sie in ein SELECT einzubetten. Dieses Flag ist nicht standardmäßig aktiviert, um die Abwärtskompatibilität zu gewährleisten - aber jetzt kann ein „polymorphes“ wählbares Element als Ziel ohne Unterabfragen gejoint werden

employee_alias = with_polymorphic(Person, [Engineer, Manager], flat=True)

session.query(Company).join(Company.employees.of_type(employee_alias)).filter(
    or_(Engineer.primary_language == "python", Manager.manager_name == "dilbert")
)

Generiert (überall außer SQLite)

SELECT companies.company_id AS companies_company_id, companies.name AS companies_name
FROM companies JOIN (
    people AS people_1
    LEFT OUTER JOIN engineers AS engineers_1 ON people_1.person_id = engineers_1.person_id
    LEFT OUTER JOIN managers AS managers_1 ON people_1.person_id = managers_1.person_id
) ON companies.company_id = people_1.company_id
WHERE engineers.primary_language = %(primary_language_1)s
    OR managers.manager_name = %(manager_name_1)s

#2369 #2587

Rechts verschachtelte innere Joins in Joined Eager Loads verfügbar

Ab Version 0.9.4 kann das oben erwähnte rechts verschachtelte Joining im Falle eines Joined Eager Loads aktiviert werden, bei dem ein „äußerer“ Join mit einem „inneren“ auf der rechten Seite verbunden ist.

Normalerweise ein Joined Eager Load Kette wie die folgende

query(User).options(
    joinedload("orders", innerjoin=False).joinedload("items", innerjoin=True)
)

Würde keinen inneren Join erzeugen; aufgrund des LEFT OUTER JOIN von user->order konnte Joined Eager Loading keinen INNER JOIN von order->items verwenden, ohne die zurückgegebenen user-Zeilen zu ändern, und würde stattdessen die „verkettete“ Direktive innerjoin=True ignorieren. Wie 0.9.0 dies geliefert hätte, wäre anstelle von

FROM users LEFT OUTER JOIN orders ON <onclause> LEFT OUTER JOIN items ON <onclause>

die neue Logik „rechts verschachtelte Joins sind OK“ würde greifen, und wir bekämen

FROM users LEFT OUTER JOIN (orders JOIN items ON <onclause>) ON <onclause>

Da wir die Gelegenheit dazu verpasst haben, um weitere Regressionen zu vermeiden, haben wir die oben genannte Funktionalität durch Angabe der Zeichenkette "nested" für joinedload.innerjoin hinzugefügt

query(User).options(
    joinedload("orders", innerjoin=False).joinedload("items", innerjoin="nested")
)

Dieses Feature ist neu in 0.9.4.

#2976

ORM kann gerade generierte INSERT/UPDATE-Standardwerte effizient über RETURNING abrufen

Der Mapper unterstützt seit langem ein undokumentiertes Flag namens eager_defaults=True. Die Auswirkung dieses Flags ist, dass nach einem INSERT oder UPDATE, wenn die Zeile bekannte serverseitige Standardwerte hat, ein SELECT folgt, um diese neuen Werte „eagerly“ zu laden. Normalerweise werden die serverseitig generierten Spalten als „abgelaufen“ auf dem Objekt markiert, so dass kein Overhead entsteht, es sei denn, die Anwendung greift bald nach dem Flush auf diese Spalten zu. Das Flag eager_defaults war daher nicht sehr nützlich, da es nur die Leistung verringern konnte und nur zur Unterstützung exotischer Ereignisschemata vorhanden war, bei denen Benutzer Standardwerte sofort während des Flush-Prozesses benötigen.

In 0.9 können infolge der Version-ID-Verbesserungen eager_defaults eine RETURNING-Klausel für diese Werte ausgeben, so dass auf einem Backend mit starker RETURNING-Unterstützung, insbesondere PostgreSQL, das ORM neu generierte Standard- und SQL-Ausdruckswerte inline mit dem INSERT oder UPDATE abrufen kann. eager_defaults nutzt bei Aktivierung automatisch RETURNING, wenn das Ziel-Backend und die Table „implizites Returning“ unterstützen.

Subquery Eager Loading wendet DISTINCT auf den innersten SELECT für einige Abfragen an

Um die Anzahl der Duplikatzeilen zu reduzieren, die durch Subquery Eager Loading generiert werden können, wenn eine Many-to-One-Beziehung beteiligt ist, wird ein DISTINCT-Schlüsselwort auf den innersten SELECT angewendet, wenn der Join auf Spalten abzielt, die nicht den Primärschlüssel bilden, wie z. B. beim Laden entlang eines Many-to-One.

Das heißt, beim Subquery-Laden eines Many-to-One von A nach B

SELECT b.id AS b_id, b.name AS b_name, anon_1.b_id AS a_b_id
FROM (SELECT DISTINCT a_b_id FROM a) AS anon_1
JOIN b ON b.id = anon_1.a_b_id

Da a.b_id ein nicht-disjunkter Fremdschlüssel ist, wird DISTINCT angewendet, um redundante a.b_id zu eliminieren. Das Verhalten kann für eine bestimmte relationship() bedingungslos ein- oder ausgeschaltet werden, indem das Flag distinct_target_key gesetzt wird, wobei der Wert True für bedingungslos eingeschaltet, False für bedingungslos ausgeschaltet und None dafür steht, dass die Funktion greift, wenn der Ziel-SELECT auf Spalten abzielt, die keinen vollständigen Primärschlüssel bilden. In 0.9 ist None der Standard.

Die Option wird auch auf 0.8 zurückportiert, wo die Option distinct_target_key standardmäßig auf False gesetzt ist.

Während das Feature hier dazu dient, die Leistung durch die Eliminierung von Duplikatzeilen zu verbessern, kann das DISTINCT-Schlüsselwort in SQL selbst eine negative Auswirkung auf die Leistung haben. Wenn Spalten im SELECT nicht indiziert sind, wird DISTINCT wahrscheinlich eine ORDER BY auf der Zeilenmenge durchführen, was teuer sein kann. Indem das Feature nur auf Fremdschlüssel beschränkt wird, die in jedem Fall hoffentlich indiziert sind, werden die neuen Standardwerte als angemessen erachtet.

Das Feature eliminiert auch nicht jedes mögliche Duplikatzeilen-Szenario; wenn ein Many-to-One woanders in der Join-Kette vorhanden ist, können Duplikatzeilen immer noch vorhanden sein.

#2836

Backref-Handler können nun mehr als eine Ebene tief propagieren

Der Mechanismus, durch den Attributereignisse ihren „Initiator“, d. h. das Objekt, das mit dem Beginn des Ereignisses verbunden ist, weitergeben, wurde geändert; anstelle eines AttributeImpl wird ein neues Objekt Event übergeben; dieses Objekt bezieht sich auf das AttributeImpl sowie auf einen „Operations-Token“, der darstellt, ob die Operation eine Hinzufüge-, Entfernungs- oder Ersetzungsoperation ist.

Das Attributereignissystem prüft nicht mehr auf dieses „Initiator“-Objekt, um eine rekursive Serie von Attributereignissen zu stoppen. Stattdessen wurde das System zur Verhinderung endloser Rekursion aufgrund gegenseitig abhängiger Backref-Handler auf die ORM-Backref-Ereignisbehandler verlagert, die nun die Aufgabe übernehmen, sicherzustellen, dass eine Kette von gegenseitig abhängigen Ereignissen (wie das Hinzufügen zu einer Sammlung A.bs, das Setzen des Many-to-One-Attributs B.a als Reaktion) nicht in einen endlosen Rekursionsstrom gerät. Die Begründung hierfür ist, dass das Backref-System mit mehr Details und Kontrolle über die Ereignisweitergabe es endlich ermöglicht, Operationen mehr als eine Ebene tief durchzuführen; das typische Szenario ist, wenn das Hinzufügen zu einer Sammlung eine Many-to-One-Ersetzungsoperation zur Folge hat, die wiederum dazu führen sollte, dass das Element aus einer vorherigen Sammlung entfernt wird

class Parent(Base):
    __tablename__ = "parent"

    id = Column(Integer, primary_key=True)
    children = relationship("Child", backref="parent")


class Child(Base):
    __tablename__ = "child"

    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))


p1 = Parent()
p2 = Parent()
c1 = Child()

p1.children.append(c1)

assert c1.parent is p1  # backref event establishes c1.parent as p1

p2.children.append(c1)

assert c1.parent is p2  # backref event establishes c1.parent as p2
assert c1 not in p1.children  # second backref event removes c1 from p1.children

Oben, vor dieser Änderung, wäre das c1-Objekt immer noch in p1.children vorhanden gewesen, obwohl es sich gleichzeitig auch in p2.children befindet; die Backref-Handler hätten aufgehört, c1.parent durch p2 anstelle von p1 zu ersetzen. In 0.9, unter Verwendung des detaillierteren Event-Objekts und der Ermöglichung, dass die Backref-Handler detailliertere Entscheidungen über diese Objekte treffen können, kann die Weitergabe fortgesetzt werden, um c1 aus p1.children zu entfernen, während gleichzeitig eine Überprüfung gegen eine endlose Rekursionsschleife beibehalten wird.

Endbenutzercode, der a. die Ereignisse AttributeEvents.set(), AttributeEvents.append() oder AttributeEvents.remove() verwendet und b. weitere Attributmodifikationsoperationen als Ergebnis dieser Ereignisse initiiert, muss möglicherweise angepasst werden, um rekursive Schleifen zu verhindern, da das Attributsystem die Weitergabe einer Kette von Ereignissen im Falle von fehlenden Backref-Ereignisbehandlern nicht mehr endlos stoppt. Darüber hinaus muss Code, der vom Wert des initiator abhängt, an die neue API angepasst werden, und darüber hinaus bereit sein, dass sich der Wert des initiator innerhalb einer Kette von Backref-initiierten Ereignissen von seinem ursprünglichen Wert ändert, da die Backref-Handler für einige Operationen einen neuen initiator-Wert einfügen können.

#2789

Das Typsystem übernimmt nun die Aufgabe, „literal bind“-Werte zu rendern

Eine neue Methode wird zu TypeEngine TypeEngine.literal_processor() sowie TypeDecorator.process_literal_param() für TypeDecorator hinzugefügt, die die Aufgabe des Renderns sogenannter „inline literal parameters“ übernehmen – Parameter, die normalerweise als „gebundene“ Werte gerendert werden, aber aufgrund der Compilerkonfiguration stattdessen inline in die SQL-Anweisung gerendert werden. Dieses Feature wird beim Generieren von DDL für Konstrukte wie CheckConstraint verwendet, sowie von Alembic bei der Verwendung von Konstrukten wie op.inline_literal(). Zuvor prüfte eine einfache „isinstance“-Prüfung einige grundlegende Typen, und der „bind processor“ wurde bedingungslos verwendet, was zu Problemen führte, wie z. B. dass Strings vorzeitig in utf-8 kodiert wurden.

Benutzerdefinierte Typen, die mit TypeDecorator geschrieben wurden, sollten auch in „inline literal“-Szenarien funktionieren, da TypeDecorator.process_literal_param() standardmäßig auf TypeDecorator.process_bind_param() zurückfällt, da diese Methoden normalerweise eine Datenmanipulation durchführen und nicht so sehr, wie die Daten der Datenbank präsentiert werden. TypeDecorator.process_literal_param() kann angegeben werden, um speziell einen String zu erzeugen, der darstellt, wie ein Wert in eine Inline-DDL-Anweisung gerendert werden soll.

#2838

Schema-Identifikatoren tragen nun ihre eigenen Quoting-Informationen

Diese Änderung vereinfacht die Verwendung von sogenannten "Anführungszeichen"-Flags im Core, wie z. B. dem quote-Flag, das an Table und Column übergeben wird. Das Flag ist nun in den String-Namen selbst integriert, der nun als Instanz von quoted_name, einer String-Unterklasse, dargestellt wird. Der IdentifierPreparer verlässt sich nun in den meisten Fällen ausschließlich auf die Zitierpräferenzen, die vom quoted_name-Objekt gemeldet werden, anstatt explizite quote-Flags zu prüfen. Das hier behobene Problem umfasst, dass verschiedene case-sensitive Methoden wie Engine.has_table() sowie ähnliche Methoden in Dialekten nun mit explizit zitierten Namen funktionieren, ohne dass die Details der Zitier-Flags die APIs (von denen viele von Drittanbietern stammen) verkomplizieren oder rückwärts inkompatible Änderungen einführen müssen - insbesondere funktionieren nun eine größere Bandbreite von Bezeichnern korrekt mit den sogenannten "uppercase"-Backends wie Oracle, Firebird und DB2 (Backends, die Tabellen- und Spaltennamen in Großbuchstaben speichern und melden, um case-insensitive Namen zu handhaben).

Das quoted_name-Objekt wird intern nach Bedarf verwendet; wenn jedoch andere Schlüsselwörter feste Zitierpräferenzen erfordern, ist die Klasse öffentlich verfügbar.

#2812

Verbesserte Darstellung von Booleschen Konstanten, NULL-Konstanten, Konjunktionen

Neue Funktionen wurden zu den Konstanten true() und false() hinzugefügt, insbesondere in Verbindung mit den Funktionen and_() und or_() sowie dem Verhalten der WHERE/HAVING-Klauseln in Verbindung mit diesen Typen, booleschen Typen im Allgemeinen und der Konstante null().

Beginnend mit einer Tabelle wie dieser

from sqlalchemy import Table, Boolean, Integer, Column, MetaData

t1 = Table("t", MetaData(), Column("x", Boolean()), Column("y", Integer))

Ein SELECT-Konstrukt wird nun die boolesche Spalte als binären Ausdruck für Backends rendern, die kein true/false-Konstantenverhalten aufweisen

>>> from sqlalchemy import select, and_, false, true
>>> from sqlalchemy.dialects import mysql, postgresql

>>> print(select([t1]).where(t1.c.x).compile(dialect=mysql.dialect()))
SELECT t.x, t.y FROM t WHERE t.x = 1

Die Konstrukte and_() und or_() zeigen nun ein quasi "Short-Circuit"-Verhalten, d. h. sie kürzen einen gerenderten Ausdruck ab, wenn eine true() oder false() Konstante vorhanden ist

>>> print(
...     select([t1]).where(and_(t1.c.y > 5, false())).compile(dialect=postgresql.dialect())
... )
SELECT t.x, t.y FROM t WHERE false

true() kann als Basis zum Aufbau eines Ausdrucks verwendet werden

>>> expr = true()
>>> expr = expr & (t1.c.y > 5)
>>> print(select([t1]).where(expr))
SELECT t.x, t.y FROM t WHERE t.y > :y_1

Die booleschen Konstanten true() und false() selbst werden als 0 = 1 und 1 = 1 für ein Backend ohne boolesche Konstanten gerendert

>>> print(select([t1]).where(and_(t1.c.y > 5, false())).compile(dialect=mysql.dialect()))
SELECT t.x, t.y FROM t WHERE 0 = 1

Die Interpretation von None ist zwar nicht besonders gültiges SQL, aber zumindest nun konsistent

>>> print(select([t1.c.x]).where(None))
SELECT t.x FROM t WHERE NULL
>>> print(select([t1.c.x]).where(None).where(None))
SELECT t.x FROM t WHERE NULL AND NULL
>>> print(select([t1.c.x]).where(and_(None, None)))
SELECT t.x FROM t WHERE NULL AND NULL

#2804

Label-Konstrukte können nun in einer ORDER BY-Klausel als ihr eigener Name gerendert werden

Für den Fall, dass ein Label sowohl in der Spaltenklausel als auch in der ORDER BY-Klausel eines SELECT verwendet wird, wird das Label in der ORDER BY-Klausel nur als sein Name gerendert, vorausgesetzt, der zugrunde liegende Dialekt unterstützt diese Funktion.

Z.B. ein Beispiel wie

from sqlalchemy.sql import table, column, select, func

t = table("t", column("c1"), column("c2"))
expr = (func.foo(t.c.c1) + t.c.c2).label("expr")

stmt = select([expr]).order_by(expr)

print(stmt)

Vor 0.9 würde gerendert werden als

SELECT foo(t.c1) + t.c2 AS expr
FROM t ORDER BY foo(t.c1) + t.c2

Und wird nun gerendert als

SELECT foo(t.c1) + t.c2 AS expr
FROM t ORDER BY expr

Die ORDER BY rendert das Label nur, wenn das Label nicht weiter in einen Ausdruck innerhalb der ORDER BY eingebettet ist, außer einem einfachen ASC oder DESC.

Das obige Format funktioniert auf allen getesteten Datenbanken, könnte aber Kompatibilitätsprobleme mit älteren Datenbankversionen haben (MySQL 4? Oracle 8? etc.). Basierend auf Benutzerberichten können wir Regeln hinzufügen, die die Funktion basierend auf der Erkennung der Datenbankversion deaktivieren.

#1068

RowProxy hat nun ein Tupel-Sortierverhalten

Das RowProxy-Objekt verhält sich weitgehend wie ein Tupel, aber bis jetzt wurde es nicht wie ein Tupel sortiert, wenn eine Liste davon mit sorted() sortiert wurde. Die Methode __eq__() vergleicht nun beide Seiten als Tupel und eine Methode __lt__() wurde ebenfalls hinzugefügt.

users.insert().execute(
    dict(user_id=1, user_name="foo"),
    dict(user_id=2, user_name="bar"),
    dict(user_id=3, user_name="def"),
)

rows = users.select().order_by(users.c.user_name).execute().fetchall()

eq_(rows, [(2, "bar"), (3, "def"), (1, "foo")])

eq_(sorted(rows), [(1, "foo"), (2, "bar"), (3, "def")])

#2848

Ein bindparam()-Konstrukt ohne Typ wird per Kopie aufgewertet, wenn ein Typ verfügbar ist

Die Logik, die ein bindparam()-Konstrukt "aufwertet", um den Typ des umschließenden Ausdrucks anzunehmen, wurde auf zwei Arten verbessert. Erstens wird das bindparam()-Objekt **kopiert**, bevor der neue Typ zugewiesen wird, sodass das gegebene bindparam() nicht an Ort und Stelle mutiert wird. Zweitens geschieht dieselbe Operation, wenn ein Insert- oder Update-Konstrukt kompiliert wird, in Bezug auf die "Werte", die über die Methode ValuesBase.values() in der Anweisung gesetzt wurden.

Wenn ein nicht typisiertes bindparam() gegeben wird

bp = bindparam("some_col")

Wenn wir diesen Parameter wie folgt verwenden

expr = mytable.c.col == bp

Der Typ für bp bleibt NullType, aber wenn mytable.c.col vom Typ String ist, dann nimmt expr.right, also die rechte Seite des binären Ausdrucks, den Typ String an. Zuvor wäre bp selbst an Ort und Stelle geändert worden, um String als Typ zu haben.

Ähnlich geschieht diese Operation bei einem Insert oder Update

stmt = mytable.update().values(col=bp)

Oben bleibt bp unverändert, aber der String-Typ wird verwendet, wenn die Anweisung ausgeführt wird, was wir durch Untersuchung des binds-Wörterbuchs sehen können.

>>> compiled = stmt.compile()
>>> compiled.binds["some_col"].type
String

Die Funktion ermöglicht es benutzerdefinierten Typen, ihre erwartete Wirkung innerhalb von INSERT/UPDATE-Anweisungen zu erzielen, ohne dass diese Typen explizit in jedem bindparam()-Ausdruck angegeben werden müssen.

Die potenziell rückwärtskompatiblen Änderungen betreffen zwei unwahrscheinliche Szenarien. Da der gebundene Parameter **geklont** wird, sollten Benutzer nicht darauf vertrauen, dass Änderungen an einem bindparam()-Konstrukt nach dessen Erstellung an Ort und Stelle vorgenommen werden. Zusätzlich wird Code, der bindparam() innerhalb einer Insert- oder Update-Anweisung verwendet und darauf vertraut, dass das bindparam() nicht entsprechend der zugewiesenen Spalte typisiert ist, auf diese Weise nicht mehr funktionieren.

#2850

Spalten können ihren Typ zuverlässig von einer Spalte erhalten, auf die über einen Fremdschlüssel verwiesen wird

Es gibt ein seit langem bestehendes Verhalten, das besagt, dass eine Column ohne Typ deklariert werden kann, solange diese Column von einer ForeignKeyConstraint referenziert wird und der Typ der referenzierten Spalte in diese kopiert wird. Das Problem war, dass diese Funktion nie sehr gut funktionierte und nicht gepflegt wurde. Das Kernproblem war, dass das ForeignKey-Objekt nicht weiß, auf welche Ziel-Column es sich bezieht, bis es dazu aufgefordert wird, typischerweise beim ersten Mal, wenn der Fremdschlüssel zur Konstruktion eines Join verwendet wird. Bis dahin hatte die übergeordnete Column keinen Typ, oder genauer gesagt, sie hatte den Standardtyp NullType.

Obwohl es lange gedauert hat, wurde die Arbeit zur Reorganisation der Initialisierung von ForeignKey-Objekten abgeschlossen, so dass diese Funktion endlich akzeptabel funktioniert. Im Kern der Änderung steht, dass das Attribut ForeignKey.column die Position der Ziel-Column nicht mehr verzögert initialisiert; das Problem bei diesem System war, dass die zugehörige Column mit NullType als Typ feststeckte, bis der ForeignKey zufällig verwendet wurde.

In der neuen Version koordiniert der ForeignKey mit der späteren Column, auf die er sich beziehen wird, mithilfe interner Anhangsereignisse, so dass in dem Moment, in dem die referenzierende Column mit der MetaData verknüpft wird, alle ForeignKey-Objekte, die sich darauf beziehen, eine Nachricht erhalten, dass sie ihre übergeordnete Spalte initialisieren müssen. Dieses System ist komplizierter, funktioniert aber solider; als Bonus gibt es nun Tests für eine Vielzahl von Column / ForeignKey-Konfigurationsszenarien und die Fehlermeldungen wurden verbessert, um sehr spezifisch für nicht weniger als sieben verschiedene Fehlerbedingungen zu sein.

Szenarien, die nun korrekt funktionieren, umfassen

  1. Der Typ einer Column ist sofort vorhanden, sobald die Ziel-Column mit derselben MetaData verknüpft wird; dies funktioniert unabhängig davon, welche Seite zuerst konfiguriert wird.

    >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKey
    >>> metadata = MetaData()
    >>> t2 = Table("t2", metadata, Column("t1id", ForeignKey("t1.id")))
    >>> t2.c.t1id.type
    NullType()
    >>> t1 = Table("t1", metadata, Column("id", Integer, primary_key=True))
    >>> t2.c.t1id.type
    Integer()
  2. Das System funktioniert nun auch mit ForeignKeyConstraint

    >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKeyConstraint
    >>> metadata = MetaData()
    >>> t2 = Table(
    ...     "t2",
    ...     metadata,
    ...     Column("t1a"),
    ...     Column("t1b"),
    ...     ForeignKeyConstraint(["t1a", "t1b"], ["t1.a", "t1.b"]),
    ... )
    >>> t2.c.t1a.type
    NullType()
    >>> t2.c.t1b.type
    NullType()
    >>> t1 = Table(
    ...     "t1",
    ...     metadata,
    ...     Column("a", Integer, primary_key=True),
    ...     Column("b", Integer, primary_key=True),
    ... )
    >>> t2.c.t1a.type
    Integer()
    >>> t2.c.t1b.type
    Integer()
  3. Es funktioniert sogar für "Mehrfachsprünge" - das heißt, ein ForeignKey, der sich auf eine Column bezieht, die sich auf eine andere Column bezieht

    >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKey
    >>> metadata = MetaData()
    >>> t2 = Table("t2", metadata, Column("t1id", ForeignKey("t1.id")))
    >>> t3 = Table("t3", metadata, Column("t2t1id", ForeignKey("t2.t1id")))
    >>> t2.c.t1id.type
    NullType()
    >>> t3.c.t2t1id.type
    NullType()
    >>> t1 = Table("t1", metadata, Column("id", Integer, primary_key=True))
    >>> t2.c.t1id.type
    Integer()
    >>> t3.c.t2t1id.type
    Integer()

#1765

Dialekt-Änderungen

Firebird fdb ist nun der Standard-Firebird-Dialekt.

Der fdb-Dialekt wird nun verwendet, wenn eine Engine ohne Dialekt-Spezifizierer erstellt wird, z. B. firebird://. fdb ist ein kinterbasdb-kompatibler DBAPI, der laut dem Firebird-Projekt nun deren offizieller Python-Treiber ist.

#2504

Firebird fdb und kinterbasdb setzen standardmäßig retaining=False

Sowohl die DBAPIs fdb als auch kinterbasdb unterstützen ein Flag retaining=True, das an die Methoden commit() und rollback() ihrer Verbindung übergeben werden kann. Die dokumentierte Begründung für dieses Flag ist, dass der DBAPI interne Transaktionszustände für nachfolgende Transaktionen wiederverwenden kann, um die Leistung zu verbessern. Neuere Dokumentationen verweisen jedoch auf Analysen der Firebird-"Garbage Collection", die besagen, dass dieses Flag die Fähigkeit der Datenbank, Bereinigungsaufgaben zu verarbeiten, negativ beeinflussen kann und Berichten zufolge zu einer *Leistungsminderung* führt.

Es ist unklar, wie dieses Flag angesichts dieser Informationen tatsächlich nutzbar ist, und da es sich nur um ein leistungssteigerndes Merkmal zu handeln scheint, steht es nun standardmäßig auf False. Der Wert kann durch Übergabe des Flags retaining=True an den Aufruf von create_engine() gesteuert werden. Dies ist ein neues Flag, das ab 0.8.2 hinzugefügt wurde, sodass Anwendungen ab 0.8.2 dieses nach Wunsch auf True oder False setzen können.

Siehe auch

sqlalchemy.dialects.firebird.fdb

sqlalchemy.dialects.firebird.kinterbasdb

https://pythonhosted.org/fdb/usage-guide.html#retaining-transactions - Informationen zum "retaining"-Flag.

#2763