Was gibt's Neues in SQLAlchemy 1.0?

Über dieses Dokument

Dieses Dokument beschreibt die Änderungen zwischen SQLAlchemy Version 0.9, die im Mai 2014 unter Wartungsreleases lief, und SQLAlchemy Version 1.0, die im April 2015 veröffentlicht wurde.

Dokument zuletzt aktualisiert: 9. Juni 2015

Einleitung

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

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

Neue Features und Verbesserungen - ORM

Neue Session Bulk INSERT/UPDATE API

Eine neue Reihe von Session-Methoden, die Hooks direkt in die Unit-of-Work-Einrichtung zur Ausgabe von INSERT- und UPDATE-Anweisungen bereitstellen, wurde erstellt. Bei korrekter Verwendung kann dieses Experiensystem ORM-Mappings verwenden, um Bulk-Insert- und Update-Anweisungen zu generieren, die in executemany-Gruppen gebündelt sind, wodurch die Anweisungen mit Geschwindigkeiten ausgeführt werden können, die mit der direkten Verwendung von Core konkurrieren.

Siehe auch

Bulk Operations - Einführung und vollständige Dokumentation

#3100

Neue Performance-Beispiel-Suite

Inspiriert von den Benchmarks für die Funktion Bulk Operations sowie für den Abschnitt How can I profile a SQLAlchemy powered application? im FAQ wurde ein neuer Beispielbereich hinzugefügt, der mehrere Skripte enthält, die dazu dienen, das relative Leistungsprofil verschiedener Core- und ORM-Techniken zu veranschaulichen. Die Skripte sind in Anwendungsfälle unterteilt und unter einer einzigen Konsolenoberfläche zusammengefasst, sodass jede Kombination von Demonstrationen ausgeführt werden kann, wobei Timings, Python-Profilergebnisse und/oder RunSnake-Profilergebnisse ausgegeben werden.

Siehe auch

Performance

„Baked“ Queries

Die „Baked“ Query-Funktion ist ein ungewöhnlicher neuer Ansatz, der die einfache Erstellung und Ausführung von Query-Objekten mit Caching ermöglicht, was bei aufeinanderfolgenden Aufrufen zu einer drastisch reduzierten Python-Funktionsaufruf-Overhead (über 75%) führt. Durch die Angabe eines Query-Objekts als eine Reihe von Lambdas, die nur einmal aufgerufen werden, wird eine Abfrage als vorkompilierte Einheit möglich.

from sqlalchemy.ext import baked
from sqlalchemy import bindparam

bakery = baked.bakery()


def search_for_user(session, username, email=None):
    baked_query = bakery(lambda session: session.query(User))
    baked_query += lambda q: q.filter(User.name == bindparam("username"))

    baked_query += lambda q: q.order_by(User.id)

    if email:
        baked_query += lambda q: q.filter(User.email == bindparam("email"))

    result = baked_query(session).params(username=username, email=email).all()

    return result

Siehe auch

Baked Queries

#3054

ORM-Vollobjekt-Abfragen 25 % schneller

Die Mechanismen des Moduls loading.py sowie die Identity Map wurden mehreren Durchläufen von Inline-Erstellung, Refactoring und Bereinigung unterzogen, sodass ein roher Abruf von Zeilen jetzt ORM-basierte Objekte etwa 25 % schneller befüllt. Bei einer Tabelle mit 1 Million Zeilen illustriert ein Skript wie das folgende den Typ des Ladevorgangs, der am meisten verbessert wurde.

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

Base = declarative_base()


class Foo(Base):
    __table__ = Table(
        "foo",
        Base.metadata,
        Column("id", Integer, primary_key=True),
        Column("a", Integer(), nullable=False),
        Column("b", Integer(), nullable=False),
        Column("c", Integer(), nullable=False),
    )


engine = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo=True)

sess = Session(engine)

now = time.time()

# avoid using all() so that we don't have the overhead of building
# a large list of full objects in memory
for obj in sess.query(Foo).yield_per(100).limit(1000000):
    pass

print("Total time: %d" % (time.time() - now))

Lokale MacBookPro-Ergebnisse zeigen einen Benchmark von 19 Sekunden für 0.9 auf 14 Sekunden für 1.0. Der Aufruf von Query.yield_per() ist immer eine gute Idee, wenn riesige Mengen an Zeilen gebündelt werden, da er verhindert, dass der Python-Interpreter auf einmal eine riesige Menge an Speicher für alle Objekte und deren Instrumentierung zuweisen muss. Ohne Query.yield_per() dauert das obige Skript auf dem MacBookPro 31 Sekunden unter 0.9 und 26 Sekunden unter 1.0, wobei die zusätzliche Zeit für die Einrichtung sehr großer Speicherpuffer aufgewendet wird.

Neue KeyedTuple-Implementierung dramatisch schneller

Wir haben uns die Implementierung von KeyedTuple angesehen, in der Hoffnung, Abfragen wie diese zu verbessern.

rows = sess.query(Foo.a, Foo.b, Foo.c).all()

Die Klasse KeyedTuple wird anstelle von Python collections.namedtuple() verwendet, da letzteres eine sehr komplexe Routine zur Typenerstellung hat, die langsamer als KeyedTuple performt. Beim Abrufen von Hunderttausenden von Zeilen übertrifft collections.namedtuple() jedoch schnell KeyedTuple, das mit steigender Instanzaufrufe dramatisch langsamer wird. Was tun? Ein neuer Typ, der zwischen den Ansätzen beider liegt. Beim Benchmarking aller drei Typen nach „Größe“ (Anzahl der zurückgegebenen Zeilen) und „num“ (Anzahl der verschiedenen Abfragen) übertrifft das neue „leichtgewichtige keyed tuple“ entweder beide oder liegt nur geringfügig hinter dem schnelleren Objekt zurück, abhängig vom Szenario. Im „Sweet Spot“, wo wir sowohl eine gute Anzahl neuer Typen erstellen als auch eine gute Anzahl von Zeilen abrufen, übertrifft das leichtgewichtige Objekt sowohl namedtuple als auch KeyedTuple.

-----------------
size=10 num=10000                 # few rows, lots of queries
namedtuple: 3.60302400589         # namedtuple falls over
keyedtuple: 0.255059957504        # KeyedTuple very fast
lw keyed tuple: 0.582715034485    # lw keyed trails right on KeyedTuple
-----------------
size=100 num=1000                 # <--- sweet spot
namedtuple: 0.365247011185
keyedtuple: 0.24896979332
lw keyed tuple: 0.0889317989349   # lw keyed blows both away!
-----------------
size=10000 num=100
namedtuple: 0.572599887848
keyedtuple: 2.54251694679
lw keyed tuple: 0.613876104355
-----------------
size=1000000 num=10               # few queries, lots of rows
namedtuple: 5.79669594765         # namedtuple very fast
keyedtuple: 28.856498003          # KeyedTuple falls over
lw keyed tuple: 6.74346804619     # lw keyed trails right on namedtuple

#3176

Signifikante Verbesserungen im strukturellen Speicherverbrauch

Der strukturelle Speicherverbrauch wurde durch eine deutlich stärkere Nutzung von __slots__ für viele interne Objekte verbessert. Diese Optimierung ist besonders auf die Basisgröße von großen Anwendungen mit vielen Tabellen und Spalten ausgerichtet und reduziert die Speichergröße für eine Vielzahl von hochvolumigen Objekten, einschließlich interner Event-Listener, Komparator-Objekte und Teile des ORM-Attribut- und Loader-Strategie-Systems.

Ein Benchmark, der heapy verwendet, um die Startgröße von Nova zu messen, zeigt einen Unterschied von etwa 3,7 MB weniger oder 46 %, die von SQLAlchemy-Objekten, zugehörigen Wörterbüchern sowie Weakrefs innerhalb eines grundlegenden Imports von „nova.db.sqlalchemy.models“ eingenommen werden.

# reported by heapy, summation of SQLAlchemy objects +
# associated dicts + weakref-related objects with core of Nova imported:

    Before: total count 26477 total bytes 7975712
    After: total count 18181 total bytes 4236456

# reported for the Python module space overall with the
# core of Nova imported:

    Before: Partition of a set of 355558 objects. Total size = 61661760 bytes.
    After: Partition of a set of 346034 objects. Total size = 57808016 bytes.

UPDATE-Anweisungen werden jetzt mit executemany() in einem Flush gebündelt

UPDATE-Anweisungen können jetzt innerhalb eines ORM-Flushes in einen leistungsfähigeren executemany()-Aufruf gebündelt werden, ähnlich wie INSERT-Anweisungen gebündelt werden können; dies wird basierend auf den folgenden Kriterien im Flush aufgerufen.

  • zwei oder mehr aufeinanderfolgende UPDATE-Anweisungen betreffen dieselben zu ändernden Spalten.

  • Die Anweisung enthält keine eingebetteten SQL-Ausdrücke in der SET-Klausel.

  • Das Mapping verwendet keine mapper.version_id_col, oder das Backend-Dialekt unterstützt einen „sinnvollen“ Rowcount für eine executemany()-Operation; die meisten DBAPIs unterstützen dies jetzt korrekt.

Session.get_bind() behandelt eine breitere Palette von Vererbungsszenarien

Die Methode Session.get_bind() wird aufgerufen, wenn ein Query- oder Unit-of-Work-Flush-Prozess versucht, die Datenbank-Engine zu finden, die zu einer bestimmten Klasse gehört. Die Methode wurde verbessert, um eine Vielzahl von vererbungsbezogenen Szenarien zu behandeln, einschließlich

  • Binden an einen Mixin oder eine abstrakte Klasse

    class MyClass(SomeMixin, Base):
        __tablename__ = "my_table"
        # ...
    
    
    session = Session(binds={SomeMixin: some_engine})
  • Individuelles Binden an vererbte konkrete Unterklassen basierend auf der Tabelle

    class BaseClass(Base):
        __tablename__ = "base"
    
        # ...
    
    
    class ConcreteSubClass(BaseClass):
        __tablename__ = "concrete"
    
        # ...
    
        __mapper_args__ = {"concrete": True}
    
    
    session = Session(binds={base_table: some_engine, concrete_table: some_other_engine})

#3035

Session.get_bind() erhält den Mapper in allen relevanten Query-Fällen

Eine Reihe von Problemen wurden behoben, bei denen Session.get_bind() nicht den primären Mapper der Query erhielt, obwohl dieser Mapper leicht verfügbar war (der primäre Mapper ist der einzelne Mapper oder alternativ der erste Mapper, der mit einem Query-Objekt verbunden ist).

Das Mapper-Objekt, wenn es an Session.get_bind() übergeben wird, wird typischerweise von Sessions verwendet, die den Parameter Session.binds verwenden, um Mapper mit einer Reihe von Engines zu verknüpfen (obwohl in diesem Anwendungsfall die Dinge in den meisten Fällen oft „funktionieren“, da die Bindung über das zugeordnete Tabellenobjekt gefunden würde), oder spezifischer eine benutzerdefinierte Methode Session.get_bind() implementieren, die ein Muster der Auswahl von Engines basierend auf Mappern bietet, wie z. B. horizontal Sharding oder eine sogenannte „Routing“-Session, die Abfragen an verschiedene Backends weiterleitet.

Diese Szenarien umfassen

  • Query.count():

    session.query(User).count()
  • Query.update() und Query.delete(), sowohl für die UPDATE/DELETE-Anweisung als auch für die SELECT, die von der „fetch“-Strategie verwendet wird

    session.query(User).filter(User.id == 15).update(
        {"name": "foob"}, synchronize_session="fetch"
    )
    
    session.query(User).filter(User.id == 15).delete(synchronize_session="fetch")
  • Abfragen gegen einzelne Spalten

    session.query(User.id, User.name).all()
  • SQL-Funktionen und andere Ausdrücke gegen indirekte Zuordnungen wie column_property

    class User(Base):
        ...
    
        score = column_property(func.coalesce(self.tables.users.c.name, None))
    
    
    session.query(func.max(User.score)).scalar()

#3227 #3242 #1326

.info Dictionary-Verbesserungen

