SQLAlchemy 2.0 Dokumentation
SQLAlchemy ORM
- ORM Schnellstart
- ORM Abgebildete Klassenkonfiguration
- Beziehungskonfiguration
- ORM Abfragehandbuch
- Verwendung der Sitzung
- Ereignisse und Interna
- ORM Erweiterungen
- Asynchrone E/A (asyncio)
- Assoziationsproxy
- Automap
- Baked Queries
- Deklarative Erweiterungen
- Mypy / Pep-484 Unterstützung für ORM-Mappings¶
- Mutationsverfolgung
- Sortierungsliste
- Horizontale Sharding
- Hybrid-Attribute
- Indexierbar
- Alternative Klassen-Instrumentierung
- ORM Beispiele
Projektversionen
- Vorher: Deklarative Erweiterungen
- Nächste: Mutationsverfolgung
- Oben: Startseite
- Auf dieser Seite
Mypy / Pep-484 Unterstützung für ORM-Mappings¶
Unterstützung für PEP 484-Typannotationen sowie das MyPy-Typüberprüfungswerkzeug bei Verwendung von SQLAlchemy deklarativen Mappings, die direkt auf das Column-Objekt verweisen, anstatt auf die in SQLAlchemy 2.0 eingeführte Konstruktion mapped_column().
Seit Version 2.0 veraltet: Das SQLAlchemy Mypy Plugin ist VERALTET und wird in der SQLAlchemy 2.1-Version entfernt. Wir bitten die Benutzer dringend, es so schnell wie möglich zu migrieren. Das mypy-Plugin funktioniert auch nur bis mypy Version 1.10.1. Version 1.11.0 und höher funktionieren möglicherweise nicht ordnungsgemäß.
Dieses Plugin kann nicht über ständig wechselnde Versionen von mypy hinweg gewartet werden und seine zukünftige Stabilität KANN NICHT garantiert werden.
Modernes SQLAlchemy bietet jetzt vollständige PEP-484-konforme Mapping-Syntaxen; siehe den verlinkten Abschnitt für Migrationsdetails.
Installation¶
Nur für SQLAlchemy 2.0: Es sollten keine Stubs installiert werden und Pakete wie sqlalchemy-stubs und sqlalchemy2-stubs sollten vollständig deinstalliert werden.
Das Mypy-Paket selbst ist eine Abhängigkeit.
Mypy kann über die "mypy"-Extras-Hook mit pip installiert werden
pip install sqlalchemy[mypy]Das Plugin selbst wird wie in Konfigurieren von mypy zur Verwendung von Plugins beschrieben konfiguriert, unter Verwendung des Modulnamens sqlalchemy.ext.mypy.plugin, beispielsweise in setup.cfg
[mypy]
plugins = sqlalchemy.ext.mypy.pluginWas das Plugin tut¶
Der Hauptzweck des Mypy-Plugins ist es, die statische Definition von SQLAlchemy deklarativen Mappings abzufangen und zu ändern, damit sie mit der Art und Weise übereinstimmen, wie sie nach der Instrumentierung durch ihre Mapper-Objekte strukturiert sind. Dies ermöglicht es sowohl der Klassenstruktur selbst als auch dem Code, der die Klasse verwendet, für das Mypy-Werkzeug verständlich zu sein, was ansonsten aufgrund der Funktionsweise von deklarativen Mappings nicht der Fall wäre. Das Plugin ist nicht unähnlich zu ähnlichen Plugins, die für Bibliotheken wie Dataclasses erforderlich sind, die Klassen zur Laufzeit dynamisch ändern.
Um die wichtigsten Bereiche abzudecken, in denen dies auftritt, betrachten Sie das folgende ORM-Mapping, das typische Beispiel der User-Klasse
from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import declarative_base
# "Base" is a class that is created dynamically from the
# declarative_base() function
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
# "some_user" is an instance of the User class, which
# accepts "id" and "name" kwargs based on the mapping
some_user = User(id=5, name="user")
# it has an attribute called .name that's a string
print(f"Username: {some_user.name}")
# a select() construct makes use of SQL expressions derived from the
# User class itself
select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))Die Schritte, die die Mypy-Erweiterung oben ausführen kann, umfassen:
Interpretation der von
declarative_base()generierten dynamischenBase-Klasse, sodass Klassen, die von ihr erben, als gemappt erkannt werden. Es kann auch den Klassen-Decorator-Ansatz unterstützen, der unter Deklaratives Mapping mittels eines Decorators (ohne deklarative Basis) beschrieben wird.Typinferenz für ORM-gemappte Attribute, die im deklarativen "Inline"-Stil definiert sind, im obigen Beispiel die Attribute
idundnamederUser-Klasse. Dies beinhaltet, dass eine Instanz vonUserintfüridundstrfürnameverwendet. Dies beinhaltet auch, dass, wenn auf die KlassenattributeUser.idundUser.namezugegriffen wird, wie oben in derselect()-Anweisung, sie mit dem SQL-Ausdrucksverhalten kompatibel sind, das von der AttributdeskriptorklasseInstrumentedAttributeabgeleitet wird.Anwendung einer
__init__()-Methode auf gemappte Klassen, die noch keinen expliziten Konstruktor enthalten, der Schlüsselwörter mit spezifischen Typen für alle erkannten gemappten Attribute akzeptiert.
Wenn das Mypy-Plugin die obige Datei verarbeitet, ist die resultierende statische Klassendefinition und der Python-Code, der dem Mypy-Tool übergeben wird, äquivalent zu folgendem:
from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import Mapped
from sqlalchemy.orm.decl_api import DeclarativeMeta
class Base(metaclass=DeclarativeMeta):
__abstract__ = True
class User(Base):
__tablename__ = "user"
id: Mapped[Optional[int]] = Mapped._special_method(
Column(Integer, primary_key=True)
)
name: Mapped[Optional[str]] = Mapped._special_method(Column(String))
def __init__(self, id: Optional[int] = ..., name: Optional[str] = ...) -> None: ...
some_user = User(id=5, name="user")
print(f"Username: {some_user.name}")
select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))Die wichtigsten Schritte, die oben durchgeführt wurden, umfassen:
Die
Base-Klasse wird nun explizit im Sinne derDeclarativeMeta-Klasse definiert, anstatt eine dynamische Klasse zu sein.Die Attribute
idundnamewerden im Sinne derMapped-Klasse definiert, die einen Python-Deskriptor darstellt, der unterschiedliche Verhaltensweisen auf Klassen- vs. Instanzebene aufweist. DieMapped-Klasse ist nun die Basisklasse für dieInstrumentedAttribute-Klasse, die für alle ORM-gemappten Attribute verwendet wird.Mappedist als generische Klasse für beliebige Python-Typen definiert, was bedeutet, dass spezifische Vorkommen vonMappedmit einem spezifischen Python-Typ verbunden sind, wie z. B.Mapped[Optional[int]]undMapped[Optional[str]]oben.Die rechte Seite der Zuweisungen von deklarativ gemappten Attributen wird entfernt, da dies der Operation ähnelt, die die
Mapper-Klasse normalerweise tun würde, nämlich dass sie diese Attribute durch spezifische Instanzen vonInstrumentedAttributeersetzen würde. Der ursprüngliche Ausdruck wird in einen Funktionsaufruf verschoben, der es ermöglicht, ihn weiterhin typgeprüft zu lassen, ohne mit der linken Seite des Ausdrucks zu kollidieren. Für Mypy-Zwecke ist die linke Typannotation ausreichend, damit das Verhalten des Attributs verstanden wird.Ein Typ-Stub für die
User.__init__()-Methode wird hinzugefügt, die die korrekten Schlüsselwörter und Datentypen enthält.
Verwendung¶
Die folgenden Unterabschnitte behandeln einzelne Anwendungsfälle, die bisher für die PEP-484-Konformität in Betracht gezogen wurden.
Introspektion von Spalten basierend auf TypeEngine¶
Für gemappte Spalten, die einen expliziten Datentyp enthalten, wird der gemappte Typ automatisch introspektiert, wenn sie als Inline-Attribute gemappt werden.
class MyClass(Base):
# ...
id = Column(Integer, primary_key=True)
name = Column("employee_name", String(50), nullable=False)
other_name = Column(String(50))Oben werden die endgültigen Klassenebene-Datentypen von id, name und other_name als Mapped[Optional[int]], Mapped[Optional[str]] und Mapped[Optional[str]] introspektiert. Die Typen werden standardmäßig immer als Optional betrachtet, selbst für den Primärschlüssel und die nicht-nullbare Spalte. Der Grund dafür ist, dass die Datenbankspalten "id" und "name" zwar nicht NULL sein können, die Python-Attribute id und name jedoch sehr wohl None sein können, wenn kein expliziter Konstruktor vorhanden ist.
>>> m1 = MyClass()
>>> m1.id
NoneDie Typen der obigen Spalten können explizit angegeben werden, was die zwei Vorteile einer klareren Selbstdokumentation bietet und auch die Kontrolle darüber ermöglicht, welche Typen optional sind.
class MyClass(Base):
# ...
id: int = Column(Integer, primary_key=True)
name: str = Column("employee_name", String(50), nullable=False)
other_name: Optional[str] = Column(String(50))Das Mypy-Plugin akzeptiert die obigen int, str und Optional[str] und konvertiert sie, um den Mapped[]-Typ um sie herum einzuschließen. Die Mapped[]-Konstruktion kann auch explizit verwendet werden.
from sqlalchemy.orm import Mapped
class MyClass(Base):
# ...
id: Mapped[int] = Column(Integer, primary_key=True)
name: Mapped[str] = Column("employee_name", String(50), nullable=False)
other_name: Mapped[Optional[str]] = Column(String(50))Wenn der Typ nicht-optional ist, bedeutet dies einfach, dass das Attribut, wie es von einer Instanz von MyClass aus zugegriffen wird, als nicht-None betrachtet wird.
mc = MyClass(...)
# will pass mypy --strict
name: str = mc.nameFür optionale Attribute geht Mypy davon aus, dass der Typ None enthalten muss oder anderweitig Optional ist.
mc = MyClass(...)
# will pass mypy --strict
other_name: Optional[str] = mc.nameUnabhängig davon, ob das gemappte Attribut als Optional typisiert ist, wird die Generierung der __init__()-Methode immer noch alle Schlüsselwörter als optional betrachten. Dies entspricht wiederum dem, was der SQLAlchemy ORM tatsächlich tut, wenn er den Konstruktor erstellt, und sollte nicht mit dem Verhalten eines validierenden Systems wie Python Dataclasses verwechselt werden, das einen Konstruktor generiert, der den Annotationen hinsichtlich optionaler vs. erforderlicher Attribute entspricht.
Spalten ohne explizite Typisierung¶
Spalten, die einen ForeignKey-Modifikator enthalten, müssen in einem SQLAlchemy-Deklarationsmapping keinen Datentyp angeben. Für diesen Attributtyp informiert das Mypy-Plugin den Benutzer, dass eine explizite Typisierung erforderlich ist.
# .. other imports
from sqlalchemy.sql.schema import ForeignKey
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey("user.id"))Das Plugin liefert die folgende Meldung:
$ mypy test3.py --strict
test3.py:20: error: [SQLAlchemy Mypy plugin] Can't infer type from
ORM mapped expression assigned to attribute 'user_id'; please specify a
Python type or Mapped[<python type>] on the left hand side.
Found 1 error in 1 file (checked 1 source file)Zur Behebung weisen Sie der Address.user_id-Spalte eine explizite Typannotation zu.
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))Mapping von Spalten mit imperativem Tabellenobjekt¶
Im imperativen Tabellenstil werden die Column-Definitionen innerhalb einer Table-Konstruktion angegeben, die von den gemappten Attributen selbst getrennt ist. Das Mypy-Plugin berücksichtigt diese Table nicht, sondern unterstützt, dass die Attribute explizit mit einer vollständigen Annotation angegeben werden können, die zwingend die Mapped-Klasse verwenden muss, um sie als gemappte Attribute zu identifizieren.
class MyClass(Base):
__table__ = Table(
"mytable",
Base.metadata,
Column(Integer, primary_key=True),
Column("employee_name", String(50), nullable=False),
Column(String(50)),
)
id: Mapped[int]
name: Mapped[str]
other_name: Mapped[Optional[str]]Die obigen Mapped-Annotationen werden als gemappte Spalten betrachtet und in den Standardkonstruktor aufgenommen, sowie das korrekte Typisierungsprofil für MyClass sowohl auf Klassen- als auch auf Instanzebene bereitstellen.
Mapping von Beziehungen¶
Das Plugin bietet nur begrenzte Unterstützung für die Verwendung von Typinferenz zur Erkennung von Beziehungstypen. In allen Fällen, in denen der Typ nicht erkannt werden kann, wird eine informative Fehlermeldung ausgegeben. In allen Fällen kann der entsprechende Typ explizit angegeben werden, entweder mit der Mapped-Klasse oder optional durch Weglassen für eine Inline-Deklaration. Das Plugin muss auch ermitteln, ob die Beziehung auf eine Sammlung oder einen Skalar verweist. Dazu verlässt es sich auf den expliziten Wert der Parameter relationship.uselist und/oder relationship.collection_class. Ein expliziter Typ ist erforderlich, wenn keiner dieser Parameter vorhanden ist, sowie wenn der Zieltyp der relationship() eine Zeichenkette oder ein aufrufbares Objekt und keine Klasse ist.
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user = relationship(User)Das obige Mapping führt zu folgender Fehlermeldung:
test3.py:22: error: [SQLAlchemy Mypy plugin] Can't infer scalar or
collection for ORM mapped expression assigned to attribute 'user'
if both 'uselist' and 'collection_class' arguments are absent from the
relationship(); please specify a type annotation on the left hand side.
Found 1 error in 1 file (checked 1 source file)Der Fehler kann entweder durch Verwendung von relationship(User, uselist=False) oder durch Angabe des Typs, in diesem Fall des skalaren User-Objekts, behoben werden.
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user: User = relationship(User)Für Sammlungen gilt ein ähnliches Muster, bei dem in Abwesenheit von uselist=True oder einer relationship.collection_class eine Sammlungsannotation wie List verwendet werden kann. Es ist auch vollkommen angebracht, den Zeichenkettennamen der Klasse in der Annotation zu verwenden, wie er von PEP 484 unterstützt wird, wobei sichergestellt wird, dass die Klasse im TYPE_CHECKING Block entsprechend importiert wird.
from typing import TYPE_CHECKING, List
from .mymodel import Base
if TYPE_CHECKING:
# if the target of the relationship is in another module
# that cannot normally be imported at runtime
from .myaddressmodel import Address
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
addresses: List["Address"] = relationship("Address")Wie bei Spalten kann auch die Mapped-Klasse explizit verwendet werden.
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user: Mapped[User] = relationship(User, back_populates="addresses")Verwendung von @declared_attr und Declarative Mixins¶
Die declared_attr-Klasse ermöglicht die Deklaration von deklarativ gemappten Attributen in Klassenfunktions-Ebene und ist besonders nützlich bei der Verwendung von deklarativen Mixins. Für diese Funktionen sollte der Rückgabetyp der Funktion entweder mit der Mapped[]-Konstruktion annotiert werden oder die genaue Art des von der Funktion zurückgegebenen Objekts angeben. Darüber hinaus sollten "Mixin"-Klassen, die nicht anderweitig gemappt sind (d. h. nicht von einer declarative_base()-Klasse erben und auch nicht mit einer Methode wie registry.mapped() gemappt sind), mit dem declarative_mixin()-Decorator dekoriert werden, der dem Mypy-Plugin einen Hinweis gibt, dass eine bestimmte Klasse als deklaratives Mixin dienen soll.
from sqlalchemy.orm import declarative_mixin, declared_attr
@declarative_mixin
class HasUpdatedAt:
@declared_attr
def updated_at(cls) -> Column[DateTime]: # uses Column
return Column(DateTime)
@declarative_mixin
class HasCompany:
@declared_attr
def company_id(cls) -> Mapped[int]: # uses Mapped
return mapped_column(ForeignKey("company.id"))
@declared_attr
def company(cls) -> Mapped["Company"]:
return relationship("Company")
class Employee(HasUpdatedAt, HasCompany, Base):
__tablename__ = "employee"
id = Column(Integer, primary_key=True)
name = Column(String)Beachten Sie die Diskrepanz zwischen dem tatsächlichen Rückgabetyp einer Methode wie HasCompany.company und dem, was annotiert ist. Das Mypy-Plugin konvertiert alle @declared_attr-Funktionen in einfache annotierte Attribute, um diese Komplexität zu vermeiden.
# what Mypy sees
class HasCompany:
company_id: Mapped[int]
company: Mapped["Company"]Kombination mit Dataclasses oder anderen typsensiblen Attributsystemen¶
Die Beispiele für die Integration von Python-Dataclasses unter Anwendung von ORM-Mappings auf eine bestehende Dataclass (Legacy-Dataclass-Verwendung) stellen ein Problem dar; Python-Dataclasses erwarten einen expliziten Typ, den sie zur Erstellung der Klasse verwenden, und der Wert in jeder Zuweisungsanweisung ist wichtig. Das heißt, eine Klasse wie folgt muss genau so angegeben werden, um von Dataclasses akzeptiert zu werden:
mapper_registry: registry = registry()
@mapper_registry.mapped
@dataclass
class User:
__table__ = Table(
"user",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
id: int = field(init=False)
name: Optional[str] = None
fullname: Optional[str] = None
nickname: Optional[str] = None
addresses: List[Address] = field(default_factory=list)
__mapper_args__ = { # type: ignore
"properties": {"addresses": relationship("Address")}
}Wir können unsere Mapped[]-Typen nicht auf die Attribute id, name usw. anwenden, da diese vom @dataclass-Decorator abgelehnt werden. Außerdem gibt es für Dataclasses ein weiteres Mypy-Plugin, das ebenfalls mit dem, was wir tun, in Konflikt geraten kann.
Die obige Klasse wird tatsächlich ohne Probleme die Typüberprüfung von Mypy bestehen; das Einzige, was uns fehlt, ist die Möglichkeit, Attribute von User in SQL-Ausdrücken zu verwenden, wie z. B.:
stmt = select(User.name).where(User.id.in_([1, 2, 3]))Um eine Umgehungslösung dafür zu bieten, verfügt das Mypy-Plugin über eine zusätzliche Funktion, mit der wir ein zusätzliches Attribut _mypy_mapped_attrs angeben können, das eine Liste ist, die die Klassenobjekte oder ihre Zeichenkettennamen enthält. Dieses Attribut kann innerhalb der TYPE_CHECKING-Variable bedingt sein.
@mapper_registry.mapped
@dataclass
class User:
__table__ = Table(
"user",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
id: int = field(init=False)
name: Optional[str] = None
fullname: Optional[str]
nickname: Optional[str]
addresses: List[Address] = field(default_factory=list)
if TYPE_CHECKING:
_mypy_mapped_attrs = [id, name, "fullname", "nickname", addresses]
__mapper_args__ = { # type: ignore
"properties": {"addresses": relationship("Address")}
}Mit dem obigen Rezept werden die in _mypy_mapped_attrs aufgeführten Attribute mit den Mapped-Typinformationen versehen, sodass die User-Klasse sich wie eine SQLAlchemy-gemappte Klasse verhält, wenn sie in einem klassenbasierten Kontext verwendet wird.
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