SQLAlchemy 2.0 Dokumentation
SQLAlchemy Unified Tutorial
- Aufbau der Konnektivität - die Engine
- Arbeiten mit Transaktionen und der DBAPI
- Arbeiten mit Datenbankmetadaten
- Arbeiten mit Daten
- Datenmanipulation mit der ORM
- Arbeiten mit ORM-verknüpften Objekten¶
- Weitere Lektüre
Projektversionen
- Vorheriges: Datenmanipulation mit dem ORM
- Nächste: Weiterführende Literatur
- Nach oben: Startseite
- Auf dieser Seite
Arbeiten mit ORM-verknüpften Objekten¶
In diesem Abschnitt behandeln wir ein weiteres wesentliches ORM-Konzept: wie das ORM mit zugeordneten Klassen interagiert, die auf andere Objekte verweisen. Im Abschnitt Zugeordnete Klassen deklarieren wurden in den Beispielen für zugeordnete Klassen ein Konstrukt namens relationship() verwendet. Dieses Konstrukt definiert eine Verknüpfung zwischen zwei verschiedenen zugeordneten Klassen oder von einer zugeordneten Klasse zu sich selbst, was als selbstreferenzielle Beziehung bezeichnet wird.
Um die grundlegende Idee von relationship() zu beschreiben, überprüfen wir zunächst die Zuordnung in Kurzform und lassen die mapped_column()-Zuordnungen und andere Direktiven weg
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "user_account"
# ... mapped_column() mappings
addresses: Mapped[List["Address"]] = relationship(back_populates="user")
class Address(Base):
__tablename__ = "address"
# ... mapped_column() mappings
user: Mapped["User"] = relationship(back_populates="addresses")Oben hat die Klasse User nun ein Attribut User.addresses und die Klasse Address hat ein Attribut Address.user. Das relationship()-Konstrukt wird, zusammen mit dem Mapped-Konstrukt zur Angabe des Typverhaltens, verwendet, um die Tabellenbeziehungen zwischen den Table-Objekten zu inspizieren, die den Klassen User und Address zugeordnet sind. Da das Table-Objekt, das die address-Tabelle repräsentiert, eine ForeignKeyConstraint aufweist, die auf die user_account-Tabelle verweist, kann relationship() eindeutig bestimmen, dass es eine Eins-zu-viele-Beziehung von der Klasse User zur Klasse Address gibt, entlang der User.addresses-Beziehung; eine bestimmte Zeile in der user_account-Tabelle kann von vielen Zeilen in der address-Tabelle referenziert werden.
Alle Eins-zu-viele-Beziehungen entsprechen natürlich einer Viele-zu-eins-Beziehung in die andere Richtung, in diesem Fall die von Address.user bezeichnete. Der Parameter relationship.back_populates, der oben auf beiden relationship()-Objekten angezeigt wird, die auf den anderen Namen verweisen, stellt sicher, dass jede dieser beiden relationship()-Konstrukte als komplementär zueinander betrachtet werden; wir werden sehen, wie sich dies im nächsten Abschnitt auswirkt.
Beziehungen persistieren und laden¶
Wir können beginnen, indem wir veranschaulichen, was relationship() mit Instanzen von Objekten tut. Wenn wir ein neues User-Objekt erstellen, stellen wir fest, dass beim Zugriff auf das Element .addresses eine Python-Liste vorhanden ist
>>> u1 = User(name="pkrabs", fullname="Pearl Krabs")
>>> u1.addresses
[]Dieses Objekt ist eine SQLAlchemy-spezifische Version einer Python-Liste, die Änderungen verfolgen und darauf reagieren kann. Die Sammlung erschien auch automatisch, als wir auf das Attribut zugegriffen haben, obwohl wir sie dem Objekt nie zugewiesen haben. Dies ähnelt dem Verhalten, das im Abschnitt Zeilen mit dem ORM Unit of Work-Muster einfügen beschrieben wurde, wo beobachtet wurde, dass spaltenbasierte Attribute, denen wir keinen expliziten Wert zuweisen, ebenfalls automatisch als None angezeigt werden, anstatt einen AttributeError auszulösen, wie es das übliche Verhalten von Python wäre.
Da das Objekt u1 immer noch transient ist und die Liste, die wir von u1.addresses erhalten haben, nicht mutiert wurde (d. h. Elemente wurden nicht angehängt oder erweitert), ist sie noch nicht tatsächlich mit dem Objekt verknüpft. Wenn wir jedoch Änderungen daran vornehmen, wird sie Teil des Zustands des User-Objekts.
Die Sammlung ist spezifisch für die Klasse Address, da dies der einzige Typ von Python-Objekt ist, der darin gespeichert werden kann. Mit der Methode list.append() können wir ein Address-Objekt hinzufügen
>>> a1 = Address(email_address="pearl.krabs@gmail.com")
>>> u1.addresses.append(a1)An diesem Punkt enthält die Sammlung u1.addresses wie erwartet das neue Address-Objekt
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com')]Als wir das Address-Objekt mit der Sammlung User.addresses der Instanz u1 verknüpft haben, trat ein weiteres Verhalten auf: die Beziehung User.addresses synchronisierte sich mit der Beziehung Address.user. So können wir nicht nur vom User-Objekt zum Address-Objekt navigieren, sondern auch vom Address-Objekt zurück zum „Eltern“-User-Objekt
>>> a1.user
User(id=None, name='pkrabs', fullname='Pearl Krabs')Diese Synchronisierung erfolgte als Ergebnis der Verwendung des Parameters relationship.back_populates zwischen den beiden relationship()-Objekten. Dieser Parameter benennt eine weitere relationship(), für die komplementäre Attributzuweisung / Listenmutation stattfinden soll. Sie funktioniert gleichermaßen in die andere Richtung: Wenn wir ein weiteres Address-Objekt erstellen und seinem Attribut Address.user zuweisen, wird dieses Address Teil der Sammlung User.addresses auf diesem User-Objekt
>>> a2 = Address(email_address="pearl@aol.com", user=u1)
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')]Wir haben tatsächlich den Parameter user als Schlüsselwortargument im Konstruktor von Address verwendet, was genau wie jedes andere zugeordnete Attribut akzeptiert wird, das auf der Klasse Address deklariert wurde. Es ist gleichwertig mit der nachträglichen Zuweisung des Attributs Address.user
# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1Objekte in die Sitzung kaskadieren¶
Wir haben jetzt ein User- und zwei Address-Objekte, die in einem bidirektionalen Gebilde im Speicher verknüpft sind. Wie bereits in Zeilen mit dem ORM Unit of Work-Muster einfügen erwähnt, befinden sich diese Objekte im transienten Zustand, bis sie einem Session-Objekt zugeordnet werden.
Wir verwenden die noch laufende Session und stellen fest, dass beim Aufruf der Methode Session.add() auf das führende User-Objekt auch das zugehörige Address-Objekt zu derselben Session hinzugefügt wird
>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
TrueDas obige Verhalten, bei dem die Session ein User-Objekt empfing und der Beziehung User.addresses folgte, um ein zugehöriges Address-Objekt zu finden, wird als save-update-Kaskade bezeichnet und im ORM-Referenzdokument unter Kaskaden ausführlich erläutert.
Die drei Objekte befinden sich nun im pending-Zustand; das bedeutet, sie sind bereit, einer INSERT-Operation unterzogen zu werden, aber dies ist noch nicht geschehen. Alle drei Objekte haben noch keinen Primärschlüssel zugewiesen, und zusätzlich haben die Objekte a1 und a2 ein Attribut namens user_id, das auf die Column verweist, die eine ForeignKeyConstraint aufweist, die auf die Spalte user_account.id verweist. Diese sind ebenfalls None, da die Objekte noch nicht mit einer realen Datenbankzeile verknüpft sind.
>>> print(u1.id)
None
>>> print(a1.user_id)
NoneHier erkennen wir den sehr großen Nutzen, den der Unit-of-Work-Prozess bietet. Erinnern Sie sich, dass im Abschnitt INSERT generiert normalerweise automatisch die „VALUES“-Klausel Zeilen in die Tabellen user_account und address eingefügt wurden, wobei einige ausgeklügelte Syntax verwendet wurde, um die Spalten address.user_id automatisch mit denen der user_account-Zeilen zu verknüpfen. Außerdem war es notwendig, zuerst INSERT für user_account-Zeilen auszugeben, bevor die für address, da Zeilen in address für einen Wert in ihrer user_id-Spalte von ihrer Elter-Zeile in user_account abhängig sind.
Bei Verwendung der Session wird all dieser Aufwand für uns erledigt, und selbst die eingefleischtesten SQL-Puristen können von der Automatisierung von INSERT-, UPDATE- und DELETE-Anweisungen profitieren. Wenn wir die Transaktion Session.commit(), werden alle Schritte in der richtigen Reihenfolge aufgerufen, und außerdem wird der neu generierte Primärschlüssel der user_account-Zeile entsprechend der address.user_id-Spalte zugewiesen.
>>> session.commit()
INSERT INTO user_account (name, fullname) VALUES (?, ?)
[...] ('pkrabs', 'Pearl Krabs')
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[... (insertmanyvalues) 1/2 (ordered; batch not supported)] ('pearl.krabs@gmail.com', 6)
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[insertmanyvalues 2/2 (ordered; batch not supported)] ('pearl@aol.com', 6)
COMMIT
Beziehungen laden¶
Im letzten Schritt haben wir Session.commit() aufgerufen, was ein COMMIT für die Transaktion ausgelöst hat. Danach wurden gemäß Session.commit.expire_on_commit alle Objekte abgelaufen (expired), damit sie für die nächste Transaktion neu geladen werden.
Wenn wir im nächsten Schritt auf ein Attribut dieser Objekte zugreifen, sehen wir die SELECT-Abfrage, die für die primären Attribute der Zeile ausgelöst wird, z. B. wenn wir den neu generierten Primärschlüssel für das Objekt u1 betrachten
>>> u1.id
BEGIN (implicit)
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name,
user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (6,)
6Das u1 User-Objekt hat nun eine persistente Sammlung User.addresses, auf die wir ebenfalls zugreifen können. Da diese Sammlung aus zusätzlichen Zeilen aus der address-Tabelle besteht, sehen wir beim Zugriff auf diese Sammlung erneut einen lazy load, um die Objekte abzurufen.
>>> u1.addresses
SELECT address.id AS address_id, address.email_address AS address_email_address,
address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (6,)
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]Sammlungen und zugehörige Attribute im SQLAlchemy ORM sind im Speicher persistent. Sobald die Sammlung oder das Attribut gefüllt ist, wird keine SQL-Abfrage mehr ausgeführt, bis diese Sammlung oder dieses Attribut abgelaufen ist. Wir können erneut auf u1.addresses zugreifen sowie Elemente hinzufügen oder entfernen, und dies wird keine neuen SQL-Aufrufe verursachen.
>>> u1.addresses
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]Obwohl das durch Lazy Loading ausgelöste Laden schnell teuer werden kann, wenn wir keine expliziten Schritte zur Optimierung unternehmen, ist das Netzwerk des Lazy Loadings zumindest recht gut optimiert, um keine redundanten Arbeiten zu leisten. Da die Sammlung u1.addresses gemäß der Identitätszuordnung neu geladen wurde, handelt es sich tatsächlich um dieselben Address-Instanzen wie die a1 und a2-Objekte, mit denen wir bereits gearbeitet haben. Daher sind wir mit dem Laden aller Attribute im aktuellen Objektgraphen fertig.
>>> a1
Address(id=4, email_address='pearl.krabs@gmail.com')
>>> a2
Address(id=5, email_address='pearl@aol.com')Die Frage, wie Beziehungen geladen werden oder nicht, ist ein eigenes Thema. Eine zusätzliche Einführung in diese Konzepte finden Sie später in diesem Abschnitt unter Laderstrategien.
Beziehungen in Abfragen verwenden¶
Der vorherige Abschnitt stellte das Verhalten des relationship()-Konstrukts bei der Arbeit mit Instanzen einer zugeordneten Klasse vor, oben die Instanzen u1, a1 und a2 der Klassen User und Address. In diesem Abschnitt stellen wir das Verhalten von relationship() vor, wie es auf Klassenlevel-Verhalten einer zugeordneten Klasse angewendet wird, wo es auf verschiedene Weise zur Automatisierung der Konstruktion von SQL-Abfragen dient.
Beziehungen zum Verknüpfen verwenden¶
Die Abschnitte Explizite FROM-Klauseln und JOINs und ON-Klausel festlegen führten die Verwendung der Methoden Select.join() und Select.join_from() zur Komposition von SQL JOIN-Klauseln ein. Um zu beschreiben, wie Tabellen verknüpft werden, inferieren diese Methoden entweder die ON-Klausel basierend auf dem Vorhandensein eines einzelnen, eindeutigen ForeignKeyConstraint-Objekts innerhalb der Tabellenmetadatenstruktur, die die beiden Tabellen verknüpft, oder wir können explizit ein SQL-Ausdruckskonstrukt bereitstellen, das eine spezifische ON-Klausel angibt.
Bei Verwendung von ORM-Entitäten steht ein zusätzlicher Mechanismus zur Verfügung, der uns hilft, die ON-Klausel eines Joins einzurichten: die Verwendung der relationship()-Objekte, die wir in unserer Benutzerzuordnung eingerichtet haben, wie unter Zugeordnete Klassen deklarieren gezeigt. Das klassengebundene Attribut, das der relationship() entspricht, kann als einzelnes Argument an Select.join() übergeben werden, wo es sowohl die rechte Seite des Joins als auch die ON-Klausel gleichzeitig angibt.
>>> print(select(Address.email_address).select_from(User).join(User.addresses))
SELECT address.email_address
FROM user_account JOIN address ON user_account.id = address.user_id
Das Vorhandensein einer ORM- relationship() in einer Zuordnung wird von Select.join() oder Select.join_from() nicht zur Ableitung der ON-Klausel verwendet, wenn wir sie nicht angeben. Das bedeutet, wenn wir von User zu Address ohne ON-Klausel verknüpfen, funktioniert dies aufgrund der ForeignKeyConstraint zwischen den beiden zugeordneten Table-Objekten, nicht wegen der relationship()-Objekte auf den Klassen User und Address.
>>> print(select(Address.email_address).join_from(User, Address))
SELECT address.email_address
FROM user_account JOIN address ON user_account.id = address.user_id
Siehe den Abschnitt Joins im ORM Querying Guide für viele weitere Beispiele, wie Select.join() und Select.join_from() mit relationship()-Konstrukten verwendet werden.
Siehe auch
Beziehungs-WHERE-Operatoren¶
Es gibt einige zusätzliche Varianten von SQL-Generierungshilfen, die mit relationship() geliefert werden und die typischerweise nützlich sind, wenn die WHERE-Klausel einer Anweisung aufgebaut wird. Siehe den Abschnitt Beziehungs-WHERE-Operatoren im ORM Querying Guide.
Siehe auch
Laderstrategien¶
Im Abschnitt Beziehungen laden haben wir das Konzept eingeführt, dass beim Arbeiten mit Instanzen von zugeordneten Objekten der Zugriff auf Attribute, die mit relationship() zugeordnet sind, im Standardfall einen lazy load auslöst, wenn die Sammlung nicht gefüllt ist, um die Objekte zu laden, die in dieser Sammlung vorhanden sein sollten.
Lazy Loading ist eines der bekanntesten ORM-Muster und auch das umstrittenste. Wenn mehrere Dutzend ORM-Objekte im Speicher jeweils auf eine Handvoll ungeladener Attribute verweisen, können routinemäßige Manipulationen dieser Objekte viele zusätzliche Abfragen auslösen, die sich summieren können (auch bekannt als das N-plus-one-Problem). Und schlimmer noch, sie werden implizit ausgelöst. Diese impliziten Abfragen werden möglicherweise nicht bemerkt, können Fehler verursachen, wenn sie versucht werden, nachdem keine Datenbanktransaktion mehr verfügbar ist, oder funktionieren bei Verwendung alternativer Nebenläufigkeitsmuster wie asyncio überhaupt nicht.
Gleichzeitig ist Lazy Loading ein äußerst beliebtes und nützliches Muster, wenn es mit dem verwendeten Nebenläufigkeitsansatz kompatibel ist und ansonsten keine Probleme verursacht. Aus diesen Gründen legt SQLAlchemy viel Wert darauf, dieses Ladeverhalten steuern und optimieren zu können.
Vor allem ist der erste Schritt zur effektiven Nutzung von ORM Lazy Loading: Testen Sie die Anwendung, schalten Sie die SQL-Ausgabe ein und beobachten Sie die generierte SQL-Abfrage. Wenn es viele redundante SELECT-Anweisungen zu geben scheint, die sehr gut zu einer einzigen, effizienteren Abfrage zusammengefasst werden könnten, oder wenn das Laden unangemessen für Objekte erfolgt, die von ihrer Session detached wurden, dann sollten Sie über die Verwendung von Laderstrategien nachdenken.
Laderstrategien werden als Objekte dargestellt, die mit einer SELECT-Anweisung über die Methode Select.options() verknüpft werden können, z. B.
for user_obj in session.execute(
select(User).options(selectinload(User.addresses))
).scalars():
user_obj.addresses # access addresses collection already loadedSie können auch als Standardwerte für eine relationship() über die Option relationship.lazy konfiguriert werden, z. B.
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "user_account"
addresses: Mapped[List["Address"]] = relationship(
back_populates="user", lazy="selectin"
)Jedes Laderstrategie-Objekt fügt der Anweisung einige Informationen hinzu, die später von der Session verwendet werden, wenn sie entscheidet, wie verschiedene Attribute geladen werden sollen und/oder wie sie sich verhalten sollen, wenn darauf zugegriffen wird.
Die folgenden Abschnitte stellen einige der am häufigsten verwendeten Laderstrategien vor.
Siehe auch
Zwei Abschnitte in Beziehungsladetechniken
Laderstrategien beim Zuordnungszeitpunkt konfigurieren – Details zur Konfiguration der Strategie auf
relationship()Beziehungsladung mit Laderoptionen – Details zur Verwendung von Laderstrategien zur Abfragezeit
Selectin-Laden¶
Die nützlichste Laderstrategie im modernen SQLAlchemy ist die Laderoption selectinload(). Diese Option löst die häufigste Form des „N-plus-one“-Problems, nämlich eine Menge von Objekten, die auf zugehörige Sammlungen verweisen. selectinload() stellt sicher, dass eine bestimmte Sammlung für eine ganze Reihe von Objekten im Voraus mit einer einzigen Abfrage geladen wird. Dies geschieht mithilfe einer SELECT-Form, die in den meisten Fällen allein gegen die zugehörige Tabelle ausgeführt werden kann, ohne die Einführung von JOINs oder Unterabfragen, und nur für die übergeordneten Objekte abfragt, für die die Sammlung noch nicht geladen ist. Unten illustrieren wir selectinload() durch das Laden aller User-Objekte und all ihrer zugehörigen Address-Objekte; während wir Session.execute() nur einmal aufrufen, wobei eine select()-Konstruktion gegeben ist, werden beim Zugriff auf die Datenbank tatsächlich zwei SELECT-Anweisungen ausgegeben, wobei die zweite zum Abrufen der zugehörigen Address-Objekte dient.
>>> from sqlalchemy.orm import selectinload
>>> stmt = select(User).options(selectinload(User.addresses)).order_by(User.id)
>>> for row in session.execute(stmt):
... print(
... f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})"
... )
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account ORDER BY user_account.id
[...] ()
SELECT address.user_id AS address_user_id, address.id AS address_id,
address.email_address AS address_email_address
FROM address
WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
[...] (1, 2, 3, 4, 5, 6)
spongebob (spongebob@sqlalchemy.org)
sandy (sandy@sqlalchemy.org, sandy@squirrelpower.org)
patrick ()
squidward ()
ehkrabs ()
pkrabs (pearl.krabs@gmail.com, pearl@aol.com)Siehe auch
Verknüpftes Laden¶
Die Eager-Loading-Strategie joinedload() ist die älteste Eager-Loader-Strategie in SQLAlchemy. Sie erweitert die an die Datenbank übergebene SELECT-Anweisung um einen JOIN (der je nach Optionen ein Outer- oder Inner-Join sein kann), mit dem dann zugehörige Objekte geladen werden können.
Die Strategie joinedload() eignet sich am besten zum Laden von zugehörigen Many-to-One-Objekten, da hierfür nur zusätzliche Spalten zu einer primären Entitätszeile hinzugefügt werden müssen, die ohnehin abgerufen würde. Zur größeren Effizienz akzeptiert sie auch eine Option joinedload.innerjoin, sodass ein Inner-Join anstelle eines Outer-Joins verwendet werden kann, z. B. in Fällen, in denen wir wissen, dass alle Address-Objekte einen zugehörigen User haben.
>>> from sqlalchemy.orm import joinedload
>>> stmt = (
... select(Address)
... .options(joinedload(Address.user, innerjoin=True))
... .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
... print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT address.id, address.email_address, address.user_id, user_account_1.id AS id_1,
user_account_1.name, user_account_1.fullname
FROM address
JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
ORDER BY address.id
[...] ()
spongebob@sqlalchemy.org spongebob
sandy@sqlalchemy.org sandy
sandy@squirrelpower.org sandy
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabsjoinedload() funktioniert auch für Collections, d. h. für Eins-zu-Viele-Beziehungen. Es hat jedoch den Effekt, dass die Primärzeilen pro verwandtem Element auf rekursive Weise multipliziert werden, wodurch die Datenmenge, die für einen Ergebnisdatensatz gesendet wird, bei verschachtelten Collections und/oder größeren Collections um Größenordnungen ansteigt. Daher sollte die Verwendung gegenüber anderen Optionen wie selectinload() von Fall zu Fall bewertet werden.
Es ist wichtig zu beachten, dass die WHERE- und ORDER-BY-Kriterien der umschließenden Select-Anweisung **nicht auf die von joinedload() gerenderte Tabelle abzielen**. Oben ist im SQL zu sehen, dass der Tabelle user_account ein **anonymer Alias** zugewiesen wird, sodass sie in der Abfrage nicht direkt adressierbar ist. Dieses Konzept wird im Abschnitt The Zen of Joined Eager Loading detaillierter erläutert.
Tipp
Es ist wichtig zu beachten, dass Many-to-One-Eager-Loads oft nicht notwendig sind, da das "N plus One"-Problem im gängigen Fall viel seltener auftritt. Wenn viele Objekte auf dasselbe verwandte Objekt verweisen, z. B. viele Address-Objekte, die jeweils auf dasselbe User-Objekt verweisen, wird für dieses User-Objekt bei normalem Lazy Loading nur einmal SQL ausgegeben. Die Lazy-Load-Routine sucht das verwandte Objekt anhand des Primärschlüssels in der aktuellen Session und gibt, wenn möglich, kein SQL aus.
Siehe auch
Explizites Join + Eager-Load¶
Wenn wir Address-Zeilen laden und dabei mit der Tabelle user_account über eine Methode wie Select.join() joinen würden, um den JOIN zu rendern, könnten wir diesen JOIN auch nutzen, um die Inhalte des Attributs Address.user auf jedem zurückgegebenen Address-Objekt eager zu laden. Dies ist im Wesentlichen "joined eager loading", aber wir rendern den JOIN selbst. Dieser gängige Anwendungsfall wird durch die Option contains_eager() erreicht. Diese Option ähnelt stark joinedload(), außer dass sie davon ausgeht, dass wir den JOIN selbst eingerichtet haben, und stattdessen nur angibt, dass zusätzliche Spalten in der COLUMNS-Klausel in die verwandten Attribute jedes zurückgegebenen Objekts geladen werden sollen, zum Beispiel
>>> from sqlalchemy.orm import contains_eager
>>> stmt = (
... select(Address)
... .join(Address.user)
... .where(User.name == "pkrabs")
... .options(contains_eager(Address.user))
... .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
... print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT user_account.id, user_account.name, user_account.fullname,
address.id AS id_1, address.email_address, address.user_id
FROM address JOIN user_account ON user_account.id = address.user_id
WHERE user_account.name = ? ORDER BY address.id
[...] ('pkrabs',)
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabsOben haben wir sowohl die Zeilen nach user_account.name gefiltert als auch Zeilen aus user_account in das Attribut Address.user der zurückgegebenen Zeilen geladen. Wenn wir joinedload() separat angewendet hätten, hätten wir eine SQL-Abfrage erhalten, die unnötigerweise zweimal joined.
>>> stmt = (
... select(Address)
... .join(Address.user)
... .where(User.name == "pkrabs")
... .options(joinedload(Address.user))
... .order_by(Address.id)
... )
>>> print(stmt) # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily
SELECT address.id, address.email_address, address.user_id,
user_account_1.id AS id_1, user_account_1.name, user_account_1.fullname
FROM address JOIN user_account ON user_account.id = address.user_id
LEFT OUTER JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
WHERE user_account.name = :name_1 ORDER BY address.id
Siehe auch
Zwei Abschnitte in Beziehungsladetechniken
The Zen of Joined Eager Loading - beschreibt das obige Problem im Detail
Routing Explicit Joins/Statements into Eagerly Loaded Collections - Verwendung von
contains_eager()
Raiseload¶
Eine weitere erwähnenswerte Laderstrategie ist raiseload(). Diese Option wird verwendet, um zu verhindern, dass eine Anwendung überhaupt mit dem N plus One-Problem konfrontiert wird, indem ein normalerweise latentes Laden stattdessen einen Fehler auslöst. Sie verfügt über zwei Varianten, die über die Option raiseload.sql_only gesteuert werden, um entweder latente Ladevorgänge, die SQL erfordern, oder alle "Ladevorgänge", einschließlich derer, die nur die aktuelle Session konsultieren müssen, zu blockieren.
Eine Möglichkeit, raiseload() zu verwenden, ist die direkte Konfiguration in relationship(), indem relationship.lazy auf den Wert "raise_on_sql" gesetzt wird, so dass für eine bestimmte Zuordnung eine bestimmte Beziehung niemals versuchen wird, SQL auszugeben.
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import relationship
>>> class User(Base):
... __tablename__ = "user_account"
... id: Mapped[int] = mapped_column(primary_key=True)
... addresses: Mapped[List["Address"]] = relationship(
... back_populates="user", lazy="raise_on_sql"
... )
>>> class Address(Base):
... __tablename__ = "address"
... id: Mapped[int] = mapped_column(primary_key=True)
... user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
... user: Mapped["User"] = relationship(back_populates="addresses", lazy="raise_on_sql")Mit einer solchen Zuordnung wird die Anwendung am Lazy Loading gehindert, was darauf hindeutet, dass eine bestimmte Abfrage eine Laderstrategie angeben müsste.
>>> u1 = session.execute(select(User)).scalars().first()
SELECT user_account.id FROM user_account
[...] ()
>>> u1.addresses
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql'Die Ausnahme würde darauf hinweisen, dass diese Collection stattdessen im Voraus geladen werden sollte.
>>> u1 = (
... session.execute(select(User).options(selectinload(User.addresses)))
... .scalars()
... .first()
... )
SELECT user_account.id
FROM user_account
[...] ()
SELECT address.user_id AS address_user_id, address.id AS address_id
FROM address
WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
[...] (1, 2, 3, 4, 5, 6)
Die Option lazy="raise_on_sql" versucht auch bei Many-to-One-Beziehungen intelligent zu sein; wenn das Attribut Address.user eines Address-Objekts nicht geladen worden wäre, aber dieses User-Objekt lokal in derselben Session vorhanden wäre, würde die "raiseload"-Strategie keinen Fehler auslösen.
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