Nicht-traditionelle Abbildungen

Abbildung einer Klasse gegen mehrere Tabellen

Mapper können über einfache Tabellen hinaus gegen beliebige relationale Einheiten (sogenannte auswählbare Elemente) konstruiert werden. Zum Beispiel erstellt die Funktion join() eine auswählbare Einheit, die aus mehreren Tabellen besteht, komplett mit ihrem eigenen zusammengesetzten Primärschlüssel, die auf die gleiche Weise wie eine Table abgebildet werden kann.

from sqlalchemy import Table, Column, Integer, String, MetaData, join, ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import column_property

metadata_obj = MetaData()

# define two Table objects
user_table = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String),
)

address_table = Table(
    "address",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("user_id", Integer, ForeignKey("user.id")),
    Column("email_address", String),
)

# define a join between them.  This
# takes place across the user.id and address.user_id
# columns.
user_address_join = join(user_table, address_table)


class Base(DeclarativeBase):
    metadata = metadata_obj


# map to it
class AddressUser(Base):
    __table__ = user_address_join

    id = column_property(user_table.c.id, address_table.c.user_id)
    address_id = address_table.c.id

Im obigen Beispiel drückt der Join Spalten sowohl für die user- als auch für die address-Tabelle aus. Die Spalten user.id und address.user_id werden per Fremdschlüssel gleichgesetzt, so dass sie in der Abbildung als ein Attribut, AddressUser.id, definiert werden, wobei column_property() verwendet wird, um eine spezialisierte Spaltenabbildung anzuzeigen. Basierend auf diesem Teil der Konfiguration kopiert die Abbildung bei einem Flush neue Primärschlüsselwerte von user.id in die Spalte address.user_id.

Zusätzlich wird die Spalte address.id explizit auf ein Attribut namens address_id abgebildet. Dies dient dazu, die Abbildung der Spalte address.id von dem gleichnamigen Attribut AddressUser.id zu **unterscheiden**, das hier zugewiesen wurde, um sich auf die user-Tabelle in Verbindung mit dem Fremdschlüssel address.user_id zu beziehen.

Der natürliche Primärschlüssel der obigen Abbildung ist die Zusammensetzung von (user.id, address.id), da dies die Primärschlüsselspalten der kombinierten user- und address-Tabellen sind. Die Identität eines AddressUser-Objekts wird in Bezug auf diese beiden Werte sein und wird von einem AddressUser-Objekt als (AddressUser.id, AddressUser.address_id) dargestellt.

Bei der Bezugnahme auf die Spalte AddressUser.id werden die meisten SQL-Ausdrücke nur die erste Spalte in der Liste der abgebildeten Spalten verwenden, da die beiden Spalten synonym sind. Für den Spezialfall wie einen GROUP BY-Ausdruck, bei dem beide Spalten gleichzeitig referenziert werden müssen, wobei der richtige Kontext genutzt wird, d. h. Aliase und ähnliches berücksichtigt werden, kann der Accessor Comparator.expressions verwendet werden.

stmt = select(AddressUser).group_by(*AddressUser.id.expressions)

Neu in Version 1.3.17: Accessor Comparator.expressions hinzugefügt.

Hinweis

Eine Abbildung gegen mehrere Tabellen wie oben dargestellt unterstützt Persistenz, d. h. INSERT, UPDATE und DELETE von Zeilen innerhalb der Zieltabellen. Sie unterstützt jedoch keine Operation, die eine Tabelle aktualisieren und gleichzeitig andere für einen Datensatz einfügen oder löschen würde. Das heißt, wenn ein Datensatz PtoQ auf die Tabellen "p" und "q" abgebildet wird, wobei er eine Zeile basierend auf einem LEFT OUTER JOIN von "p" und "q" hat, muss im Falle eines UPDATEs, der Daten in der Tabelle "q" in einem bestehenden Datensatz ändern soll, die Zeile in "q" existieren; es wird kein INSERT ausgegeben, wenn die Primärschlüsselidentität bereits vorhanden ist. Wenn die Zeile nicht existiert, wird bei den meisten DBAPI-Treibern, die die Anzahl der von einem UPDATE betroffenen Zeilen melden können, der ORM eine aktualisierte Zeile nicht erkennen und einen Fehler auslösen; andernfalls werden die Daten stillschweigend ignoriert.

Ein Rezept, um ein "on-the-fly"-Einfügen der zugehörigen Zeile zu ermöglichen, könnte das Event .MapperEvents.before_update verwenden und wie folgt aussehen:

from sqlalchemy import event


@event.listens_for(PtoQ, "before_update")
def receive_before_update(mapper, connection, target):
    if target.some_required_attr_on_q is None:
        connection.execute(q_table.insert(), {"id": target.id})