Die Sammlung InspectionAttr.info ist jetzt auf jeder Art von Objekt verfügbar, das man aus der Sammlung Mapper.all_orm_descriptors abrufen würde. Dies umfasst hybrid_property und association_proxy(). Da diese Objekte jedoch klassenbindende Deskriptoren sind, müssen sie **separat** von der Klasse, an die sie angehängt sind, aufgerufen werden, um auf das Attribut zuzugreifen. Unten wird dies anhand des Mapper.all_orm_descriptors-Namensraums veranschaulicht.

class SomeObject(Base):
    # ...

    @hybrid_property
    def some_prop(self):
        return self.value + 5


inspect(SomeObject).all_orm_descriptors.some_prop.info["foo"] = "bar"

Es ist auch als Konstruktorargument für alle SchemaItem-Objekte (z. B. ForeignKey, UniqueConstraint etc.) verfügbar und bleibt für ORM-Konstrukte wie synonym() erhalten.

#2971

#2963

ColumnProperty-Konstrukte funktionieren viel besser mit Aliasen, order_by

Eine Vielzahl von Problemen im Zusammenhang mit column_property() wurden behoben, insbesondere im Hinblick auf das aliased()-Konstrukt sowie die in 0.9 eingeführte Logik für „order by label“ (siehe Label-Konstrukte können jetzt als ihr alleiniger Name in einem ORDER BY gerendert werden).

Gegeben sei eine Zuordnung wie die folgende

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(ForeignKey("a.id"))


A.b = column_property(select([func.max(B.id)]).where(B.a_id == A.id).correlate(A))

Ein einfaches Szenario, das „A.b“ zweimal enthielt, würde nicht korrekt gerendert werden

print(sess.query(A, a1).order_by(a1.b))

Dies würde nach der falschen Spalte sortieren

SELECT a.id AS a_id, (SELECT max(b.id) AS max_1 FROM b
WHERE b.a_id = a.id) AS anon_1, a_1.id AS a_1_id,
(SELECT max(b.id) AS max_2
FROM b WHERE b.a_id = a_1.id) AS anon_2
FROM a, a AS a_1 ORDER BY anon_1

Neue Ausgabe

SELECT a.id AS a_id, (SELECT max(b.id) AS max_1
FROM b WHERE b.a_id = a.id) AS anon_1, a_1.id AS a_1_id,
(SELECT max(b.id) AS max_2
FROM b WHERE b.a_id = a_1.id) AS anon_2
FROM a, a AS a_1 ORDER BY anon_2

Es gab auch viele Szenarien, in denen die „order by“-Logik das Sortieren nach Label nicht korrekt ausführte, z. B. wenn die Zuordnung „polymorph“ war.

class A(Base):
    __tablename__ = "a"

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

    __mapper_args__ = {"polymorphic_on": type, "with_polymorphic": "*"}

Die order_by würde das Label nicht verwenden, da es aufgrund des polymorphen Ladens anonymisiert würde

SELECT a.id AS a_id, a.type AS a_type, (SELECT max(b.id) AS max_1
FROM b WHERE b.a_id = a.id) AS anon_1
FROM a ORDER BY (SELECT max(b.id) AS max_2
FROM b WHERE b.a_id = a.id)

Da nun die order by label das anonymisierte Label verfolgt, funktioniert dies jetzt

SELECT a.id AS a_id, a.type AS a_type, (SELECT max(b.id) AS max_1
FROM b WHERE b.a_id = a.id) AS anon_1
FROM a ORDER BY anon_1

In diesen Korrekturen sind eine Vielzahl von Heisenbugs enthalten, die den Zustand eines aliased()-Konstrukts beschädigen konnten, sodass die Label-Logik erneut fehlschlagen würde; diese wurden ebenfalls behoben.

#3148 #3188

Neue Features und Verbesserungen - Core

Select/Query LIMIT / OFFSET können als beliebiger SQL-Ausdruck angegeben werden

Die Methoden Select.limit() und Select.offset() akzeptieren jetzt zusätzlich zu Ganzzahlwerten beliebige SQL-Ausdrücke als Argumente. Das ORM Query-Objekt leitet auch jeden Ausdruck an das zugrunde liegende Select-Objekt weiter. Typischerweise wird dies verwendet, um die Übergabe eines gebundenen Parameters zu ermöglichen, der später durch einen Wert ersetzt werden kann.

sel = select([table]).limit(bindparam("mylimit")).offset(bindparam("myoffset"))

Dialekte, die keine nicht-ganzzahligen LIMIT- oder OFFSET-Ausdrücke unterstützen, unterstützen dieses Verhalten möglicherweise weiterhin nicht; Drittanbieter-Dialekte müssen möglicherweise ebenfalls modifiziert werden, um das neue Verhalten zu nutzen. Ein Dialekt, der derzeit die Attribute ._limit oder ._offset verwendet, funktioniert weiterhin für Fälle, in denen das Limit/Offset als einfache Ganzzahl angegeben wurde. Wenn jedoch ein SQL-Ausdruck angegeben wird, lösen diese beiden Attribute stattdessen eine CompileError beim Zugriff aus. Ein Drittanbieter-Dialekt, der das neue Feature unterstützen möchte, sollte jetzt die Attribute ._limit_clause und ._offset_clause aufrufen, um den vollständigen SQL-Ausdruck anstelle des Ganzzahlwerts zu erhalten.

Das use_alter Flag bei ForeignKeyConstraint wird (normalerweise) nicht mehr benötigt

Die Methoden MetaData.create_all() und MetaData.drop_all() verwenden nun ein System, das automatisch eine ALTER-Anweisung für Fremdschlüssel-Constraints rendert, die in sich gegenseitig abhängigen Zyklen zwischen Tabellen beteiligt sind, ohne dass ForeignKeyConstraint.use_alter angegeben werden muss. Darüber hinaus müssen die Fremdschlüssel-Constraints keinen Namen mehr haben, um per ALTER erstellt zu werden; nur die DROP-Operation erfordert einen Namen. Im Falle eines DROP stellt die Funktion sicher, dass nur Constraints mit expliziten Namen als ALTER-Anweisungen einbezogen werden. Im Falle eines unauflösbaren Zyklus innerhalb eines DROP gibt das System nun eine prägnante und klare Fehlermeldung aus, wenn der DROP nicht fortgesetzt werden kann.

Die Flags ForeignKeyConstraint.use_alter und ForeignKey.use_alter bleiben bestehen und haben weiterhin die gleiche Auswirkung, nämlich die Festlegung, für welche Constraints während eines CREATE/DROP-Szenarios ein ALTER erforderlich ist.

Ab Version 1.0.1 übernimmt eine spezielle Logik im Fall von SQLite, das ALTER nicht unterstützt, wenn während eines DROP die gegebenen Tabellen einen unauflösbaren Zyklus haben; in diesem Fall wird eine Warnung ausgegeben und die Tabellen werden **ohne** Sortierung gelöscht, was auf SQLite normalerweise in Ordnung ist, es sei denn, Constraints sind aktiviert. Um die Warnung zu beheben und mit mindestens einer teilweisen Sortierung auf einer SQLite-Datenbank fortzufahren, insbesondere wenn Constraints aktiviert sind, wenden Sie die „use_alter“-Flags erneut auf die ForeignKey- und ForeignKeyConstraint-Objekte an, die explizit von der Sortierung ausgeschlossen werden sollen.

Siehe auch

Erstellen/Löschen von Fremdschlüssel-Constraints mit ALTER - vollständige Beschreibung des neuen Verhaltens.

#3282

ResultProxy „automatisches Schließen“ ist jetzt ein „weiches“ Schließen

Seit vielen Versionen wurde das ResultProxy-Objekt automatisch geschlossen, sobald alle Ergebniszeilen abgerufen wurden. Dies diente dazu, die Nutzung des Objekts zu ermöglichen, ohne ResultProxy.close() explizit aufrufen zu müssen; da alle DBAPI-Ressourcen freigegeben waren, war das Objekt sicher zu verwerfen. Das Objekt behielt jedoch ein strenges „geschlossenes“ Verhalten bei, was bedeutete, dass jede nachfolgende Aufrufe von ResultProxy.fetchone(), ResultProxy.fetchmany() oder ResultProxy.fetchall() nun eine ResourceClosedError auslösen würden.

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
exception: ResourceClosedError

Dieses Verhalten widerspricht dem, was pep-249 besagt, nämlich dass die Abrufmethode auch nach Erschöpfung der Ergebnisse wiederholt aufgerufen werden können. Es stört auch das Verhalten einiger Implementierungen von ResultProxy, wie z. B. BufferedColumnResultProxy, das vom cx_oracle-Dialekt für bestimmte Datentypen verwendet wird.

Um dies zu lösen, wurde der „geschlossene“ Zustand des ResultProxy in zwei Zustände unterteilt: einen „Soft-Close“, der den Großteil dessen tut, was „Close“ tut, indem er den DBAPI-Cursor freigibt und im Falle eines „Close with Result“-Objekts auch die Verbindung freigibt, und einen „Closed“-Zustand, der alles umfasst, was „Soft Close“ tut, sowie die Abrufmethode als „Closed“ etabliert. Die Methode ResultProxy.close() wird nun nie mehr implizit aufgerufen, sondern nur noch die Methode ResultProxy._soft_close(), die nicht öffentlich ist.

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
None  # still None
>>> result.fetchall()
[]
>>> result.close()
>>> result.fetchone()
exception: ResourceClosedError  # *now* it raises

#3330 #3329

CHECK Constraints unterstützen nun das Token %(column_0_name)s in Namenskonventionen

Der Token %(column_0_name)s wird aus der ersten Spalte abgeleitet, die im Ausdruck eines CheckConstraint gefunden wird.

