SQLAlchemy 2.0 Dokumentation
SQLAlchemy ORM
- ORM Schnellstart
- ORM Abgebildete Klassenkonfiguration
- Beziehungskonfiguration
- Grundlegende Beziehungsmuster¶
- Adjazenzlisten-Beziehungen
- Konfiguration, wie Beziehungen verknüpft werden
- Arbeiten mit großen Sammlungen
- Sammlungsanpassung und API-Details
- Spezielle Beziehungspersistenzmuster
- Verwendung des Legacy-Parameters ‘backref’ für Beziehungen
- Beziehungs-API
- ORM Abfragehandbuch
- Verwendung der Sitzung
- Ereignisse und Interna
- ORM Erweiterungen
- ORM Beispiele
Projektversionen
Grundlegende Beziehungsmuster¶
Ein kurzer Überblick über die grundlegenden Beziehungsmuster, die in diesem Abschnitt anhand von deklarativen Stil-Mappings basierend auf der Verwendung des Annotations-Typs Mapped illustriert werden.
Die Einrichtung für jeden der folgenden Abschnitte lautet wie folgt:
from __future__ import annotations
from typing import List
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
passDeklarative vs. Imperative Formen¶
Da sich SQLAlchemy weiterentwickelt hat, sind verschiedene Konfigurationsstile für das ORM entstanden. Für Beispiele in diesem Abschnitt und anderen, die annotierte deklarative Mappings mit Mapped verwenden, sollte die entsprechende nicht-annotierte Form die gewünschte Klasse oder den gewünschten Klassennamen als erstes Argument für relationship() verwenden. Das folgende Beispiel illustriert die in diesem Dokument verwendete Form, die ein vollständig deklaratives Beispiel ist, das PEP 484-Annotationen verwendet, bei dem die relationship()-Konstruktion auch die Zielklasse und den Sammlungstyp aus der Mapped-Annotation ableitet, was die modernste Form des SQLAlchemy-Deklarativ-Mappings ist.
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="children")Im Gegensatz dazu ist die Verwendung eines deklarativen Mappings ohne Annotationen die "klassischere" Form des Mappings, bei der relationship() alle ihr übergebenen Parameter direkt erfordert, wie im folgenden Beispiel:
class Parent(Base):
__tablename__ = "parent_table"
id = mapped_column(Integer, primary_key=True)
children = relationship("Child", back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id = mapped_column(Integer, primary_key=True)
parent_id = mapped_column(ForeignKey("parent_table.id"))
parent = relationship("Parent", back_populates="children")Schließlich, bei Verwendung von Imperativem Mapping, der ursprünglichen Mapping-Form von SQLAlchemy, bevor Deklaratives erstellt wurde (was dennoch von einer lautstarken Minderheit von Benutzern bevorzugt wird), sieht die obige Konfiguration wie folgt aus:
registry.map_imperatively(
Parent,
parent_table,
properties={"children": relationship("Child", back_populates="parent")},
)
registry.map_imperatively(
Child,
child_table,
properties={"parent": relationship("Parent", back_populates="children")},
)Zusätzlich ist der Standard-Sammlungstyp für nicht-annotierte Mappings list. Um ein set oder eine andere Sammlung ohne Annotationen zu verwenden, geben Sie dies über den Parameter relationship.collection_class an.
class Parent(Base):
__tablename__ = "parent_table"
id = mapped_column(Integer, primary_key=True)
children = relationship("Child", collection_class=set, ...)Anpassen von Sammlungszugriffen - enthält weitere Details zur Sammlungs-Konfiguration, einschließlich einiger Techniken zum Mappen von relationship() zu Dictionaries.
Zusätzliche Unterschiede zwischen annotierten und nicht-annotierten / imperativen Stilen werden bei Bedarf vermerkt.
Eins zu Viele¶
Eine Eins-zu-Viele-Beziehung platziert einen Fremdschlüssel in der Kind-Tabelle, der auf die Eltern-Tabelle verweist. relationship() wird dann auf der Eltern-Seite angegeben und verweist auf eine Sammlung von Elementen, die durch das Kind repräsentiert werden.
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship()
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))Um eine bidirektionale Beziehung in Eins-zu-Viele zu etablieren, bei der die "umgekehrte" Seite eine Viele-zu-Eins-Beziehung ist, geben Sie eine zusätzliche relationship() an und verbinden Sie die beiden über den Parameter relationship.back_populates, wobei der Attributname jeder relationship() als Wert für relationship.back_populates auf der anderen Seite verwendet wird.
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="children")Das Kind erhält ein Elternteil-Attribut mit Viele-zu-Eins-Semantik.
Verwendung von Sets, Listen oder anderen Sammlungstypen für Eins zu Viele¶
Bei der Verwendung von annotierten deklarativen Mappings wird der für die relationship() verwendete Sammlungstyp aus dem Sammlungstyp abgeleitet, der an den Mapped-Container-Typ übergeben wird. Das Beispiel aus dem vorherigen Abschnitt kann so geschrieben werden, dass es ein set anstelle einer list für die Elternteil.Kinder-Sammlung mit Mapped[Set["Kind"]] verwendet.
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[Set["Child"]] = relationship(back_populates="parent")Bei Verwendung von nicht-annotierten Formen, einschließlich imperativer Mappings, kann die Python-Klasse, die als Sammlung verwendet werden soll, über den Parameter relationship.collection_class übergeben werden.
Siehe auch
Anpassen von Sammlungszugriffen - enthält weitere Details zur Sammlungs-Konfiguration, einschließlich einiger Techniken zum Mappen von relationship() zu Dictionaries.
Konfigurieren des Löschverhaltens für Eins zu Viele¶
Es kommt oft vor, dass alle Kind-Objekte gelöscht werden sollen, wenn ihr besitzendes Elternteil gelöscht wird. Um dieses Verhalten zu konfigurieren, wird die unter Löschen beschriebene delete-Kaskadenoption verwendet. Eine zusätzliche Option ist, dass ein Kind-Objekt selbst gelöscht werden kann, wenn es von seinem Elternteil getrennt wird. Dieses Verhalten wird unter delete-orphan beschrieben.
Viele zu Eins¶
Viele zu Eins platziert einen Fremdschlüssel in der Eltern-Tabelle, der auf das Kind verweist. relationship() wird auf der Eltern-Seite deklariert, wo ein neues skalare Attribut erstellt wird.
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
child: Mapped["Child"] = relationship()
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)Das obige Beispiel zeigt eine Viele-zu-Eins-Beziehung, die nicht-nullifizierbares Verhalten annimmt; der nächste Abschnitt, Nullifizierbare Viele zu Eins, illustriert eine nullifizierbare Version.
Bidirektionales Verhalten wird erreicht, indem eine zweite relationship() hinzugefügt und der Parameter relationship.back_populates in beide Richtungen angewendet wird, wobei der Attributname jeder relationship() als Wert für relationship.back_populates auf der anderen Seite verwendet wird.
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
child: Mapped["Child"] = relationship(back_populates="parents")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List["Parent"]] = relationship(back_populates="child")Nullifizierbare Viele zu Eins¶
Im vorherigen Beispiel ist die Beziehung Elternteil.Kind nicht als nullifizierbar typisiert; dies folgt daraus, dass die Spalte Elternteil.Kind_ID selbst nicht nullifizierbar ist, da sie mit Mapped[int] typisiert ist. Wenn wir möchten, dass Elternteil.Kind eine nullifizierbare Viele-zu-Eins-Beziehung ist, können wir sowohl Elternteil.Kind_ID als auch Elternteil.Kind auf Optional[] setzen, in diesem Fall würde die Konfiguration so aussehen:
from typing import Optional
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child_id: Mapped[Optional[int]] = mapped_column(ForeignKey("child_table.id"))
child: Mapped[Optional["Child"]] = relationship(back_populates="parents")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List["Parent"]] = relationship(back_populates="child")Oben wird die Spalte für Elternteil.Kind_ID in DDL so erstellt, dass NULL-Werte zulässig sind. Bei Verwendung von mapped_column() mit expliziten Typdeklarationen ist die Angabe von kind_id: Mapped[Optional[int]] äquivalent zur Einstellung von Column.nullable auf True für die Column, während kind_id: Mapped[int] äquivalent zur Einstellung auf False ist. Siehe mapped_column() leitet den Datentyp und die Nullifizierbarkeit aus der Mapped-Annotation ab für Hintergrundinformationen zu diesem Verhalten.
Tipp
Wenn Python 3.10 oder höher verwendet wird, ist die PEP 604-Syntax bequemer, um optionale Typen mit | None anzugeben, was in Kombination mit PEP 563 verzögerter Auswertung von Annotationen, damit keine Zeichenketten-basierten Typen erforderlich sind, wie folgt aussehen würde:
from __future__ import annotations
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child_id: Mapped[int | None] = mapped_column(ForeignKey("child_table.id"))
child: Mapped[Child | None] = relationship(back_populates="parents")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List[Parent]] = relationship(back_populates="child")Eins zu Eins¶
Eins zu Eins ist im Wesentlichen eine Eins-zu-Viele-Beziehung aus Sicht des Fremdschlüssels, deutet aber darauf hin, dass zu jeder Zeit nur eine Zeile vorhanden sein wird, die sich auf eine bestimmte Elternzeile bezieht.
Bei der Verwendung von annotierten Mappings mit Mapped wird die "Eins-zu-Eins"-Konvention durch die Anwendung eines Nicht-Sammlungstyps auf die Mapped-Annotation auf beiden Seiten der Beziehung erreicht, was dem ORM impliziert, dass auf keiner Seite eine Sammlung verwendet werden soll, wie im folgenden Beispiel:
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child: Mapped["Child"] = relationship(back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="child")Oben, wenn wir ein Elternteil-Objekt laden, wird das Attribut Elternteil.Kind auf ein einzelnes Kind-Objekt verweisen und nicht auf eine Sammlung. Wenn wir den Wert von Elternteil.Kind durch ein neues Kind-Objekt ersetzen, ersetzt der Unit of Work-Prozess des ORM die vorherige Kind-Zeile durch die neue und setzt die Spalte Kind.Elternteil_ID standardmäßig auf NULL, es sei denn, es sind spezifische Kaskaden-Verhaltensweisen eingerichtet.
Tipp
Wie bereits erwähnt, betrachtet das ORM das Muster "Eins-zu-Eins" als eine Konvention, bei der es davon ausgeht, dass es beim Laden des Attributs Elternteil.Kind auf einem Elternteil-Objekt nur eine Zeile zurückerhält. Wenn mehr als eine Zeile zurückgegeben wird, gibt das ORM eine Warnung aus.
Die Seite Kind.Elternteil der obigen Beziehung bleibt jedoch eine "Viele-zu-Eins"-Beziehung. Allein wird sie keine Zuweisung von mehr als einem Kind erkennen, es sei denn, der Parameter relationship.single_parent ist gesetzt, was nützlich sein kann.
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="child", single_parent=True)Unabhängig davon, ob relationship.single_parent verwendet wird, wird die "Eins-zu-Viele"-Seite (die hier konventionsgemäß Eins-zu-Eins ist) nicht zuverlässig erkennen, ob mehr als ein Kind mit einem einzelnen Elternteil verknüpft ist, z. B. wenn mehrere Kind-Objekte ausstehend und nicht datenbankpersistent sind.
Unabhängig davon, ob relationship.single_parent verwendet wird, wird empfohlen, dass das Datenbankschema eine eindeutige Einschränkung enthält, um anzugeben, dass die Spalte Kind.Elternteil_ID eindeutig sein sollte, um auf Datenbankebene sicherzustellen, dass nur eine Kind-Zeile zu jeder Zeit auf eine bestimmte Elternteil-Zeile verweisen kann (siehe Deklarative Tabellenkonfiguration für Hintergrundinformationen zur __table_args__-Tupelsyntax).
from sqlalchemy import UniqueConstraint
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="child")
__table_args__ = (UniqueConstraint("parent_id"),)Neu in Version 2.0: Die relationship()-Konstruktion kann den effektiven Wert des Parameters relationship.uselist aus einer gegebenen Mapped-Annotation ableiten.
Setzen von uselist=False für nicht-annotierte Konfigurationen¶
Bei Verwendung von relationship() ohne den Vorteil von Mapped-Annotationen kann das Eins-zu-Eins-Muster durch Setzen des Parameters relationship.uselist auf False auf der normalerweise "viele"-Seite aktiviert werden, illustriert in einer nicht-annotierten deklarativen Konfiguration unten.
class Parent(Base):
__tablename__ = "parent_table"
id = mapped_column(Integer, primary_key=True)
child = relationship("Child", uselist=False, back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id = mapped_column(Integer, primary_key=True)
parent_id = mapped_column(ForeignKey("parent_table.id"))
parent = relationship("Parent", back_populates="child")Viele zu Viele¶
Viele zu Viele fügt eine Assoziationstabelle zwischen zwei Klassen hinzu. Die Assoziationstabelle wird fast immer als Core Table-Objekt oder eine andere Core-selektierbare wie ein Join-Objekt angegeben und durch das Argument relationship.secondary für relationship() angegeben. Normalerweise verwendet die Table das MetaData-Objekt, das mit der deklarativen Basisklasse verbunden ist, damit die ForeignKey-Direktiven die entfernten Tabellen finden können, mit denen sie verknüpft werden soll.
from __future__ import annotations
from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
# note for a Core table, we use the sqlalchemy.Column construct,
# not sqlalchemy.orm.mapped_column
association_table = Table(
"association_table",
Base.metadata,
Column("left_id", ForeignKey("left_table.id")),
Column("right_id", ForeignKey("right_table.id")),
)
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List[Child]] = relationship(secondary=association_table)
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)Tipp
Die obige "Assoziationstabelle" hat Fremdschlüssel-Einschränkungen, die auf die beiden Entitätstabellen auf beiden Seiten der Beziehung verweisen. Der Datentyp von association.left_id und association.right_id wird normalerweise aus dem der referenzierten Tabelle abgeleitet und kann weggelassen werden. Es wird auch empfohlen, wenn auch nicht durch SQLAlchemy vorgeschrieben, dass die Spalten, die auf die beiden Entitätstabellen verweisen, innerhalb einer eindeutigen Einschränkung oder üblicherweise als Primärschlüsseleinschränkung eingerichtet werden; dies stellt sicher, dass keine doppelten Zeilen in der Tabelle gespeichert werden, unabhängig von Problemen auf der Anwendungsseite.
association_table = Table(
"association_table",
Base.metadata,
Column("left_id", ForeignKey("left_table.id"), primary_key=True),
Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)Bidirektionale Viele zu Viele einrichten¶
Für eine bidirektionale Beziehung enthalten beide Seiten der Beziehung eine Sammlung. Geben Sie dies mit relationship.back_populates an, und geben Sie für jede relationship() die gemeinsame Assoziationstabelle an.
from __future__ import annotations
from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
association_table = Table(
"association_table",
Base.metadata,
Column("left_id", ForeignKey("left_table.id"), primary_key=True),
Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List[Child]] = relationship(
secondary=association_table, back_populates="parents"
)
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List[Parent]] = relationship(
secondary=association_table, back_populates="children"
)Verwendung einer spät ausgewerteten Form für das "secondary"-Argument¶
Der Parameter relationship.secondary von relationship() akzeptiert auch zwei verschiedene "spät ausgewertete" Formen, einschließlich des String-Tabellennamens sowie einer Lambda-Funktion. Siehe den Abschnitt Verwendung einer spät ausgewerteten Form für das "secondary"-Argument von Viele zu Viele für Hintergrundinformationen und Beispiele.
Verwendung von Sets, Listen oder anderen Sammlungstypen für Viele zu Viele¶
Die Konfiguration von Sammlungen für eine Viele-zu-Viele-Beziehung ist identisch mit der von Eins zu Viele, wie unter Verwendung von Sets, Listen oder anderen Sammlungstypen für Eins zu Viele beschrieben. Für ein annotiertes Mapping mit Mapped kann die Sammlung durch die Art der Sammlung, die innerhalb der generischen Klasse Mapped verwendet wird, wie z. B. set, angegeben werden.
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[Set["Child"]] = relationship(secondary=association_table)Bei Verwendung von nicht-annotierten Formen, einschließlich imperativer Mappings, kann, wie im Fall von Eins-zu-Viele, die Python-Klasse, die als Sammlung verwendet werden soll, über den Parameter relationship.collection_class übergeben werden.
Siehe auch
Anpassen von Sammlungszugriffen - enthält weitere Details zur Sammlungs-Konfiguration, einschließlich einiger Techniken zum Mappen von relationship() zu Dictionaries.
Zeilen aus der Viele-zu-Viele-Tabelle löschen¶
Ein für das Argument relationship.secondary zu relationship() einzigartiges Verhalten ist, dass die Table, die hier angegeben ist, automatisch INSERT- und DELETE-Anweisungen unterliegt, wenn Objekte zur Sammlung hinzugefügt oder aus ihr entfernt werden. Es ist nicht notwendig, manuell aus dieser Tabelle zu löschen. Das Entfernen eines Datensatzes aus der Sammlung führt dazu, dass die Zeile beim Spülen gelöscht wird.
# row will be deleted from the "secondary" table
# automatically
myparent.children.remove(somechild)Eine oft aufkommende Frage ist, wie die Zeile in der "sekundären" Tabelle gelöscht werden kann, wenn das Kind-Objekt direkt an Session.delete() übergeben wird.
session.delete(somechild)Hier gibt es mehrere Möglichkeiten:
Wenn es eine Beziehung
ElternteilzuKindgibt, aber keine umgekehrte Beziehung, die ein bestimmtesKindmit jedemElternteilverbindet, wird SQLAlchemy keine Kenntnis davon haben, dass beim Löschen dieses bestimmtenKind-Objekts die "sekundäre" Tabelle, die es mit demElternteilverbindet, beibehalten werden muss. Es erfolgt kein Löschen der "sekundären" Tabelle.Wenn es eine Beziehung gibt, die ein bestimmtes
Kindmit jedemElternteilverbindet, nehmen wir an, sie heißtKind.Elternteile, wird SQLAlchemy standardmäßig die SammlungKind.Elternteileladen, um alleElternteil-Objekte zu lokalisieren und jede Zeile aus der "sekundären" Tabelle zu entfernen, die diese Verbindung herstellt. Beachten Sie, dass diese Beziehung nicht bidirektional sein muss; SQLAlchemy betrachtet streng genommen jederelationship(), die mit dem zu löschendenKind-Objekt verbunden ist.Eine leistungsfähigere Option hier ist die Verwendung von ON DELETE CASCADE-Anweisungen mit den von der Datenbank verwendeten Fremdschlüsseln. Unter der Annahme, dass die Datenbank diese Funktion unterstützt, kann die Datenbank so konfiguriert werden, dass Zeilen in der "sekundären" Tabelle automatisch gelöscht werden, wenn verweisende Zeilen in "Kind" gelöscht werden. SQLAlchemy kann angewiesen werden, das aktive Laden der Sammlung
Kind.Elternteilein diesem Fall zu vermeiden, indem die Direktiverelationship.passive_deletesaufrelationship()angewendet wird; siehe Verwendung von Fremdschlüssel-ON-DELETE-Kaskaden mit ORM-Beziehungen für weitere Details hierzu.
Beachten Sie nochmals, dass diese Verhaltensweisen *nur* für die Option relationship.secondary gelten, die mit relationship() verwendet wird. Wenn Sie mit Assoziationstabellen arbeiten, die explizit gemappt sind und *nicht* in der Option relationship.secondary einer relevanten relationship() vorhanden sind, können stattdessen Kaskadenregeln verwendet werden, um Entitäten automatisch als Reaktion auf das Löschen einer zugehörigen Entität zu löschen - siehe Kaskaden für Informationen zu dieser Funktion.
Assoziations-Objekt¶
Das Muster des Assoziations-Objekts ist eine Variante von Viele-zu-Viele: es wird verwendet, wenn eine Assoziationstabelle zusätzliche Spalten enthält, die über die Fremdschlüssel zu den Eltern- und Kindtabellen (oder linken und rechten Tabellen) hinausgehen, Spalten, die am besten zu ihrer eigenen ORM-gemappten Klasse zugeordnet werden. Diese gemappte Klasse wird gegen die Table gemappt, die ansonsten als relationship.secondary bei Verwendung des Viele-zu-Viele-Musters angegeben würde.
Im Muster des Assoziations-Objekts wird der Parameter relationship.secondary nicht verwendet; stattdessen wird eine Klasse direkt der Assoziationstabelle zugeordnet. Zwei einzelne relationship()-Konstrukte verbinden dann zuerst die Elternseite mit der gemappten Assoziationsklasse über Eins-zu-Viele, und dann die gemappte Assoziationsklasse mit der Kindseite über Viele-zu-Eins, um eine unidirektionale Assoziations-Objekt-Beziehung von Eltern zu Assoziation zu Kind zu bilden. Für eine bidirektionale Beziehung werden vier relationship()-Konstrukte verwendet, um die gemappte Assoziationsklasse bidirektional mit beiden Eltern- und Kindseiten zu verbinden.
Das folgende Beispiel veranschaulicht eine neue Klasse Association, die auf die Table namens association abgebildet wird; diese Tabelle enthält nun eine zusätzliche Spalte namens extra_data, die ein String-Wert ist, der zusammen mit jeder Assoziation zwischen Parent und Child gespeichert wird. Durch die Abbildung der Tabelle auf eine explizite Klasse macht rudimentärer Zugriff von Parent auf Child explizite Verwendung von Association
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class Association(Base):
__tablename__ = "association_table"
left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
right_id: Mapped[int] = mapped_column(
ForeignKey("right_table.id"), primary_key=True
)
extra_data: Mapped[Optional[str]]
child: Mapped["Child"] = relationship()
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Association"]] = relationship()
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)Um die bidirektionale Version zu veranschaulichen, fügen wir zwei weitere relationship()-Konstrukte hinzu, die mit den bestehenden über relationship.back_populates verbunden sind
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class Association(Base):
__tablename__ = "association_table"
left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
right_id: Mapped[int] = mapped_column(
ForeignKey("right_table.id"), primary_key=True
)
extra_data: Mapped[Optional[str]]
child: Mapped["Child"] = relationship(back_populates="parents")
parent: Mapped["Parent"] = relationship(back_populates="children")
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Association"]] = relationship(back_populates="parent")
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List["Association"]] = relationship(back_populates="child")Die Arbeit mit dem Assoziationsmuster in seiner direkten Form erfordert, dass Kindobjekte mit einer Assoziationsinstanz assoziiert werden, bevor sie dem Elternteil hinzugefügt werden; ebenso geht der Zugriff vom Elternteil zum Kind über das Assoziationsobjekt
# create parent, append a child via association
p = Parent()
a = Association(extra_data="some data")
a.child = Child()
p.children.append(a)
# iterate through child objects via association, including association
# attributes
for assoc in p.children:
print(assoc.extra_data)
print(assoc.child)Um das Muster für Assoziationsobjekte zu verbessern, so dass der direkte Zugriff auf das Association-Objekt optional ist, bietet SQLAlchemy die Erweiterung Association Proxy. Diese Erweiterung ermöglicht die Konfiguration von Attributen, die mit einem einzigen Zugriff zwei "Hops" durchlaufen, einen "Hop" zum assoziierten Objekt und einen zweiten zu einem Zielattribut.
Siehe auch
Association Proxy - ermöglicht direkten Zugriff im Stil von "many to many" zwischen Elternteil und Kind für eine Mapping mit drei Klassenobjekten für Assoziationen.
Warnung
Vermeiden Sie es, das Muster für Assoziationsobjekte direkt mit dem Muster für many-to-many zu mischen, da dies zu Bedingungen führt, unter denen Daten inkonsistent gelesen und geschrieben werden können, ohne spezielle Schritte; der association proxy wird typischerweise verwendet, um einen prägnanteren Zugriff zu ermöglichen. Weitere detaillierte Hintergrundinformationen zu den Einschränkungen, die sich aus dieser Kombination ergeben, finden Sie im nächsten Abschnitt Kombination von Assoziationsobjekt und Many-to-Many-Zugriffsmustern.
Kombination von Assoziationsobjekt und Many-to-Many-Zugriffsmustern¶
Wie im vorherigen Abschnitt erwähnt, integriert sich das Muster für Assoziationsobjekte nicht automatisch mit der Verwendung des Many-to-Many-Musters auf denselben Tabellen/Spalten gleichzeitig. Daraus folgt, dass Leseoperationen widersprüchliche Daten zurückgeben können und Schreiboperationen auch versuchen können, widersprüchliche Änderungen zu spülen, was zu Integritätsfehlern oder unerwarteten Einfügungen oder Löschungen führt.
Zur Veranschaulichung konfiguriert das folgende Beispiel eine bidirektionale Many-to-Many-Beziehung zwischen Parent und Child über Parent.children und Child.parents. Gleichzeitig wird auch eine Assoziationsobjekt-Beziehung konfiguriert, zwischen Parent.child_associations -> Association.child und Child.parent_associations -> Association.parent
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class Association(Base):
__tablename__ = "association_table"
left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
right_id: Mapped[int] = mapped_column(
ForeignKey("right_table.id"), primary_key=True
)
extra_data: Mapped[Optional[str]]
# association between Assocation -> Child
child: Mapped["Child"] = relationship(back_populates="parent_associations")
# association between Assocation -> Parent
parent: Mapped["Parent"] = relationship(back_populates="child_associations")
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
# many-to-many relationship to Child, bypassing the `Association` class
children: Mapped[List["Child"]] = relationship(
secondary="association_table", back_populates="parents"
)
# association between Parent -> Association -> Child
child_associations: Mapped[List["Association"]] = relationship(
back_populates="parent"
)
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
# many-to-many relationship to Parent, bypassing the `Association` class
parents: Mapped[List["Parent"]] = relationship(
secondary="association_table", back_populates="children"
)
# association between Child -> Association -> Parent
parent_associations: Mapped[List["Association"]] = relationship(
back_populates="child"
)Beim Verwenden dieses ORM-Modells zum Vornehmen von Änderungen werden Änderungen, die an Parent.children vorgenommen werden, nicht mit Änderungen koordiniert, die an Parent.child_associations oder Child.parent_associations in Python vorgenommen werden; während all diese Beziehungen für sich genommen normal funktionieren, werden Änderungen in einer nicht in einer anderen angezeigt, bis die Session abgelaufen ist, was normalerweise automatisch nach Session.commit() geschieht.
Zusätzlich dazu, wenn widersprüchliche Änderungen vorgenommen werden, wie z.B. das Hinzufügen eines neuen Association-Objekts, während gleichzeitig dasselbe zugehörige Child zu Parent.children hinzugefügt wird, führt dies zu Integritätsfehlern, wenn der Unit-of-Work-Flush-Prozess fortschreitet, wie im folgenden Beispiel
p1 = Parent()
c1 = Child()
p1.children.append(c1)
# redundant, will cause a duplicate INSERT on Association
p1.child_associations.append(Association(child=c1))Das direkte Hinzufügen von Child zu Parent.children impliziert auch die Erstellung von Zeilen in der association-Tabelle, ohne einen Wert für die Spalte association.extra_data anzugeben, die NULL als Wert erhält.
Es ist in Ordnung, ein Mapping wie das obige zu verwenden, wenn Sie wissen, was Sie tun; es kann gute Gründe geben, Many-to-Many-Beziehungen zu verwenden, wenn die Verwendung des Musters "Association Object" selten ist, da es einfacher ist, Beziehungen entlang einer einzigen Many-to-Many-Beziehung zu laden, was auch die Verwendung der "sekundären" Tabelle in SQL-Anweisungen etwas besser optimieren kann, verglichen mit der Verwendung von zwei separaten Beziehungen zu einer expliziten Assoziationsklasse. Es ist zumindest eine gute Idee, den Parameter relationship.viewonly auf die "sekundäre" Beziehung anzuwenden, um das Problem widersprüchlicher Änderungen zu vermeiden und zu verhindern, dass NULL in die zusätzlichen Assoziationsspalten geschrieben wird, wie unten gezeigt
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
# many-to-many relationship to Child, bypassing the `Association` class
children: Mapped[List["Child"]] = relationship(
secondary="association_table", back_populates="parents", viewonly=True
)
# association between Parent -> Association -> Child
child_associations: Mapped[List["Association"]] = relationship(
back_populates="parent"
)
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
# many-to-many relationship to Parent, bypassing the `Association` class
parents: Mapped[List["Parent"]] = relationship(
secondary="association_table", back_populates="children", viewonly=True
)
# association between Child -> Association -> Parent
parent_associations: Mapped[List["Association"]] = relationship(
back_populates="child"
)Das obige Mapping schreibt keine Änderungen an Parent.children oder Child.parents in die Datenbank und verhindert so widersprüchliche Schreibvorgänge. Lesezugriffe auf Parent.children oder Child.parents stimmen jedoch nicht unbedingt mit den Daten überein, die aus Parent.child_associations oder Child.parent_associations gelesen werden, wenn Änderungen an diesen Sammlungen innerhalb derselben Transaktion oder Session vorgenommen werden, in der die Viewonly-Sammlungen gelesen werden. Wenn die Verwendung der Assoziationsobjekt-Beziehungen selten ist und sorgfältig gegen Code organisiert wird, der auf die Many-to-Many-Sammlungen zugreift, um veraltete Lesevorgänge zu vermeiden (in extremen Fällen durch direkte Verwendung von Session.expire(), um Sammlungen innerhalb der aktuellen Transaktion neu zu laden), kann das Muster machbar sein.
Eine beliebte Alternative zum obigen Muster ist eines, bei dem die direkten Many-to-Many-Beziehungen Parent.children und Child.parents durch eine Erweiterung ersetzt werden, die transparent durch die Association-Klasse proxyt, während alles aus Sicht des ORM konsistent bleibt. Diese Erweiterung ist bekannt als Association Proxy.
Siehe auch
Association Proxy - ermöglicht direkten Zugriff im Stil von "many to many" zwischen Elternteil und Kind für eine Mapping mit drei Klassenobjekten für Assoziationen.
Späte Auswertung von Beziehungsargumenten¶
Die meisten Beispiele in den vorherigen Abschnitten veranschaulichen Mappings, bei denen die verschiedenen relationship()-Konstrukte ihre Zielklassen über einen String-Namen referenzieren, anstatt die Klasse selbst. Wenn z.B. Mapped verwendet wird, wird eine Vorwärtsreferenz generiert, die zur Laufzeit nur als String existiert
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(back_populates="parent")
class Child(Base):
# ...
parent: Mapped["Parent"] = relationship(back_populates="children")Ähnlich, wenn nicht-annotierte Formen wie nicht-annotierte deklarative oder imperative Mappings verwendet werden, wird ein String-Name auch direkt vom relationship()-Konstrukt unterstützt
registry.map_imperatively(
Parent,
parent_table,
properties={"children": relationship("Child", back_populates="parent")},
)
registry.map_imperatively(
Child,
child_table,
properties={"parent": relationship("Parent", back_populates="children")},
)Diese String-Namen werden in der Mapper-Resolutionsphase in Klassen aufgelöst, einem internen Prozess, der typischerweise stattfindet, nachdem alle Mappings definiert wurden und normalerweise durch die erste Verwendung der Mappings selbst ausgelöst wird. Das registry-Objekt ist der Container, in dem diese Namen gespeichert und zu den abgebildeten Klassen aufgelöst werden, auf die sie sich beziehen.
Zusätzlich zum Hauptklassenargument für relationship() können andere Argumente, die von den Spalten einer noch undefinierten Klasse abhängen, entweder als Python-Funktionen oder häufiger als Strings angegeben werden. Für die meisten dieser Argumente außer dem Hauptargument werden String-Eingaben **als Python-Ausdrücke mit der integrierten eval()-Funktion von Python ausgewertet**, da sie vollständige SQL-Ausdrücke empfangen sollen.
Warnung
Da die Python-Funktion eval() zur Interpretation der spät ausgewerteten String-Argumente verwendet wird, die an das Konfigurationskonstrukt relationship() übergeben werden, sollten diese Argumente **nicht** zweckentfremdet werden, sodass sie nicht vertrauenswürdige Benutzereingaben erhalten würden; eval() ist **nicht sicher** gegen nicht vertrauenswürdige Benutzereingaben.
Der vollständige Namespace, der in dieser Auswertung verfügbar ist, umfasst alle für diese deklarative Basis abgebildeten Klassen sowie den Inhalt des Pakets sqlalchemy, einschließlich Ausdrucksfunktionen wie desc() und sqlalchemy.sql.functions.func
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(
order_by="desc(Child.email_address)",
primaryjoin="Parent.id == Child.parent_id",
)Für den Fall, dass mehr als ein Modul eine Klasse mit demselben Namen enthält, können String-Klassennamen auch als Modul-qualifizierte Pfade innerhalb dieser String-Ausdrücke angegeben werden
class Parent(Base):
# ...
children: Mapped[List["myapp.mymodel.Child"]] = relationship(
order_by="desc(myapp.mymodel.Child.email_address)",
primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
)In einem Beispiel wie dem obigen kann der String, der an Mapped übergeben wird, von einem spezifischen Klassenargument unterschieden werden, indem der Klassenpfad-String direkt an relationship.argument übergeben wird. Das Folgende veranschaulicht einen reinen Typ-Import für Child, kombiniert mit einem Laufzeit-Spezifizierer für die Zielklasse, der nach dem richtigen Namen innerhalb des registry sucht
import typing
if typing.TYPE_CHECKING:
from myapp.mymodel import Child
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(
"myapp.mymodel.Child",
order_by="desc(myapp.mymodel.Child.email_address)",
primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
)Der qualifizierte Pfad kann jeder Teilpfad sein, der die Mehrdeutigkeit zwischen den Namen aufhebt. Zum Beispiel, um zwischen myapp.model1.Child und myapp.model2.Child zu unterscheiden, können wir model1.Child oder model2.Child angeben
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(
"model1.Child",
order_by="desc(mymodel1.Child.email_address)",
primaryjoin="Parent.id == model1.Child.parent_id",
)Das Konstrukt relationship() akzeptiert auch Python-Funktionen oder Lambdas als Eingabe für diese Argumente. Ein funktionaler Ansatz in Python könnte wie folgt aussehen
import typing
from sqlalchemy import desc
if typing.TYPE_CHECKING:
from myapplication import Child
def _resolve_child_model():
from myapplication import Child
return Child
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(
_resolve_child_model,
order_by=lambda: desc(_resolve_child_model().email_address),
primaryjoin=lambda: Parent.id == _resolve_child_model().parent_id,
)Die vollständige Liste der Parameter, die Python-Funktionen/Lambdas oder Strings akzeptieren, die an eval() übergeben werden, lautet
Warnung
Wie bereits erwähnt, werden die obigen Parameter von relationship() **als Python-Code-Ausdrücke mithilfe von eval() ausgewertet. GEBEN SIE KEINE UNVERTRAULICHEN EINGABEN AN DIESE ARGUMENTE WEITER.**
Hinzufügen von Beziehungen zu abgebildeten Klassen nach der Deklaration¶
Es sollte auch beachtet werden, dass, ähnlich wie unter Hinzufügen zusätzlicher Spalten zu einer bestehenden deklarativ abgebildeten Klasse beschrieben, jedes MapperProperty-Konstrukt jederzeit zu einem deklarativ abgebildeten Basis-Mapping hinzugefügt werden kann (beachten Sie, dass annotierte Formen in diesem Kontext nicht unterstützt werden). Wenn wir diese relationship() implementieren wollten, nachdem die Klasse Address verfügbar war, könnten wir sie auch nachträglich anwenden
# first, module A, where Child has not been created yet,
# we create a Parent class which knows nothing about Child
class Parent(Base): ...
# ... later, in Module B, which is imported after module A:
class Child(Base): ...
from module_a import Parent
# assign the User.addresses relationship as a class variable. The
# declarative base class will intercept this and map the relationship.
Parent.children = relationship(Child, primaryjoin=Child.parent_id == Parent.id)Wie bei ORM-abgebildeten Spalten kann die Mapped-Annotation nicht an dieser Operation teilnehmen. Daher muss die zugehörige Klasse direkt innerhalb des relationship()-Konstrukts angegeben werden, entweder als die Klasse selbst, der String-Name der Klasse oder eine aufrufbare Funktion, die eine Referenz auf die Zielklasse zurückgibt.
Hinweis
Wie bei ORM-abgebildeten Spalten funktioniert die Zuweisung von abgebildeten Eigenschaften zu einer bereits abgebildeten Klasse nur dann korrekt, wenn die Klasse "deklarative Basis" verwendet wird, d.h. die benutzerdefinierte Unterklasse von DeclarativeBase oder die dynamisch generierte Klasse, die von declarative_base() oder registry.generate_base() zurückgegeben wird. Diese "Basis"-Klasse enthält eine Python-Metaklasse, die eine spezielle Methode __setattr__() implementiert, die diese Operationen abfängt.
Die Laufzeitzuweisung von klassen-abgebildeten Attributen zu einer abgebildeten Klasse funktioniert **nicht**, wenn die Klasse mit Decorators wie registry.mapped() oder imperativen Funktionen wie registry.map_imperatively() abgebildet wird.
Verwendung einer spät ausgewerteten Form für das "secondary"-Argument von Many-to-Many¶
Many-to-Many-Beziehungen verwenden den Parameter relationship.secondary, der normalerweise eine Referenz auf ein typischerweise nicht abgebildetes Table-Objekt oder ein anderes Core-Selektions-Objekt angibt. Späte Auswertung mit einem Lambda-Aufruf ist üblich.
Für das Beispiel unter Many To Many, wenn wir davon ausgehen, dass das association_table Table-Objekt später im Modul als die abgebildete Klasse definiert wird, könnten wir die relationship() mit einer Lambda wie folgt schreiben
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(
"Child", secondary=lambda: association_table
)Als Abkürzung für Tabellennamen, die auch **gültige Python-Identifikatoren** sind, kann der Parameter relationship.secondary auch als String übergeben werden, wobei die Auflösung durch Auswertung des Strings als Python-Ausdruck erfolgt, mit einfachen Bezeichnernamen, die mit gleichnamigen Table-Objekten verbunden sind, die sich in derselben MetaData-Sammlung befinden, auf die durch den aktuellen registry verwiesen wird.
Im folgenden Beispiel wird der Ausdruck "association_table" als Variable namens "association_table" ausgewertet, die gegen die Tabellennamen innerhalb der MetaData-Sammlung aufgelöst wird
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(secondary="association_table")Hinweis
Wenn sie als String übergeben wird, **muss** der Name, der an relationship.secondary übergeben wird, **ein gültiger Python-Identifikator** sein, der mit einem Buchstaben beginnt und nur alphanumerische Zeichen oder Unterstriche enthält. Andere Zeichen wie Bindestriche etc. werden als Python-Operatoren interpretiert, die nicht mit dem angegebenen Namen aufgelöst werden. Bitte erwägen Sie die Verwendung von Lambda-Ausdrücken anstelle von Strings für eine bessere Klarheit.
Warnung
Wenn sie als String übergeben wird, wird das Argument relationship.secondary unter Verwendung der Python-Funktion eval() interpretiert, auch wenn es sich typischerweise um den Namen einer Tabelle handelt. **GEBEN SIE KEINE UNVERTRAULICHEN EINGABEN AN DIESEN STRING WEITER.**
Die Designs von flambé! dem Drachen und Der Alchemist wurden von Rotem Yaari erstellt und großzügig gespendet.
Erstellt mit Sphinx 7.2.6. Dokumentation zuletzt generiert: Di 11 Mär 2025 14:40:17 EDT