wobei oben eine Zeile in die Tabelle q_table eingefügt wird, indem ein INSERT-Konstrukt mit Table.insert() erstellt und dann mit der gegebenen Connection ausgeführt wird, die die gleiche ist, die auch für andere SQL-Befehle während des Flush-Vorgangs verwendet wird. Die benutzerdefinierte Logik müsste erkennen, dass der LEFT OUTER JOIN von "p" nach "q" keinen Eintrag für die "q"-Seite hat.

Abbildung einer Klasse gegen beliebige Unterabfragen

Ähnlich wie bei der Abbildung gegen einen Join kann auch ein einfaches select()-Objekt mit einem Mapper verwendet werden. Das unten stehende Beispiel illustriert die Abbildung einer Klasse namens Customer auf eine select(), die einen Join zu einer Unterabfrage beinhaltet.

from sqlalchemy import select, func

subq = (
    select(
        func.count(orders.c.id).label("order_count"),
        func.max(orders.c.price).label("highest_order"),
        orders.c.customer_id,
    )
    .group_by(orders.c.customer_id)
    .subquery()
)

customer_select = (
    select(customers, subq)
    .join_from(customers, subq, customers.c.id == subq.c.customer_id)
    .subquery()
)


class Customer(Base):
    __table__ = customer_select

Oben ist die vollständige Zeile, die von customer_select dargestellt wird, alle Spalten der Tabelle customers, zusätzlich zu den von der Unterabfrage subq bereitgestellten Spalten, nämlich order_count, highest_order und customer_id. Die Abbildung der Klasse Customer auf dieses auswählbare Element erstellt dann eine Klasse, die diese Attribute enthalten wird.

Wenn der ORM neue Instanzen von Customer persistent speichert, erhält nur die Tabelle customers einen INSERT. Dies liegt daran, dass der Primärschlüssel der Tabelle orders nicht in der Abbildung dargestellt ist; der ORM gibt nur dann einen INSERT in eine Tabelle aus, für die er den Primärschlüssel abgebildet hat.

Hinweis

Die Praxis, auf beliebige SELECT-Anweisungen, insbesondere komplexe wie oben, abzubilden, ist fast nie erforderlich; sie führt zwangsläufig zu komplexen Abfragen, die oft weniger effizient sind als die, die durch direkte Abfragekonstruktion erzeugt würden. Die Praxis basiert in gewissem Maße auf der sehr frühen Geschichte von SQLAlchemy, als das Mapper-Konstrukt die primäre Abfrageschnittstelle darstellen sollte; in der modernen Nutzung kann das Query-Objekt verwendet werden, um praktisch jede SELECT-Anweisung, einschließlich komplexer Zusammensetzungen, zu erstellen, und sollte dem "Map-to-Selectable"-Ansatz vorgezogen werden.

Mehrere Mapper für eine Klasse

Im modernen SQLAlchemy wird eine bestimmte Klasse zu einem bestimmten Zeitpunkt von nur einem sogenannten **primären** Mapper abgebildet. Dieser Mapper ist in drei Hauptfunktionsbereichen tätig: Abfragen, Persistenz und Instrumentierung der abgebildeten Klasse. Die Begründung für den primären Mapper beruht auf der Tatsache, dass der Mapper die Klasse selbst modifiziert, nicht nur persistiert für eine bestimmte Table, sondern auch Attribute auf der Klasse instrumentiert, die speziell nach den Tabellenmetadaten strukturiert sind. Es ist nicht möglich, dass mehr als ein Mapper in gleichem Maße mit einer Klasse assoziiert ist, da nur ein Mapper die Klasse tatsächlich instrumentieren kann.

Das Konzept eines „nicht-primären“ Mappers existierte viele Versionen von SQLAlchemy, ist aber seit Version 1.3 veraltet. Der einzige Fall, in dem ein solcher nicht-primärer Mapper nützlich ist, ist die Konstruktion einer Beziehung zu einer Klasse, die auf einem alternativen auswählbaren Element basiert. Dieser Anwendungsfall wird jetzt mit dem aliased-Konstrukt abgedeckt und ist unter Beziehung zu einem aliasgebenden Element beschrieben.

Was den Anwendungsfall einer Klasse betrifft, die unter verschiedenen Szenarien tatsächlich vollständig in verschiedene Tabellen persistiert werden kann, so boten sehr frühe Versionen von SQLAlchemy eine Funktion dafür, die von Hibernate übernommen wurde, bekannt als die „Entity Name“-Funktion. Dieser Anwendungsfall wurde jedoch in SQLAlchemy, sobald die abgebildete Klasse selbst zur Quelle der SQL-Ausdruckskonstruktion wurde, undurchführbar; das heißt, die Attribute der Klasse verknüpfen sich direkt mit abgebildeten Tabellenspalten. Die Funktion wurde entfernt und durch ein einfaches rezeptorientiertes Vorgehen ersetzt, um diese Aufgabe ohne jegliche Mehrdeutigkeit bei der Instrumentierung zu lösen – neue Unterklassen erstellen, die jeweils einzeln abgebildet werden. Dieses Muster ist jetzt als Rezept unter Entity Name verfügbar.