metadata = MetaData(naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"})

foo = Table("foo", metadata, Column("value", Integer))

CheckConstraint(foo.c.value > 5)

Wird gerendert

CREATE TABLE foo (
    value INTEGER,
    CONSTRAINT ck_foo_value CHECK (value > 5)
)

Die Kombination von Namenskonventionen mit dem durch einen SchemaType wie Boolean oder Enum erzeugten Constraint nutzt nun ebenfalls alle CHECK-Constraint-Konventionen.

#3299

Constraints, die sich auf nicht angehängte Spalten beziehen, können sich automatisch an die Tabelle anhängen, wenn ihre referenzierten Spalten angehängt werden

Seit mindestens Version 0.8 hat ein Constraint die Möglichkeit, sich selbst an eine Table „anzuhängen“, basierend auf der Übergabe von an eine Tabelle angehängten Spalten.

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

m = MetaData()

t = Table("t", m, Column("a", Integer), Column("b", Integer))

uq = UniqueConstraint(t.c.a, t.c.b)  # will auto-attach to Table

assert uq in t.constraints

Um einige Fälle zu unterstützen, die bei deklarativen Ansätzen häufig auftreten, kann dieselbe Auto-Attachment-Logik nun auch dann funktionieren, wenn die Column-Objekte noch nicht mit der Table verknüpft sind; zusätzliche Ereignisse werden eingerichtet, so dass, wenn diese Column-Objekte verknüpft werden, auch der Constraint hinzugefügt wird.

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

m = MetaData()

a = Column("a", Integer)
b = Column("b", Integer)

uq = UniqueConstraint(a, b)

t = Table("t", m, a, b)

assert uq in t.constraints  # constraint auto-attached

Das obige Feature wurde als späte Ergänzung ab Version 1.0.0b3 hinzugefügt. Ein Fix ab Version 1.0.4 für #3411 stellt sicher, dass diese Logik nicht auftritt, wenn der Constraint auf eine Mischung aus Column-Objekten und Zeichenketten-Spaltennamen verweist; da wir noch keine Verfolgung der Hinzufügung von Namen zu einer Table haben.

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

m = MetaData()

a = Column("a", Integer)
b = Column("b", Integer)

uq = UniqueConstraint(a, "b")

t = Table("t", m, a, b)

# constraint *not* auto-attached, as we do not have tracking
# to locate when a name 'b' becomes available on the table
assert uq not in t.constraints

Oben löst das Anhangsereignis für die Spalte „a“ an die Tabelle „t“ aus, bevor die Spalte „b“ angehängt wird (da „a“ im Table-Konstruktor vor „b“ aufgeführt ist) und der Constraint kann „b“ nicht finden, wenn er versuchen würde, sich anzuhängen. Zur Konsistenz, wenn der Constraint auf Zeichenkettennamen verweist, wird die Auto-Attach-on-Column-Attach-Logik übersprungen.

Die ursprüngliche Auto-Attach-Logik bleibt natürlich bestehen, wenn die Table zur Zeit der Konstruktion des Constraint bereits alle Ziel-Column-Objekte enthält.

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

m = MetaData()

a = Column("a", Integer)
b = Column("b", Integer)


t = Table("t", m, a, b)

uq = UniqueConstraint(a, "b")

# constraint auto-attached normally as in older versions
assert uq in t.constraints

#3341 #3411

INSERT FROM SELECT beinhaltet nun Python- und SQL-Ausdruck-Defaults

Insert.from_select() beinhaltet nun Python- und SQL-Ausdruck-Defaults, falls diese sonst nicht spezifiziert sind; die Einschränkung, dass Nicht-Server-Spalten-Defaults nicht in ein INSERT FROM SELECT aufgenommen werden, ist nun aufgehoben und diese Ausdrücke werden als Konstanten in die SELECT-Anweisung gerendert.

from sqlalchemy import Table, Column, MetaData, Integer, select, func

m = MetaData()

t = Table(
    "t", m, Column("x", Integer), Column("y", Integer, default=func.somefunction())
)

stmt = select([t.c.x])
print(t.insert().from_select(["x"], stmt))

Wird gerendert

INSERT INTO t (x, y) SELECT t.x, somefunction() AS somefunction_1
FROM t

Das Feature kann mit Insert.from_select.include_defaults deaktiviert werden.

Spalten-Server-Defaults rendern nun Literalwerte

Das Compiler-Flag „literal binds“ wird aktiviert, wenn eine DefaultClause, die durch Column.server_default eingerichtet wurde, als zu kompilierender SQL-Ausdruck vorhanden ist. Dies ermöglicht es, Literale, die in SQL eingebettet sind, korrekt zu rendern, wie z. B.

from sqlalchemy import Table, Column, MetaData, Text
from sqlalchemy.schema import CreateTable
from sqlalchemy.dialects.postgresql import ARRAY, array
from sqlalchemy.dialects import postgresql

metadata = MetaData()

tbl = Table(
    "derp",
    metadata,
    Column("arr", ARRAY(Text), server_default=array(["foo", "bar", "baz"])),
)

print(CreateTable(tbl).compile(dialect=postgresql.dialect()))

Rendert nun

CREATE TABLE derp (
    arr TEXT[] DEFAULT ARRAY['foo', 'bar', 'baz']
)

Zuvor würden die Literalwerte "foo", "bar", "baz" als gebundene Parameter gerendert, was in DDL nutzlos ist.

#3087

UniqueConstraint ist nun Teil des Tabellenreflexionsprozesses

Ein Table-Objekt, das mit autoload=True befüllt wurde, enthält nun UniqueConstraint-Konstrukte sowie Index-Konstrukte. Diese Logik hat einige Vorbehalte für PostgreSQL und MySQL.

PostgreSQL

PostgreSQL hat das Verhalten, dass bei der Erstellung eines UNIQUE-Constraints implizit auch ein UNIQUE INDEX erstellt wird, der diesem Constraint entspricht. Die Methoden Inspector.get_indexes() und Inspector.get_unique_constraints() geben diese Einträge weiterhin **beide** getrennt zurück, wobei Inspector.get_indexes() nun ein Token duplicates_constraint im Indexeintrag enthält, der den entsprechenden Constraint angibt, wenn er erkannt wird. Bei der vollständigen Tabellenreflexion mit Table(..., autoload=True) wird das Index-Konstrukt als mit dem UniqueConstraint verbunden erkannt und ist **nicht** in der Table.indexes-Sammlung vorhanden; nur der UniqueConstraint ist in der Table.constraints-Sammlung vorhanden. Diese Deduplizierungslogik funktioniert durch eine Verknüpfung mit der pg_constraint-Tabelle beim Abfragen von pg_index, um zu sehen, ob die beiden Konstrukte verknüpft sind.

MySQL

MySQL hat keine getrennten Konzepte für einen UNIQUE INDEX und einen UNIQUE Constraint. Während es beide Syntaxen beim Erstellen von Tabellen und Indizes unterstützt, speichert es sie nicht anders ab. Die Methoden Inspector.get_indexes() und Inspector.get_unique_constraints() geben weiterhin **beide** einen Eintrag für einen UNIQUE Index in MySQL zurück, wobei Inspector.get_unique_constraints() ein neues Token duplicates_index im Constraint-Eintrag enthält, das angibt, dass dies ein Duplikat-Eintrag ist, der dem Index entspricht. Bei der vollständigen Tabellenreflexion mit Table(..., autoload=True) ist das UniqueConstraint-Konstrukt jedoch unter keinen Umständen Teil der vollständig reflektierten Table-Konstruktion; dieses Konstrukt wird immer durch einen Index mit der Einstellung unique=True in der Table.indexes-Sammlung dargestellt.

#3184

Neue Systeme zur sicheren Ausgabe von parametrisierten Warnungen

Lange Zeit gab es die Einschränkung, dass Warnmeldungen sich nicht auf Datenelemente beziehen konnten, so dass eine bestimmte Funktion eine unendliche Anzahl eindeutiger Warnungen ausgeben konnte. Der Hauptort, an dem dies auftritt, ist die Warnung Unicode type received non-unicode bind param value. Das Einfügen des Datenwerts in diese Nachricht hätte bedeutet, dass das Python-Modul __warningregistry__ oder in einigen Fällen das Python-globale warnings.onceregistry unbegrenzt anwachsen würde, da in den meisten Warnszenarien eine dieser beiden Sammlungen mit jeder eindeutigen Warnmeldung gefüllt wird.

Die Änderung besteht darin, dass wir durch die Verwendung eines speziellen string-Typs, der das Hashing der Zeichenkette absichtlich verändert, steuern können, dass eine große Anzahl von parametrisierten Nachrichten nur auf eine kleine Anzahl möglicher Hash-Werte gehasht wird, so dass eine Warnung wie Unicode type received non-unicode bind param value so angepasst werden kann, dass sie nur eine bestimmte Anzahl von Malen ausgegeben wird; danach beginnt die Python-Warnungsregistrierung, sie als Duplikate zu erfassen.

Zur Veranschaulichung zeigt das folgende Testskript nur zehn Warnungen für zehn der Parameter-Sets von insgesamt 1000 an.

from sqlalchemy import create_engine, Unicode, select, cast
import random
import warnings

e = create_engine("sqlite://")

# Use the "once" filter (which is also the default for Python
# warnings).  Exactly ten of these warnings will
# be emitted; beyond that, the Python warnings registry will accumulate
# new values as dupes of one of the ten existing.
warnings.filterwarnings("once")

for i in range(1000):
    e.execute(
        select([cast(("foo_%d" % random.randint(0, 1000000)).encode("ascii"), Unicode)])
    )

Das Format der Warnung hier ist

/path/lib/sqlalchemy/sql/sqltypes.py:186: SAWarning: Unicode type received
  non-unicode bind param value 'foo_4852'. (this warning may be
  suppressed after 10 occurrences)

#3178

Wichtige Verhaltensänderungen – ORM

query.update() löst nun Zeichenkettennamen in zugeordnete Attributnamen auf

Die Dokumentation für Query.update() besagt, dass das gegebene values-Dictionary „ein Dictionary mit Attributnamen als Schlüsseln“ ist, was impliziert, dass es sich um zugeordnete Attributnamen handelt. Leider wurde die Funktion eher mit Blick auf die Aufnahme von Attributen und SQL-Ausdrücken entwickelt und nicht so sehr auf Zeichenketten; wenn Zeichenketten übergeben wurden, wurden diese Zeichenketten direkt an die Kern-Update-Anweisung weitergegeben, ohne eine Auflösung dahingehend, wie diese Namen auf der zugeordneten Klasse repräsentiert werden. Das bedeutete, dass der Name exakt dem einer Tabellenspalte entsprechen musste und nicht, wie ein Attribut dieses Namens auf der Klasse zugeordnet wurde.

Die Zeichenkettennamen werden nun ernsthaft als Attributnamen aufgelöst.

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column("user_name", String(50))

Oben ist die Spalte user_name als name zugeordnet. Zuvor hätte ein Aufruf von Query.update(), dem Zeichenketten übergeben wurden, wie folgt aufgerufen werden müssen:

session.query(User).update({"user_name": "moonbeam"})

Der gegebene String wird nun gegen die Entität aufgelöst.

session.query(User).update({"name": "moonbeam"})

Normalerweise ist es vorzuziehen, das Attribut direkt zu verwenden, um Mehrdeutigkeiten zu vermeiden.

session.query(User).update({User.name: "moonbeam"})

Die Änderung zeigt auch an, dass Synoyme und hybride Attribute ebenfalls anhand ihres Zeichenkettennamens referenziert werden können.

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column("user_name", String(50))

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


session.query(User).update({"fullname": "moonbeam"})

#3228

Warnungen bei Vergleichen von Objekten mit None-Werten zu Beziehungen

Diese Änderung ist neu ab 1.0.1. Einige Benutzer führen Abfragen durch, die im Wesentlichen die folgende Form haben:

session.query(Address).filter(Address.user == User(id=None))

Dieses Muster wird in SQLAlchemy derzeit nicht unterstützt. Für alle Versionen wird SQL generiert, das etwa so aussieht:

SELECT address.id AS address_id, address.user_id AS address_user_id,
address.email_address AS address_email_address
FROM address WHERE ? = address.user_id
(None,)

Beachten Sie oben den Vergleich WHERE ? = address.user_id, wobei der gebundene Wert ? None oder in SQL NULL erhält. **Dies gibt in SQL immer False zurück**. Der Vergleich würde theoretisch wie folgt SQL generieren:

SELECT address.id AS address_id, address.user_id AS address_user_id,
address.email_address AS address_email_address
FROM address WHERE address.user_id IS NULL

Aber im Moment **tut es das nicht**. Anwendungen, die darauf angewiesen sind, dass „NULL = NULL“ in allen Fällen False ergibt, laufen Gefahr, dass SQLAlchemy dieses Problem eines Tages behebt, um „IS NULL“ zu generieren, und die Abfragen dann unterschiedliche Ergebnisse liefern. Daher erhalten Sie bei dieser Art von Operation eine Warnung:

SAWarning: Got None for value of column user.id; this is unsupported
for a relationship comparison and will not currently produce an
IS comparison (but may in a future release)

Beachten Sie, dass dieses Muster ab Version 1.0.0, einschließlich aller Betas, in den meisten Fällen fehlerhaft war; ein Wert wie SYMBOL('NEVER_SET') würde generiert. Dieses Problem wurde behoben, aber als Ergebnis der Identifizierung dieses Musters gibt es nun die Warnung, damit wir dieses fehlerhafte Verhalten (jetzt erfasst in #3373) in einer zukünftigen Version sicherer reparieren können.

#3371

Ein „negierter Enthält-oder-Gleich“-Beziehungsvergleich verwendet den aktuellen Wert von Attributen, nicht den Datenbankwert

Diese Änderung ist neu ab 1.0.1; obwohl wir uns gewünscht hätten, dass sie in 1.0.0 enthalten ist, wurde sie erst als Ergebnis von #3371 deutlich.

Gegeben eine Zuordnung

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(ForeignKey("a.id"))
    a = relationship("A")

Gegeben A mit Primärschlüssel 7, das wir aber ohne Flush auf 10 geändert haben

s = Session(autoflush=False)
a1 = A(id=7)
s.add(a1)
s.commit()

a1.id = 10

Eine Abfrage gegen eine Many-to-One-Beziehung mit diesem Objekt als Ziel verwendet den Wert 10 in den gebundenen Parametern.

s.query(B).filter(B.a == a1)

Erzeugt

SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE ? = b.a_id
(10,)

Vor dieser Änderung würde die Negation dieses Kriteriums jedoch **nicht** 10 verwenden, sondern 7, es sei denn, das Objekt wurde zuerst geflusht.

s.query(B).filter(B.a != a1)

Erzeugt (in 0.9 und allen Versionen vor 1.0.1)

SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE b.a_id != ? OR b.a_id IS NULL
(7,)

Für ein transientes Objekt würde dies zu einer fehlerhaften Abfrage führen.

SELECT b.id, b.a_id
FROM b
WHERE b.a_id != :a_id_1 OR b.a_id IS NULL
-- {u'a_id_1': symbol('NEVER_SET')}

Diese Inkonsistenz wurde behoben, und in allen Abfragen wird nun der aktuelle Attributwert, in diesem Beispiel 10, verwendet.

#3374

Änderungen an Attribut-Ereignissen und anderen Operationen bezüglich Attribute ohne vorhandenen Wert

Bei dieser Änderung wird der Standardrückgabewert von None beim Zugriff auf ein Objekt dynamisch bei jedem Zugriff zurückgegeben, anstatt den Zustand des Attributs beim ersten Zugriff implizit mit einer speziellen „set“-Operation zu setzen. Das sichtbare Ergebnis dieser Änderung ist, dass obj.__dict__ nicht implizit bei einem get geändert wird, und es gibt auch einige geringfügige Verhaltensänderungen für get_history() und verwandte Funktionen.

Gegeben ein Objekt ohne Zustand

>>> obj = Foo()

Es war schon immer das Verhalten von SQLAlchemy, dass, wenn wir auf ein Skalar- oder Many-to-One-Attribut zugreifen, das nie gesetzt wurde, es als None zurückgegeben wird.

>>> obj.someattr
None

Dieser Wert von None ist tatsächlich Teil des Zustands von obj und ist nicht unähnlich, als hätten wir das Attribut explizit gesetzt, z. B. obj.someattr = None. Das „Setzen beim Abrufen“ würde hier jedoch anders aussehen, was die Historie und Ereignisse betrifft. Es würde kein Attribut-Ereignis auslösen, und zusätzlich sehen wir, wenn wir die Historie betrachten:

>>> inspect(obj).attrs.someattr.history
History(added=(), unchanged=[None], deleted=())   # 0.9 and below

Das heißt, es ist, als wäre das Attribut immer None gewesen und nie geändert worden. Dies ist ausdrücklich anders, als wenn wir das Attribut stattdessen zuerst gesetzt hätten.

>>> obj = Foo()
>>> obj.someattr = None
>>> inspect(obj).attrs.someattr.history
History(added=[None], unchanged=(), deleted=())  # all versions

Dies bedeutet, dass das Verhalten unserer „set“-Operation durch die Tatsache korrumpiert werden kann, dass der Wert zuvor über „get“ abgerufen wurde. In 1.0 wurde diese Inkonsistenz behoben, indem nichts mehr tatsächlich gesetzt wird, wenn der Standard-„Getter“ verwendet wird.

>>> obj = Foo()
>>> obj.someattr
None
>>> inspect(obj).attrs.someattr.history
History(added=(), unchanged=(), deleted=())  # 1.0
>>> obj.someattr = None
>>> inspect(obj).attrs.someattr.history
History(added=[None], unchanged=(), deleted=())

Der Grund, warum das obige Verhalten noch keine großen Auswirkungen hatte, ist, dass die INSERT-Anweisung in relationalen Datenbanken einen fehlenden Wert in den meisten Fällen als gleich NULL betrachtet. Ob SQLAlchemy ein Historienereignis für ein bestimmtes Attribut erhalten hat, das auf None gesetzt wurde oder nicht, wäre normalerweise nicht wichtig; da der Unterschied zwischen dem Senden von None/NULL oder nicht keine Auswirkungen hätte. Wie jedoch #3060 (hier beschrieben in Die Priorität von Attributänderungen bei beziehungsgebundenen Attributen gegenüber FK-gebundenen Attributen kann sich ändern) zeigt, gibt es einige seltene Randfälle, in denen wir tatsächlich None gesetzt haben wollen. Außerdem ist es durch die Zulassung des Attribut-Ereignisses nun möglich, „Standardwert“-Funktionen für ORM-zuordnete Attribute zu erstellen.

Als Teil dieser Änderung ist die Erzeugung des impliziten „None“ nun für andere Situationen deaktiviert, in denen dies früher geschah; dies beinhaltet, wenn eine Attribut-Set-Operation für eine Many-to-One-Beziehung empfangen wird; zuvor war der „alte“ Wert „None“, wenn er nicht anderweitig gesetzt war; nun wird der Wert NEVER_SET gesendet, ein Wert, der nun an einen Attribut-Listener gesendet werden kann. Dieses Symbol kann auch beim Aufruf von Mapper-Hilfsfunktionen wie Mapper.primary_key_from_instance() empfangen werden; wenn die Primärschlüsselattribute gar keine Einstellung haben, während der Wert zuvor None war, ist es nun das Symbol NEVER_SET und es findet keine Zustandsänderung am Objekt statt.

#3061

Die Priorität von Attributänderungen bei beziehungsgebundenen Attributen gegenüber FK-gebundenen Attributen kann sich ändern

Als Nebenwirkung von #3060 ist das Setzen eines beziehungsgebundenen Attributs auf None nun ein nachverfolgbares Historienereignis, das die Absicht widerspiegelt, None für dieses Attribut zu speichern. Da es schon immer der Fall war, dass das Setzen eines beziehungsgebundenen Attributs die direkte Zuweisung an die Fremdschlüsselattribute überschreibt, kann hier eine Verhaltensänderung bei der Zuweisung von None beobachtet werden. Gegeben eine Zuordnung

class A(Base):
    __tablename__ = "table_a"

    id = Column(Integer, primary_key=True)


class B(Base):
    __tablename__ = "table_b"

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

In 1.0 hat das beziehungsgebundene Attribut Vorrang vor dem FK-gebundenen Attribut in allen Fällen, unabhängig davon, ob der zugewiesene Wert eine Referenz auf ein A-Objekt oder None ist. In 0.9 ist das Verhalten inkonsistent und tritt nur in Kraft, wenn ein Wert zugewiesen wird; das None wird nicht berücksichtigt.

a1 = A(id=1)
a2 = A(id=2)
session.add_all([a1, a2])
session.flush()

b1 = B()
b1.a = a1  # we expect a_id to be '1'; takes precedence in 0.9 and 1.0

b2 = B()
b2.a = None  # we expect a_id to be None; takes precedence only in 1.0

b1.a_id = 2
b2.a_id = 2

session.add_all([b1, b2])
session.commit()

assert b1.a is a1  # passes in both 0.9 and 1.0
assert b2.a is None  # passes in 1.0, in 0.9 it's a2

#3060

session.expunge() löst ein gelöschtes Objekt vollständig ab

Das Verhalten von Session.expunge() hatte einen Fehler, der zu einer Inkonsistenz im Verhalten bezüglich gelöschter Objekte führte. Die Funktion object_session() sowie das Attribut InstanceState.session würden das Objekt auch nach dem Expunge noch als zur Session gehörig melden.

u1 = sess.query(User).first()
sess.delete(u1)

sess.flush()

assert u1 not in sess
assert inspect(u1).session is sess  # this is normal before commit

sess.expunge(u1)

assert u1 not in sess
assert inspect(u1).session is None  # would fail

Beachten Sie, dass es normal ist, dass u1 not in sess True ist, während inspect(u1).session immer noch auf die Sitzung verweist, während die Transaktion nach dem Löschen fortgesetzt wird und Session.expunge() noch nicht aufgerufen wurde; die vollständige Trennung normalisiert sich normalerweise, sobald die Transaktion committet ist. Dieses Problem würde auch Funktionen beeinträchtigen, die auf Session.expunge() angewiesen sind, wie z. B. make_transient().

#3139

Joined/Subquery Eager Loading explizit mit yield_per verboten

Um die Methode Query.yield_per() einfacher nutzbar zu machen, wird eine Ausnahme ausgelöst, wenn Subquery Eager Loader oder Joined Eager Loader, die Sammlungen verwenden würden, wirksam werden, wenn yield_per verwendet wird, da diese derzeit nicht mit yield-per kompatibel sind (Subquery Loading wäre theoretisch möglich). Wenn dieser Fehler ausgelöst wird, kann die Option lazyload() mit einem Sternchen gesendet werden.

q = sess.query(Object).options(lazyload("*")).yield_per(100)

oder verwenden Sie Query.enable_eagerloads()

q = sess.query(Object).enable_eagerloads(False).yield_per(100)

Die Option lazyload() hat den Vorteil, dass zusätzliche Many-to-One Joined Loader-Optionen weiterhin verwendet werden können.

q = (
    sess.query(Object)
    .options(lazyload("*"), joinedload("some_manytoone"))
    .yield_per(100)
)

Änderungen und Korrekturen bei der Handhabung doppelter Join-Ziele

Die hier vorgenommenen Änderungen umfassen Fehler, bei denen in einigen Szenarien unerwartetes und inkonsistentes Verhalten auftrat, wenn zweimal zu einer Entität oder zu mehreren Single-Table-Entitäten gegen dieselbe Tabelle gejoint wurde, ohne eine beziehungsbasierte ON-Klausel zu verwenden, sowie wenn mehrmals zu derselben Zielbeziehung gejoint wurde.

Beginnend mit einer Abbildung wie

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

Base = declarative_base()


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


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

Eine Abfrage, die zweimal zu A.bs joint

print(s.query(A).join(A.bs).join(A.bs))

Wird gerendert

SELECT a.id AS a_id
FROM a JOIN b ON a.id = b.a_id

Die Abfrage dedupliziert die redundanten A.bs, da sie versucht, einen Fall wie den folgenden zu unterstützen.

s.query(A).join(A.bs).filter(B.foo == "bar").reset_joinpoint().join(A.bs, B.cs).filter(
    C.bar == "bat"
)

Das heißt, A.bs ist Teil eines "Pfades". Im Rahmen von #3367 wird das zweimalige Erreichen desselben Endpunkts, ohne dass er Teil eines größeren Pfades ist, nun eine Warnung ausgeben.

SAWarning: Pathed join target A.bs has already been joined to; skipping

Die größere Änderung betrifft das Verknüpfen einer Entität ohne verwendung eines beziehungsgebundenen Pfades. Wenn wir zweimal zu B joinen

print(s.query(A).join(B, B.a_id == A.id).join(B, B.a_id == A.id))

In 0.9 würde dies wie folgt gerendert:

SELECT a.id AS a_id
FROM a JOIN b ON b.a_id = a.id JOIN b AS b_1 ON b_1.a_id = a.id

Dies ist problematisch, da die Aliassierung implizit ist und im Falle unterschiedlicher ON-Klauseln zu unvorhersehbaren Ergebnissen führen kann.

In 1.0 wird keine automatische Aliassierung angewendet, und wir erhalten

SELECT a.id AS a_id
FROM a JOIN b ON b.a_id = a.id JOIN b ON b.a_id = a.id

Dies wird einen Fehler von der Datenbank auslösen. Obwohl es schön wäre, wenn das "doppelte Join-Ziel" identisch behandelt würde, wenn wir beide aus redundanten Beziehungen gegenüber redundanten nicht-beziehungsbasierten Zielen joinen, ändern wir vorerst nur das Verhalten im schwerwiegenderen Fall, in dem zuvor implizit aliassiert wurde, und geben im Beziehungsfall nur eine Warnung aus. Letztendlich sollte das zweimalige Joinen desselben Dings ohne Aliassierung zur Disambiguierung in allen Fällen einen Fehler auslösen.

Die Änderung hat auch Auswirkungen auf Single-Table-Inheritance-Ziele. Verwendung einer Abbildung wie folgt

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

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

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

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


class ASub1(A):
    __mapper_args__ = {"polymorphic_identity": "asub1"}


class ASub2(A):
    __mapper_args__ = {"polymorphic_identity": "asub2"}


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)

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

    a = relationship("A", primaryjoin="B.a_id == A.id", backref="b")


s = Session()

print(s.query(ASub1).join(B, ASub1.b).join(ASub2, B.a))

print(s.query(ASub1).join(B, ASub1.b).join(ASub2, ASub2.id == B.a_id))

Die beiden Abfragen unten sind äquivalent und sollten beide denselben SQL rendern

SELECT a.id AS a_id, a.type AS a_type
FROM a JOIN b ON b.a_id = a.id JOIN a ON b.a_id = a.id AND a.type IN (:type_1)
WHERE a.type IN (:type_2)

Der obige SQL ist ungültig, da er "a" zweimal in der FROM-Liste rendert. Der Fehler der impliziten Aliassierung würde jedoch nur bei der zweiten Abfrage auftreten und stattdessen dies rendern:

SELECT a.id AS a_id, a.type AS a_type
FROM a JOIN b ON b.a_id = a.id JOIN a AS a_1
ON a_1.id = b.a_id AND a_1.type IN (:type_1)
WHERE a_1.type IN (:type_2)

Wo oben der zweite Join zu "a" aliassiert wird. Während dies bequem erscheint, ist es nicht, wie Single-Inheritance-Abfragen im Allgemeinen funktionieren, und es ist irreführend und inkonsistent.

Die Nettoauswirkung ist, dass Anwendungen, die sich auf diesen Fehler verlassen haben, nun eine von der Datenbank ausgelöste Ausnahme erhalten. Die Lösung besteht darin, die erwartete Form zu verwenden. Wenn Sie in einer Abfrage auf mehrere Unterklassen einer Single-Inheritance-Entität verweisen, müssen Sie manuell Aliase verwenden, um die Tabelle zu disambiguieren, da alle Unterklassen normalerweise auf dieselbe Tabelle verweisen.

asub2_alias = aliased(ASub2)

print(s.query(ASub1).join(B, ASub1.b).join(asub2_alias, B.a.of_type(asub2_alias)))

#3233 #3367

Deferred Columns werden nicht mehr implizit undeferred

Mit `deferred` markierte Abbildattribute ohne explizite Undeferral bleiben nun "deferred", auch wenn ihre Spalte auf andere Weise im Ergebnissatz vorhanden ist. Dies ist eine Leistungssteigerung, da eine ORM-Ladung nicht mehr damit beschäftigt ist, nach jeder deferred Spalte zu suchen, wenn das Ergebnis erzielt wird. Für eine Anwendung, die sich darauf verlassen hat, sollte jedoch nun ein explizites undefer() oder eine ähnliche Option verwendet werden, um zu verhindern, dass ein SELECT ausgelöst wird, wenn auf das Attribut zugegriffen wird.

Veraltete ORM-Event-Hooks entfernt

Die folgenden ORM-Event-Hooks, von denen einige seit 0.5 veraltet waren, wurden entfernt: translate_row, populate_instance, append_result, create_instance. Die Anwendungsfälle für diese Hooks stammen aus den sehr frühen Serien 0.1 / 0.2 von SQLAlchemy und sind seit langem überflüssig. Insbesondere waren die Hooks weitgehend unbrauchbar, da die Verhaltensverträge innerhalb dieser Ereignisse stark an die umgebenden Interna gekoppelt waren, wie z. B. die Erstellung und Initialisierung einer Instanz sowie die Lokalisierung von Spalten innerhalb einer ORM-generierten Zeile. Die Entfernung dieser Hooks vereinfacht die Mechanik des ORM-Objektladens erheblich.

API-Änderung für das neue Bundle-Feature bei Verwendung benutzerdefinierter Zeilenlader

Das neue Bundle-Objekt von 0.9 hat eine kleine API-Änderung, wenn die Methode create_row_processor() auf einer benutzerdefinierten Klasse überschrieben wird. Zuvor sah der Beispielcode wie folgt aus:

from sqlalchemy.orm import Bundle


class DictBundle(Bundle):
    def create_row_processor(self, query, procs, labels):
        """Override create_row_processor to return values as dictionaries"""

        def proc(row, result):
            return dict(zip(labels, (proc(row, result) for proc in procs)))

        return proc

Das ungenutzte Mitglied result wurde nun entfernt.

from sqlalchemy.orm import Bundle


class DictBundle(Bundle):
    def create_row_processor(self, query, procs, labels):
        """Override create_row_processor to return values as dictionaries"""

        def proc(row):
            return dict(zip(labels, (proc(row) for proc in procs)))

        return proc

Rechts verschachtelte Inner Joins nun Standard für joinedload mit innerjoin=True

Das Verhalten von joinedload.innerjoin sowie relationship.innerjoin ist nun die Verwendung von "verschachtelten" Inner Joins, d. h. rechts verschachtelt, als Standardverhalten, wenn ein Inner Join Eager Load an einen Outer Join Eager Load gekettet wird. Um das alte Verhalten des Ketten aller Joined Eager Loads als Outer Join bei Vorhandensein eines Outer Joins zu erhalten, verwenden Sie innerjoin="unnested".

Wie in Rechts verschachtelte Inner Joins in Joined Eager Loads verfügbar aus Version 0.9 eingeführt, ist das Verhalten von innerjoin="nested", dass ein Inner Join Eager Load, der an einen Outer Join Eager Load gekettet ist, einen rechts verschachtelten Join verwendet. "nested" wird nun impliziert, wenn innerjoin=True verwendet wird.

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

Mit dem neuen Standard rendert dies die FROM-Klausel in der Form

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

Das heißt, ein rechts verschachtelter Join für den INNER Join, damit das vollständige Ergebnis von users zurückgegeben werden kann. Die Verwendung eines INNER Joins ist effizienter als die Verwendung eines OUTER Joins und ermöglicht, dass der Optimierungsparameter joinedload.innerjoin in allen Fällen wirksam wird.

Um das ältere Verhalten zu erhalten, verwenden Sie innerjoin="unnested".

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

Dies vermeidet rechts verschachtelte Joins und kettet die Joins mit ausschließlich OUTER Joins zusammen, trotz der innerjoin-Direktive.

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

Wie in den 0.9er Notizen erwähnt, ist das einzige Backend, das Schwierigkeiten mit rechts verschachtelten Joins hat, SQLite; SQLAlchemy wandelt ab 0.9 einen rechts verschachtelten Join in eine Subquery als Join-Ziel auf SQLite um.

Siehe auch

Rechts verschachtelte Inner Joins in Joined Eager Loads verfügbar - Beschreibung des Features, wie es in 0.9.4 eingeführt wurde.

#3008

Subqueries werden nicht mehr auf `uselist=False` Joined Eager Loads angewendet

Gegeben ein Joined Eager Load wie das folgende

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


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


s = Session()
print(s.query(A).options(joinedload(A.b)).limit(5))

SQLAlchemy betrachtet die Beziehung A.b als "eins zu viele, als einzelner Wert geladen", was im Wesentlichen eine "eins zu eins"-Beziehung ist. Jedoch hat Joined Eager Loading dies immer als eine Situation behandelt, in der die Hauptabfrage innerhalb einer Subquery liegen muss, wie es normalerweise für eine Sammlung von B-Objekten benötigt würde, wenn die Hauptabfrage ein LIMIT angewendet hat.

SELECT anon_1.a_id AS anon_1_a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM (SELECT a.id AS a_id
FROM a LIMIT :param_1) AS anon_1
LEFT OUTER JOIN b AS b_1 ON anon_1.a_id = b_1.a_id

Da die Beziehung der inneren Abfrage zur äußeren jedoch so ist, dass höchstens eine Zeile im Fall von uselist=False geteilt wird (ähnlich wie bei einem Many-to-One), wird die mit LIMIT + Joined Eager Loading verwendete "Subquery" in diesem Fall nun fallen gelassen.

SELECT a.id AS a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
LIMIT :param_1

In dem Fall, dass der LEFT OUTER JOIN mehr als eine Zeile zurückgibt, hat die ORM hier immer eine Warnung ausgegeben und zusätzliche Ergebnisse für uselist=False ignoriert, sodass sich die Ergebnisse in dieser Fehlersituation nicht ändern sollten.

#3249

query.update() / query.delete() löst eine Ausnahme aus, wenn mit join(), select_from(), from_self() verwendet

Eine Warnung wird in SQLAlchemy 0.9.10 (noch nicht veröffentlicht am 9. Juni 2015) ausgegeben, wenn die Methoden Query.update() oder Query.delete() gegen eine Abfrage aufgerufen werden, die auch Query.join(), Query.outerjoin(), Query.select_from() oder Query.from_self() aufgerufen hat. Dies sind nicht unterstützte Anwendungsfälle, die in der 0.9er Serie bis 0.9.10 stillschweigend fehlschlagen, wo sie eine Warnung ausgeben. In 1.0 lösen diese Fälle eine Ausnahme aus.

#3349

query.update() mit synchronize_session='evaluate' löst bei Multi-Table-Updates eine Ausnahme aus

Der "Evaluator" für Query.update() funktioniert nicht mit Multi-Table-Updates und muss auf synchronize_session=False oder synchronize_session='fetch' gesetzt werden, wenn mehrere Tabellen vorhanden sind. Das neue Verhalten ist, dass nun eine explizite Ausnahme mit einer Meldung zur Änderung der Synchronisationseinstellung ausgelöst wird. Dies wurde von einer Warnung ab 0.9.7 aufgewertet.

#3117

Resurrect Event wurde entfernt

Das "resurrect" ORM-Ereignis wurde vollständig entfernt. Dieses Ereignis hatte seit Version 0.8, die das ältere "mutable"-System aus der Unit of Work entfernt hat, keine Funktion mehr.

Änderung der Single-Table-Inheritance-Kriterien bei Verwendung von from_self(), count()

Gegeben eine Single-Table-Inheritance-Abbildung, wie z. B.:

class Widget(Base):
    __table__ = "widget_table"


class FooWidget(Widget):
    pass

Die Verwendung von Query.from_self() oder Query.count() gegen eine Unterklasse würde eine Subquery erzeugen, aber dann die "WHERE"-Kriterien für Untertypen nach außen hinzufügen.

sess.query(FooWidget).from_self().all()

Rendert

SELECT
    anon_1.widgets_id AS anon_1_widgets_id,
    anon_1.widgets_type AS anon_1_widgets_type
FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type,
FROM widgets) AS anon_1
WHERE anon_1.widgets_type IN (?)

Das Problem dabei ist, dass wenn die innere Abfrage nicht alle Spalten angibt, wir die WHERE-Klausel außen nicht hinzufügen können (sie versucht es tatsächlich und erzeugt eine fehlerhafte Abfrage). Diese Entscheidung reicht scheinbar bis ins Jahr 0.6.5 zurück mit dem Hinweis "müssen möglicherweise weitere Anpassungen vornehmen". Nun, diese Anpassungen sind eingetroffen! Die obige Abfrage wird nun also gerendert:

SELECT
    anon_1.widgets_id AS anon_1_widgets_id,
    anon_1.widgets_type AS anon_1_widgets_type
FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type,
FROM widgets
WHERE widgets.type IN (?)) AS anon_1

Damit Abfragen, die nicht "type" enthalten, weiterhin funktionieren!

sess.query(FooWidget.id).count()

Rendert

SELECT count(*) AS count_1
FROM (SELECT widgets.id AS widgets_id
FROM widgets
WHERE widgets.type IN (?)) AS anon_1

#3177

Single-Table-Inheritance-Kriterien werden bedingungslos zu allen ON-Klauseln hinzugefügt

Beim Verknüpfen mit einem Single-Table-Inheritance-Unterklassenziel fügt die ORM immer die "Single Table Criteria" hinzu, wenn über eine Beziehung verknüpft wird. Bei einer Abbildung wie folgt:

class Widget(Base):
    __tablename__ = "widget"
    id = Column(Integer, primary_key=True)
    type = Column(String)
    related_id = Column(ForeignKey("related.id"))
    related = relationship("Related", backref="widget")
    __mapper_args__ = {"polymorphic_on": type}


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


class Related(Base):
    __tablename__ = "related"
    id = Column(Integer, primary_key=True)

Es war schon seit einiger Zeit das Verhalten, dass ein JOIN über die Beziehung eine "Single Inheritance"-Klausel für den Typ rendert.

s.query(Related).join(FooWidget, Related.widget).all()

SQL-Ausgabe

SELECT related.id AS related_id
FROM related JOIN widget ON related.id = widget.related_id AND widget.type IN (:type_1)

Oben, weil wir zu einer Unterklasse FooWidget gejoint haben, wusste Query.join(), die Kriterien AND widget.type IN ('foo') zur ON-Klausel hinzuzufügen.

Die Änderung hier ist, dass die Kriterien AND widget.type IN() nun an *jede* ON-Klausel angehängt werden, nicht nur an die, die aus einer Beziehung generiert werden, einschließlich einer, die explizit angegeben wurde.

# ON clause will now render as
# related.id = widget.related_id AND widget.type IN (:type_1)
s.query(Related).join(FooWidget, FooWidget.related_id == Related.id).all()

Sowie der "implizite" Join, wenn keine ON-Klausel irgendeiner Art angegeben wird.

# ON clause will now render as
# related.id = widget.related_id AND widget.type IN (:type_1)
s.query(Related).join(FooWidget).all()

Zuvor enthielt die ON-Klausel für diese keinen Single-Inheritance-Kriterien. Anwendungen, die diese Kriterien bereits hinzufügen, um dieses Problem zu umgehen, möchten ihre explizite Verwendung entfernen, obwohl sie weiterhin einwandfrei funktionieren sollte, wenn die Kriterien versehentlich zweimal gerendert werden.

#3222

Wichtige Verhaltensänderungen – Core

Warnungen werden beim Konvertieren vollständiger SQL-Fragmente in text() ausgegeben

Seit der Gründung von SQLAlchemy gab es schon immer einen Schwerpunkt darauf, die Verwendung von einfachem Text nicht zu behindern. Die Core- und ORM-Expressionssysteme waren dazu gedacht, eine beliebige Anzahl von Punkten zu ermöglichen, an denen der Benutzer einfach plain text SQL-Ausdrücke verwenden kann, nicht nur im Sinne, dass Sie eine vollständige SQL-Zeichenfolge an Connection.execute() senden können, sondern dass Sie Zeichenfolgen mit SQL-Ausdrücken an viele Funktionen senden können, wie z. B. Select.where(), Query.filter() und Select.order_by().

Beachten Sie, dass mit "SQL-Ausdrücken" ein **vollständiges Fragment einer SQL-Zeichenfolge** gemeint ist, wie z. B.

# the argument sent to where() is a full SQL expression
stmt = select([sometable]).where("somecolumn = 'value'")

und wir **nicht von Zeichenfolgenargumenten** sprechen, d. h. vom normalen Verhalten, Zeichenfolgenwerte zu übergeben, die parametrisiert werden.

# This is a normal Core expression with a string argument -
# we aren't talking about this!!
stmt = select([sometable]).where(sometable.c.somecolumn == "value")

Das Core-Tutorial enthält seit langem ein Beispiel für die Verwendung dieser Technik mit einem select()-Konstrukt, bei dem praktisch alle Komponenten als reine Zeichenfolgen angegeben sind. Trotz dieses langjährigen Verhaltens und Beispiels sind Benutzer offenbar überrascht, dass dieses Verhalten existiert, und als ich in der Community nachfragte, konnte ich keinen Benutzer finden, der in der Tat *nicht* überrascht war, dass man eine vollständige Zeichenfolge an eine Methode wie Query.filter() übergeben kann.

Die Änderung hier besteht darin, den Benutzer zu ermutigen, Textzeichenfolgen zu qualifizieren, wenn SQL zusammengestellt wird, das teilweise oder vollständig aus Textfragmenten besteht. Beim Erstellen einer Auswahl wie unten:

stmt = select(["a", "b"]).where("a = b").select_from("sometable")

Die Anweisung wird normal aufgebaut, mit denselben Konvertierungen wie zuvor. Man wird jedoch die folgenden Warnungen sehen:

SAWarning: Textual column expression 'a' should be explicitly declared
with text('a'), or use column('a') for more specificity
(this warning may be suppressed after 10 occurrences)

SAWarning: Textual column expression 'b' should be explicitly declared
with text('b'), or use column('b') for more specificity
(this warning may be suppressed after 10 occurrences)

SAWarning: Textual SQL expression 'a = b' should be explicitly declared
as text('a = b') (this warning may be suppressed after 10 occurrences)

SAWarning: Textual SQL FROM expression 'sometable' should be explicitly
declared as text('sometable'), or use table('sometable') for more
specificity (this warning may be suppressed after 10 occurrences)

Diese Warnungen versuchen genau zu zeigen, wo das Problem liegt, indem sie die Parameter sowie die Stelle, an der die Zeichenfolge empfangen wurde, anzeigen. Die Warnungen nutzen Session.get_bind() behandelt eine breitere Palette von Vererbungsszenarien, damit parametrisierte Warnungen sicher ausgegeben werden können, ohne dass der Speicher knapp wird, und wie immer, wenn man möchte, dass Warnungen Ausnahmen sind, sollte der Python Warnings Filter verwendet werden.

import warnings

warnings.simplefilter("error")  # all warnings raise an exception

Angesichts der oben genannten Warnungen funktioniert unsere Anweisung einwandfrei, aber um die Warnungen zu beseitigen, würden wir unsere Anweisung wie folgt umschreiben:

from sqlalchemy import select, text

stmt = (
    select([text("a"), text("b")]).where(text("a = b")).select_from(text("sometable"))
)

Und wie die Warnungen vorschlagen, können wir unserer Anweisung mehr Spezifität über den Text geben, wenn wir column() und table() verwenden.

from sqlalchemy import select, text, column, table

stmt = (
    select([column("a"), column("b")])
    .where(text("a = b"))
    .select_from(table("sometable"))
)

Dabei ist zu beachten, dass table() und column() nun von "sqlalchemy" ohne den "sql"-Teil importiert werden können.

Das Verhalten hier gilt für select() sowie für wichtige Methoden auf Query, einschließlich Query.filter(), Query.from_statement() und Query.having().

ORDER BY und GROUP BY sind Sonderfälle

Es gibt einen Fall, in dem die Verwendung einer Zeichenfolge eine besondere Bedeutung hat, und als Teil dieser Änderung haben wir ihre Funktionalität erweitert. Wenn wir eine select() oder Query haben, die sich auf einen Spaltennamen oder ein benanntes Label bezieht, möchten wir möglicherweise nach bekannten Spalten oder Labels GROUP BY und/oder ORDER BY durchführen.

stmt = (
    select([user.c.name, func.count(user.c.id).label("id_count")])
    .group_by("name")
    .order_by("id_count")
)

In der obigen Anweisung erwarten wir "ORDER BY id_count" zu sehen, im Gegensatz zu einer erneuten Angabe der Funktion. Das übergebene Zeichenfolgenargument wird während der Kompilierung aktiv mit einem Eintrag in der Spaltenklausel abgeglichen, so dass die obige Anweisung wie erwartet ohne Warnungen produziert wird (beachten Sie jedoch, dass der Ausdruck "name" zu users.name aufgelöst wurde!).

SELECT users.name, count(users.id) AS id_count
FROM users GROUP BY users.name ORDER BY id_count

Wenn wir uns jedoch auf einen Namen beziehen, der nicht gefunden werden kann, erhalten wir erneut die Warnung, wie unten:

stmt = select([user.c.name, func.count(user.c.id).label("id_count")]).order_by(
    "some_label"
)

Die Ausgabe tut, was wir sagen, aber sie warnt uns wieder.

SAWarning: Can't resolve label reference 'some_label'; converting to
text() (this warning may be suppressed after 10 occurrences)
SELECT users.name, count(users.id) AS id_count
FROM users ORDER BY some_label

Das obige Verhalten gilt für all jene Stellen, an denen wir uns auf eine sogenannte "Label-Referenz" beziehen möchten; ORDER BY und GROUP BY, aber auch innerhalb einer OVER-Klausel sowie einer DISTINCT ON-Klausel, die sich auf Spalten bezieht (z. B. die PostgreSQL-Syntax).

Wir können weiterhin beliebige Ausdrücke für ORDER BY oder andere mithilfe von text() angeben.

stmt = select([users]).order_by(text("some special expression"))

Der entscheidende Punkt der gesamten Änderung ist, dass SQLAlchemy von uns nun erwartet, dass wir ihm mitteilen, wenn eine Zeichenfolge übergeben wird, dass diese Zeichenfolge explizit ein text()-Konstrukt ist, oder eine Spalte, Tabelle usw., und wenn wir sie als Labelnamen in einem order by, group by oder einem anderen Ausdruck verwenden, erwartet SQLAlchemy, dass die Zeichenfolge etwas Bekanntes auflöst, andernfalls sollte sie wieder mit text() oder Ähnlichem qualifiziert werden.

#2992

Python-seitige Standardwerte werden bei der Verwendung einer mehrwertigen Einfügung für jede Zeile einzeln aufgerufen

Die Unterstützung für Python-seitige Spaltendefaults bei der Verwendung der mehrwertigen Version von Insert.values() war im Wesentlichen nicht implementiert und funktionierte nur "zufällig" in bestimmten Situationen, wenn das verwendete Dialekt einen nicht-positionalen (z. B. benannten) Stil von gebundenen Parametern verwendete und wenn es nicht notwendig war, dass ein Python-seitiger Callable für jede Zeile aufgerufen wurde.

Das Feature wurde überarbeitet, sodass es dem eines "executemany"-Aufrufs ähnlicher funktioniert.

import itertools

counter = itertools.count(1)
t = Table(
    "my_table",
    metadata,
    Column("id", Integer, default=lambda: next(counter)),
    Column("data", String),
)

conn.execute(
    t.insert().values(
        [
            {"data": "d1"},
            {"data": "d2"},
            {"data": "d3"},
        ]
    )
)

Das obige Beispiel ruft next(counter) für jede Zeile einzeln auf, wie erwartet.

INSERT INTO my_table (id, data) VALUES (?, ?), (?, ?), (?, ?)
(1, 'd1', 2, 'd2', 3, 'd3')

Zuvor würde ein positionelles Dialekt fehlschlagen, da für zusätzliche Positionen keine Bindung generiert würde.

Incorrect number of bindings supplied. The current statement uses 6,
and there are 4 supplied.
[SQL: u'INSERT INTO my_table (id, data) VALUES (?, ?), (?, ?), (?, ?)']
[parameters: (1, 'd1', 'd2', 'd3')]

Und mit einem "benannten" Dialekt würde derselbe Wert für "id" in jeder Zeile wiederverwendet (daher ist diese Änderung rückwärtskompatibel mit einem System, das sich darauf verlassen hat).

INSERT INTO my_table (id, data) VALUES (:id, :data_0), (:id, :data_1), (:id, :data_2)
-- {u'data_2': 'd3', u'data_1': 'd2', u'data_0': 'd1', 'id': 1}

Das System wird auch die Einbettung eines "serverseitigen" Standardwerts als Inline-SQL rendern verweigern, da nicht garantiert werden kann, dass ein serverseitiger Standardwert damit kompatibel ist. Wenn die VALUES-Klausel für eine bestimmte Spalte gerendert wird, ist ein Python-seitiger Wert erforderlich; wenn ein ausgelassener Wert nur auf einen serverseitigen Standardwert verweist, wird eine Ausnahme ausgelöst.

t = Table(
    "my_table",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("data", String, server_default="some default"),
)

conn.execute(
    t.insert().values(
        [
            {"data": "d1"},
            {"data": "d2"},
            {},
        ]
    )
)

wird eine Ausnahme auslösen

sqlalchemy.exc.CompileError: INSERT value for column my_table.data is
explicitly rendered as a boundparameter in the VALUES clause; a
Python-side value or SQL expression is required

Zuvor würde der Wert "d1" in die dritte Zeile kopiert werden (aber wieder nur im benannten Format!).

INSERT INTO my_table (data) VALUES (:data_0), (:data_1), (:data_0)
-- {u'data_1': 'd2', u'data_0': 'd1'}

#3288

Event-Listener können nicht aus dem Runner desselben Events hinzugefügt oder entfernt werden

Das Entfernen eines Event-Listeners aus demselben Event selbst würde die Elemente einer Liste während der Iteration ändern, was dazu führen würde, dass noch angehängte Event-Listener stillschweigend nicht ausgelöst werden. Um dies zu verhindern und gleichzeitig die Leistung aufrechtzuerhalten, wurden die Listen durch collections.deque() ersetzt, das keine Hinzufügungen oder Entfernungen während der Iteration zulässt und stattdessen RuntimeError auslöst.

#3163

Das INSERT…FROM SELECT-Konstrukt impliziert nun inline=True

Die Verwendung von Insert.from_select() impliziert nun inline=True bei insert(). Dies hilft, einen Fehler zu beheben, bei dem ein INSERT…FROM SELECT-Konstrukt versehentlich als "implizites Zurückgeben" auf unterstützenden Backends kompiliert wurde, was zu Brüchen im Fall eines INSERTs führte, der null Zeilen einfügt (da implizites Zurückgeben eine Zeile erwartet), sowie willkürliche Rückgabedaten im Fall eines INSERTs, der mehrere Zeilen einfügt (z. B. nur die erste von vielen Zeilen). Eine ähnliche Änderung wird auch auf ein INSERT..VALUES mit mehreren Parametersätzen angewendet; implizites RETURNING wird auch für diese Anweisung nicht mehr ausgegeben. Da beide dieser Konstrukte mit variablen Zeilenanzahlen umgehen, gilt der Zugriffsselektor ResultProxy.inserted_primary_key nicht. Zuvor gab es einen Dokumentationshinweis, dass man inline=True mit INSERT..FROM SELECT bevorzugen könnte, da einige Datenbanken RETURNING nicht unterstützen und daher kein "implizites" RETURNING durchführen können, aber es gibt keinen Grund, warum ein INSERT…FROM SELECT in irgendeinem Fall implizites RETURNING benötigen sollte. Reguläre explizite Insert.returning() sollte verwendet werden, um variable Mengen an Ergebniszeilen zurückzugeben, wenn eingefügte Daten benötigt werden.

#3169

autoload_with impliziert nun autoload=True

Eine Table kann für die Reflexion eingerichtet werden, indem allein Table.autoload_with übergeben wird.

my_table = Table("my_table", metadata, autoload_with=some_engine)

#3027

Verbesserungen bei der DBAPI-Ausnahme-Wrapperung und dem handle_error()-Ereignis

Die Wrapperung von DBAPI-Ausnahmen durch SQLAlchemy fand nicht statt, wenn ein Connection-Objekt ungültig wurde und dann versucht wurde, die Verbindung wiederherzustellen und ein Fehler auftrat; dies wurde behoben.

Zusätzlich wird das kürzlich hinzugefügte ConnectionEvents.handle_error()-Ereignis nun für Fehler aufgerufen, die beim ersten Verbindungsaufbau, beim Wiederherstellen der Verbindung auftreten, und wenn create_engine() mit einer benutzerdefinierten Verbindungsfunktion über create_engine.creator verwendet wird.

Das Objekt ExceptionContext hat ein neues Datenfeld ExceptionContext.engine, das immer auf die verwendete Engine verweist, in Fällen, in denen das Objekt Connection nicht verfügbar ist (z. B. bei der anfänglichen Verbindung).

#3266

ForeignKeyConstraint.columns ist jetzt eine ColumnCollection

ForeignKeyConstraint.columns war zuvor eine einfache Liste, die entweder Zeichenketten oder Column-Objekte enthielt, je nachdem, wie die ForeignKeyConstraint konstruiert wurde und ob sie einer Tabelle zugeordnet war. Die Sammlung ist jetzt eine ColumnCollection und wird erst initialisiert, nachdem die ForeignKeyConstraint einer Table zugeordnet wurde. Ein neuer Zugriff ForeignKeyConstraint.column_keys wird hinzugefügt, um bedingungslos Zeichenketten-Schlüssel für die lokale Menge von Spalten zurückzugeben, unabhängig davon, wie das Objekt konstruiert wurde oder in welchem Zustand es sich derzeit befindet.

MetaData.sorted_tables-Accessor ist "deterministisch"

Die Sortierung der Tabellen, die aus dem MetaData.sorted_tables-Accessor resultiert, ist "deterministisch"; die Reihenfolge sollte in allen Fällen gleich sein, unabhängig vom Hashing in Python. Dies geschieht, indem die Tabellen zuerst nach Namen sortiert werden, bevor sie an den topologischen Algorithmus übergeben werden, der diese Reihenfolge während der Iteration beibehält.

Beachten Sie, dass diese Änderung noch nicht für die Reihenfolge gilt, die beim Ausgeben von MetaData.create_all() oder MetaData.drop_all() angewendet wird.

#3084

null(), false() und true() Konstanten sind keine Singletons mehr

Diese drei Konstanten wurden in 0.9 geändert, um einen "Singleton"-Wert zurückzugeben; leider würde dies dazu führen, dass eine Abfrage wie die folgende nicht wie erwartet gerendert wird:

select([null(), null()])

nur SELECT NULL AS anon_1 gerendert, da die beiden null()-Konstrukte dasselbe NULL-Objekt ergeben würden und das Core-Modell von SQLAlchemy auf Objektidentität basiert, um lexikalische Bedeutung zu bestimmen. Die Änderung in 0.9 war von keinerlei Bedeutung, außer dem Wunsch, Objekt-Overhead zu sparen; im Allgemeinen muss ein unbenanntes Konstrukt lexikalisch eindeutig bleiben, damit es eindeutig benannt wird.

#3170

SQLite/Oracle haben unterschiedliche Methoden zur Meldung von temporären Tabellen-/View-Namen

Die Methoden Inspector.get_table_names() und Inspector.get_view_names() würden im Fall von SQLite/Oracle auch die Namen von temporären Tabellen und Views zurückgeben, was von keinem anderen Dialekt bereitgestellt wird (im Fall von MySQL ist dies zumindest nicht einmal möglich). Diese Logik wurde in zwei neue Methoden Inspector.get_temp_table_names() und Inspector.get_temp_view_names() verschoben.

Beachten Sie, dass die Spiegelung einer bestimmten benannten temporären Tabelle oder eines temporären Views, entweder über Table('name', autoload=True) oder über Methoden wie Inspector.get_columns(), für die meisten, wenn nicht alle, Dialekte weiterhin funktioniert. Insbesondere für SQLite gibt es auch einen Bugfix für die Reflexion von UNIQUE-Constraints aus temporären Tabellen, der unter #3203 zu finden ist.

#3204

Dialektverbesserungen und Änderungen - PostgreSQL

Überarbeitung der CREATE/DROP-Regeln für den ENUM-Typ

Die Regeln für den PostgreSQL ENUM wurden in Bezug auf das Erstellen und Löschen des TYPE strenger.

Ein ENUM, das ohne explizite Zuordnung zu einem MetaData-Objekt erstellt wird, wird entsprechend Table.create() und Table.drop() erstellt und gelöscht.

table = Table(
    "sometable", metadata, Column("some_enum", ENUM("a", "b", "c", name="myenum"))
)

table.create(engine)  # will emit CREATE TYPE and CREATE TABLE
table.drop(engine)  # will emit DROP TABLE and DROP TYPE - new for 1.0

Das bedeutet, wenn eine zweite Tabelle auch ein Enum namens 'myenum' hat, schlägt die obige DROP-Operation jetzt fehl. Um den Anwendungsfall eines gemeinsam genutzten Aufzählungstyps zu unterstützen, wurde das Verhalten einer Metadaten-assoziierten Aufzählung verbessert.

Ein ENUM, das mit expliziter Zuordnung zu einem MetaData-Objekt erstellt wird, wird nicht entsprechend Table.create() und Table.drop() erstellt oder gelöscht, mit Ausnahme von Table.create(), das mit dem Flag checkfirst=True aufgerufen wird.

my_enum = ENUM("a", "b", "c", name="myenum", metadata=metadata)

table = Table("sometable", metadata, Column("some_enum", my_enum))

# will fail: ENUM 'my_enum' does not exist
table.create(engine)

# will check for enum and emit CREATE TYPE
table.create(engine, checkfirst=True)

table.drop(engine)  # will emit DROP TABLE, *not* DROP TYPE

metadata.drop_all(engine)  # will emit DROP TYPE

metadata.create_all(engine)  # will emit CREATE TYPE

#3319

Neue PostgreSQL Tabellenoptionen

Unterstützung für PG-Tabellenoptionen TABLESPACE, ON COMMIT, WITH(OUT) OIDS und INHERITS wurde hinzugefügt, wenn DDL über den Table-Konstrukt gerendert wird.

#2051

Neue Methode get_enums() mit PostgreSQL Dialekt

Die Methode inspect() gibt im Fall von PostgreSQL ein PGInspector-Objekt zurück, das eine neue Methode PGInspector.get_enums() enthält, die Informationen über alle verfügbaren ENUM-Typen zurückgibt.

from sqlalchemy import inspect, create_engine

engine = create_engine("postgresql+psycopg2://host/dbname")
insp = inspect(engine)
print(insp.get_enums())

PostgreSQL Dialekt spiegelt Materialized Views, Foreign Tables

Die Änderungen sind wie folgt:

  • Das Table-Konstrukt mit autoload=True stimmt jetzt mit einem Namen überein, der in der Datenbank als materialized view oder foreign table vorhanden ist.

  • Inspector.get_view_names() gibt Namen von Plain Views und Materialized Views zurück.

  • Inspector.get_table_names() ändert sich für PostgreSQL nicht, es gibt weiterhin nur die Namen von Plain Tables zurück.

  • Eine neue Methode PGInspector.get_foreign_table_names() wird hinzugefügt, die die Namen von Tabellen zurückgibt, die in den PostgreSQL-Schematabellen explizit als "foreign" gekennzeichnet sind.

Die Änderung der Reflexion beinhaltet das Hinzufügen von 'm' und 'f' zur Liste der Qualifizierer, die wir beim Abfragen von pg_class.relkind verwenden, aber diese Änderung ist neu in 1.0.0, um rückwärtskompatible Überraschungen für diejenigen zu vermeiden, die 0.9 in Produktion verwenden.

#2891

PostgreSQL has_table() funktioniert jetzt für temporäre Tabellen

Dies ist ein einfacher Fix, so dass "hat Tabelle" für temporäre Tabellen jetzt funktioniert, so dass Code wie der folgende fortgesetzt werden kann:

from sqlalchemy import *

metadata = MetaData()
user_tmp = Table(
    "user_tmp",
    metadata,
    Column("id", INT, primary_key=True),
    Column("name", VARCHAR(50)),
    prefixes=["TEMPORARY"],
)

e = create_engine("postgresql://scott:tiger@localhost/test", echo="debug")
with e.begin() as conn:
    user_tmp.create(conn, checkfirst=True)

    # checkfirst will succeed
    user_tmp.create(conn, checkfirst=True)

Der sehr unwahrscheinliche Fall, dass dieses Verhalten dazu führt, dass eine nicht fehlerhafte Anwendung anders funktioniert, liegt daran, dass PostgreSQL es erlaubt, dass eine nicht-temporäre Tabelle eine temporäre Tabelle stillschweigend überschreibt. Code wie der folgende verhält sich jetzt völlig anders und erstellt die echte Tabelle nach der temporären Tabelle nicht mehr:

from sqlalchemy import *

metadata = MetaData()
user_tmp = Table(
    "user_tmp",
    metadata,
    Column("id", INT, primary_key=True),
    Column("name", VARCHAR(50)),
    prefixes=["TEMPORARY"],
)

e = create_engine("postgresql://scott:tiger@localhost/test", echo="debug")
with e.begin() as conn:
    user_tmp.create(conn, checkfirst=True)

    m2 = MetaData()
    user = Table(
        "user_tmp",
        m2,
        Column("id", INT, primary_key=True),
        Column("name", VARCHAR(50)),
    )

    # in 0.9, *will create* the new table, overwriting the old one.
    # in 1.0, *will not create* the new table
    user.create(conn, checkfirst=True)

#3264

PostgreSQL FILTER-Schlüsselwort

Das SQL-Standard-Schlüsselwort FILTER für Aggregatfunktionen wird von PostgreSQL ab 9.4 unterstützt. SQLAlchemy ermöglicht dies über FunctionElement.filter().

func.count(1).filter(True)

PG8000 Dialekt unterstützt Client-seitige Codierung

Der Parameter create_engine.encoding wird jetzt vom pg8000-Dialekt berücksichtigt, indem ein "on connect"-Handler verwendet wird, der SET CLIENT_ENCODING entsprechend der ausgewählten Codierung ausgibt.

PG8000 native JSONB-Unterstützung

Unterstützung für PG8000-Versionen über 1.10.1 wurde hinzugefügt, wo JSONB nativ unterstützt wird.

Unterstützung für psycopg2cffi Dialekt auf PyPy

Unterstützung für den pypy psycopg2cffi Dialekt wurde hinzugefügt.

Dialektverbesserungen und Änderungen - MySQL

MySQL TIMESTAMP-Typ rendert jetzt NULL / NOT NULL in allen Fällen

Der MySQL-Dialekt hat immer MySQLs impliziten NOT NULL-Standard für TIMESTAMP-Spalten umgangen, indem er NULL für einen solchen Typ ausgegeben hat, wenn die Spalte mit nullable=True eingerichtet wurde. MySQL 5.6.6 und höher verfügt jedoch über ein neues Flag explicit_defaults_for_timestamp, das das nicht standardmäßige Verhalten von MySQL repariert, um es wie jeden anderen Typ zu behandeln. Um dem Rechnung zu tragen, gibt SQLAlchemy jetzt bedingungslos NULL/NOT NULL für alle TIMESTAMP-Spalten aus.

#3155

MySQL SET-Typ überarbeitet zur Unterstützung von leeren Sets, Unicode und der Behandlung von leeren Werten

Der SET-Typ enthielt historisch kein System zur getrennten Behandlung von leeren Sets und leeren Werten; da verschiedene Treiber unterschiedliche Verhaltensweisen bei der Behandlung von leeren Strings und Darstellungen von leeren String-Sets hatten, versuchte der SET-Typ nur, zwischen diesen Verhaltensweisen zu vermitteln, und entschied sich dafür, das leere Set als set(['']) zu behandeln, was für den MySQL-Connector-Python DBAPI immer noch das aktuelle Verhalten ist. Ein Teil der Begründung war, dass es ansonsten unmöglich war, einen leeren String tatsächlich in einem MySQL SET zu speichern, da der Treiber uns Strings zurückgibt, ohne die Möglichkeit, zwischen set(['']) und set() zu unterscheiden. Es oblag dem Benutzer zu entscheiden, ob set(['']) tatsächlich "leeres Set" bedeutet oder nicht.

Das neue Verhalten verschiebt den Anwendungsfall für den leeren String, der ein ungewöhnlicher Fall ist, der nicht einmal in der MySQL-Dokumentation aufgeführt ist, in einen Sonderfall, und das Standardverhalten von SET ist jetzt:

  • den von MySQL-python zurückgegebenen leeren String '' als leeres Set set() zu behandeln;

  • den von MySQL-Connector-Python zurückgegebenen Single-Blank-Wert-Set set(['']) in das leere Set set() zu konvertieren;

  • Um den Fall eines Set-Typs zu behandeln, der tatsächlich den leeren Wert '' in seine Liste möglicher Werte aufnehmen möchte, wird eine neue Funktion (in diesem Anwendungsfall erforderlich) implementiert, bei der der Set-Wert als bitweises ganzzahliges Wert gespeichert und geladen wird; das Flag SET.retrieve_as_bitwise wird hinzugefügt, um dies zu aktivieren.

Die Verwendung des Flags SET.retrieve_as_bitwise ermöglicht es, das Set mit keinerlei Mehrdeutigkeit der Werte zu speichern und abzurufen. Theoretisch kann dieses Flag in allen Fällen aktiviert werden, solange die gegebene Liste von Werten für den Typ genau mit der im Datenbank deklarierten Reihenfolge übereinstimmt; es macht die SQL-Echo-Ausgabe nur etwas ungewöhnlicher.

Das Standardverhalten von SET bleibt ansonsten gleich, wobei Werte über Strings wiedergegeben werden. Das stringbasierte Verhalten unterstützt nun vollständig Unicode, einschließlich MySQL-Python mit use_unicode=0.

#3283

MySQL interne "no such table"-Ausnahmen werden nicht an Event-Handler weitergeleitet

Der MySQL-Dialekt wird nun die ConnectionEvents.handle_error()-Events für die Anweisungen deaktivieren, die er intern zur Erkennung der Existenz einer Tabelle verwendet. Dies wird durch eine Ausführungsoption skip_user_error_events erreicht, die das "handle error"-Event für den Gültigkeitsbereich dieser Ausführung deaktiviert. Auf diese Weise muss sich Benutzercode, der Ausnahmen umschreibt, keine Gedanken über den MySQL-Dialekt oder andere Dialekte machen, die gelegentlich SQLAlchemy-spezifische Ausnahmen abfangen müssen.

Standardwert von raise_on_warnings für MySQL-Connector geändert

Der Standardwert von "raise_on_warnings" wurde für MySQL-Connector auf False geändert. Dieser war aus irgendeinem Grund auf True gesetzt. Das "buffered"-Flag muss leider auf True bleiben, da MySQLconnector keinen Cursor schließen lässt, bevor nicht alle Ergebnisse vollständig abgeholt wurden.

#2515

MySQL boolesche Symbole "true", "false" funktionieren wieder

Die Überarbeitung der IS/IS NOT-Operatoren sowie der booleschen Typen in #2682 in 0.9 verhinderte, dass der MySQL-Dialekt die Symbole "true" und "false" im Kontext von "IS" / "IS NOT" verwenden konnte. Anscheinend unterstützt MySQL, obwohl es keinen "boolean"-Typ hat, IS / IS NOT, wenn die speziellen Symbole "true" und "false" verwendet werden, obwohl diese ansonsten mit "1" und "0" gleichbedeutend sind (und IS/IS NOT nicht mit numerischen Werten funktioniert).

Die Änderung hier ist also, dass der MySQL-Dialekt "nicht-nativer boolescher" bleibt, aber die Symbole true() und false() wieder die Schlüsselwörter "true" und "false" erzeugen, so dass ein Ausdruck wie column.is_(true()) auf MySQL wieder funktioniert.

#3186

Der match()-Operator gibt jetzt einen agnostischen MatchType zurück, der mit dem Gleitkomma-Rückgabewert von MySQL kompatibel ist

Der Rückgabetyp eines ColumnOperators.match()-Ausdrucks ist jetzt ein neuer Typ namens MatchType. Dies ist eine Unterklasse von Boolean, die vom Dialekt abgefangen werden kann, um beim SQL-Ausführungszeitpunkt einen anderen Ergebnistyp zu erzeugen.

Code wie der folgende funktioniert jetzt korrekt und gibt Gleitkommazahlen unter MySQL zurück:

>>> connection.execute(
...     select(
...         [
...             matchtable.c.title.match("Agile Ruby Programming").label("ruby"),
...             matchtable.c.title.match("Dive Python").label("python"),
...             matchtable.c.title,
...         ]
...     ).order_by(matchtable.c.id)
... )
[
    (2.0, 0.0, 'Agile Web Development with Ruby On Rails'),
    (0.0, 2.0, 'Dive Into Python'),
    (2.0, 0.0, "Programming Matz's Ruby"),
    (0.0, 0.0, 'The Definitive Guide to Django'),
    (0.0, 1.0, 'Python in a Nutshell')
]

#3263

Drizzle Dialect ist jetzt ein externer Dialekt

Der Dialekt für Drizzle ist jetzt ein externer Dialekt, verfügbar unter https://bitbucket.org/zzzeek/sqlalchemy-drizzle. Dieser Dialekt wurde zu SQLAlchemy hinzugefügt, bevor SQLAlchemy Drittanbieter-Dialekte gut unterstützen konnte; zukünftig sind alle Datenbanken, die nicht zur Kategorie "ubiquitäre Nutzung" gehören, Drittanbieter-Dialekte. Die Implementierung des Dialekts hat sich nicht geändert und basiert weiterhin auf den MySQL- und MySQLdb-Dialekten innerhalb von SQLAlchemy. Der Dialekt ist noch unveröffentlicht und im "Dachboden"-Status; er besteht jedoch die Mehrheit der Tests und ist im Allgemeinen in ordnungsgemäßem Zustand, wenn jemand an der Feinabstimmung arbeiten möchte.

Dialektverbesserungen und Änderungen – SQLite

SQLite benannte und unbenannte UNIQUE- und FOREIGN KEY-Constraints werden inspiziert und reflektiert

UNIQUE- und FOREIGN KEY-Constraints werden jetzt vollständig auf SQLite sowohl mit als auch ohne Namen reflektiert. Zuvor wurden ausländische Schlüsselnamen ignoriert und unbenannte eindeutige Constraints übersprungen. Dies wird insbesondere mit den neuen SQLite-Migrationsfunktionen von Alembic helfen.

Um dies zu erreichen, werden für ausländische Schlüssel und eindeutige Constraints die Ergebnisse von PRAGMA foreign_keys, index_list und index_info mit regulärer Ausdrucksanalyse der CREATE TABLE-Anweisung insgesamt kombiniert, um ein vollständiges Bild der Namen von Constraints zu erhalten und UNIQUE-Constraints zu unterscheiden, die als UNIQUE im Gegensatz zu unbenannten INDEXes erstellt wurden.

#3244

#3261

Dialektverbesserungen und Änderungen - SQL Server

PyODBC-Treibername ist für Hostnamen-basierte SQL Server-Verbindungen erforderlich

Die Verbindung zu SQL Server mit PyODBC über eine DSN-lose Verbindung, z. B. mit einem expliziten Hostnamen, erfordert jetzt einen Treibernamen - SQLAlchemy versucht nicht mehr, einen Standardwert zu erraten.

engine = create_engine(
    "mssql+pyodbc://scott:tiger@myhost:port/databasename?driver=SQL+Server+Native+Client+10.0"
)

Der bisher fest kodierte Standardwert von SQLAlchemy ("SQL Server") ist unter Windows veraltet, und SQLAlchemy kann nicht damit beauftragt werden, den besten Treiber basierend auf Betriebssystem-/Treibererkennung zu erraten. Die Verwendung einer DSN wird immer bevorzugt, wenn ODBC verwendet wird, um dieses Problem vollständig zu vermeiden.

#3182

SQL Server 2012 große Text-/Binärtypen werden als VARCHAR, NVARCHAR, VARBINARY gerendert

Das Rendering der Typen TextClause, UnicodeText und LargeBinary wurde für SQL Server 2012 und höher geändert, mit Optionen zur vollständigen Steuerung des Verhaltens, basierend auf den Deprecation-Richtlinien von Microsoft. Einzelheiten finden Sie unter Deprecation von großen Text-/Binärtypen.

Dialektverbesserungen und Änderungen - Oracle

Verbesserte Unterstützung für CTEs in Oracle

Die CTE-Unterstützung wurde für Oracle korrigiert und es gibt auch eine neue Funktion CTE.with_suffixes(), die bei den speziellen Direktiven von Oracle helfen kann.

included_parts = (
    select([part.c.sub_part, part.c.part, part.c.quantity])
    .where(part.c.part == "p1")
    .cte(name="included_parts", recursive=True)
    .suffix_with(
        "search depth first by part set ord1",
        "cycle part set y_cycle to 1 default 0",
        dialect="oracle",
    )
)

#3220

Neue Oracle-Schlüsselwörter für DDL

Schlüsselwörter wie COMPRESS, ON COMMIT, BITMAP

Oracle Database Tabellenoptionen

Oracle Datenbank-spezifische Indexoptionen