Integration mit Dataclasses und attrs

SQLAlchemy bietet ab Version 2.0 eine „native Dataclass“-Integration, bei der ein Mapping einer Annotierten Declarative Table in eine Python Dataclass umgewandelt werden kann, indem eine einzige Mixin-Klasse oder ein Dekorator zu den gemappten Klassen hinzugefügt wird.

Neu in Version 2.0: Integrierte Erstellung von Dataclasses mit ORM Declarative Klassen

Es gibt auch Muster, die es ermöglichen, existierende Dataclasses zu mappen, sowie Klassen zu mappen, die durch die Drittanbieter-Integrationsbibliothek attrs instrumentiert werden.

Declarative Dataclass Mapping

SQLAlchemy Annotated Declarative Table Mappings können mit einer zusätzlichen Mixin-Klasse oder Dekorator-Direktive erweitert werden. Diese fügt dem Declarative-Prozess einen zusätzlichen Schritt hinzu, nachdem das Mapping abgeschlossen ist. Dieser Schritt wandelt die gemappte Klasse in-place in eine Python Dataclass um, bevor der Mapping-Prozess abgeschlossen wird, der ORM-spezifische Instrumentierung auf die Klasse anwendet. Die wichtigste Verhaltensänderung, die dies bietet, ist die Generierung einer __init__() Methode mit feiner Kontrolle über positionsbezogene und Schlüsselwortargumente mit oder ohne Standardwerte, sowie die Generierung von Methoden wie __repr__() und __eq__().

Aus der Perspektive von PEP 484-Typisierung wird die Klasse als Dataclass mit spezifischen Verhaltensweisen erkannt, insbesondere durch die Nutzung von PEP 681 „Dataclass Transforms“, die es Typprüfungswerkzeugen ermöglicht, die Klasse so zu betrachten, als wäre sie explizit mit dem @dataclasses.dataclass Dekorator dekoriert worden.

Hinweis

Die Unterstützung für PEP 681 in Typprüfungswerkzeugen ist ab dem 4. April 2023 begrenzt und wird derzeit von Pyright sowie von Mypy ab Version 1.2 unterstützt. Beachten Sie, dass Mypy 1.1.1 die Unterstützung für PEP 681 eingeführt hat, aber Python-Deskriptoren nicht korrekt berücksichtigte, was zu Fehlern bei der Verwendung des SQLAlchemy ORM-Mapping-Schemas führen würde.

Siehe auch

https://peps.pythonlang.de/pep-0681/#the-dataclass-transform-decorator - Hintergrundinformationen darüber, wie Bibliotheken wie SQLAlchemy die Unterstützung für PEP 681 ermöglichen

Die Umwandlung in eine Dataclass kann zu jeder Declarative-Klasse hinzugefügt werden, entweder durch Hinzufügen der MappedAsDataclass Mixin zu einer DeclarativeBase Klassen-Hierarchie oder, bei Decorator-Mapping, durch Verwendung des Klassen-Dekorators registry.mapped_as_dataclass().

Die MappedAsDataclass Mixin kann entweder der Declarative Base Klasse oder einer beliebigen Oberklasse zugewiesen werden, wie im folgenden Beispiel gezeigt:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass


class Base(MappedAsDataclass, DeclarativeBase):
    """subclasses will be converted to dataclasses"""


class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

Oder kann direkt auf Klassen angewendet werden, die von der Declarative Base erben

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass


class Base(DeclarativeBase):
    pass


class User(MappedAsDataclass, Base):
    """User class will be converted to a dataclass"""

    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

Bei Verwendung der Decorator-Form wird nur der Dekorator registry.mapped_as_dataclass() unterstützt

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry


reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

Konfiguration von Klassenmerkmalen

Die Unterstützung von Dataclass-Merkmalen ist partiell. Derzeit sind unterstützt: die Merkmale init, repr, eq, order und unsafe_hash. match_args und kw_only werden ab Python 3.10 unterstützt. Derzeit nicht unterstützt sind die Merkmale frozen und slots.

Bei Verwendung der Mixin-Klassenform mit MappedAsDataclass werden Klassenkonfigurationsargumente als Parameter auf Klassenebene übergeben

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass


class Base(DeclarativeBase):
    pass


class User(MappedAsDataclass, Base, repr=False, unsafe_hash=True):
    """User class will be converted to a dataclass"""

    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

Bei Verwendung der Decorator-Form mit registry.mapped_as_dataclass() werden Klassenkonfigurationsargumente direkt an den Dekorator übergeben

from sqlalchemy.orm import registry
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


reg = registry()


@reg.mapped_as_dataclass(unsafe_hash=True)
class User:
    """User class will be converted to a dataclass"""

    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

Hintergrundinformationen zu Dataclass-Klassenoptionen finden Sie in der Dokumentation zu Dataclasses unter @dataclasses.dataclass.

Attributkonfiguration

SQLAlchemy native Dataclasses unterscheiden sich von normalen Dataclasses dadurch, dass Attribute, die gemappt werden sollen, in allen Fällen mit dem Mapped generischen Annotationscontainer beschrieben werden. Die Mappings folgen denselben Formen wie die unter Declarative Table mit mapped_column() dokumentierten, und alle Funktionen von mapped_column() und Mapped werden unterstützt.

Zusätzlich unterstützen ORM-Attributkonfigurationskonstrukte, einschließlich mapped_column(), relationship() und composite(), Feldoptionen pro Attribut, einschließlich init, default, default_factory und repr. Die Namen dieser Argumente sind fest wie in PEP 681 angegeben. Die Funktionalität ist äquivalent zu Dataclasses.

  • init, wie in mapped_column.init, relationship.init, gibt an, wenn das Feld nicht Teil der __init__() Methode sein soll

  • default, wie in mapped_column.default, relationship.default, gibt einen Standardwert für das Feld an, der als Schlüsselwortargument in der __init__() Methode übergeben wird.

  • default_factory, wie in mapped_column.default_factory, relationship.default_factory, gibt eine aufrufbare Funktion an, die aufgerufen wird, um einen neuen Standardwert für einen Parameter zu generieren, wenn dieser nicht explizit an die __init__() Methode übergeben wird.

  • repr Standardmäßig True, gibt an, dass das Feld Teil der generierten __repr__() Methode sein soll

Ein weiterer wichtiger Unterschied zu Dataclasses ist, dass Standardwerte für Attribute immer über den Parameter default des ORM-Konstrukts konfiguriert werden müssen, z.B. mapped_column(default=None). Eine Syntax, die an die Dataclass-Syntax erinnert und einfache Python-Werte als Standardwerte ohne Verwendung von @dataclases.field() akzeptiert, wird nicht unterstützt.

Als Beispiel mit mapped_column() erzeugt das folgende Mapping eine __init__() Methode, die nur die Felder name und fullname akzeptiert, wobei name erforderlich ist und positionell übergeben werden kann, und fullname optional ist. Das Feld id, von dem wir erwarten, dass es von der Datenbank generiert wird, ist überhaupt nicht Teil des Konstruktors

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]
    fullname: Mapped[str] = mapped_column(default=None)


# 'fullname' is optional keyword argument
u1 = User("name")

Spaltendefaultwerte

Um die Namensüberschneidung des Arguments default mit dem bestehenden Parameter Column.default des Column Konstrukts zu berücksichtigen, disambiguiert der mapped_column() Konstrukt die beiden Namen durch Hinzufügen eines neuen Parameters mapped_column.insert_default. Dieser wird direkt in den Parameter Column.default von Column geschrieben, unabhängig davon, was auf mapped_column.default gesetzt ist, welches immer für die Dataclass-Konfiguration verwendet wird. Um beispielsweise eine Datums-/Zeitspalte mit einem Column.default, gesetzt auf die SQL-Funktion func.utc_timestamp(), zu konfigurieren, wobei der Parameter im Konstruktor optional ist

from datetime import datetime

from sqlalchemy import func
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    created_at: Mapped[datetime] = mapped_column(
        insert_default=func.utc_timestamp(), default=None
    )

Mit dem obigen Mapping erfolgt ein INSERT für ein neues User Objekt, bei dem kein Parameter für created_at übergeben wurde, wie folgt:

>>> with Session(e) as session:
...     session.add(User())
...     session.commit()
BEGIN (implicit) INSERT INTO user_account (created_at) VALUES (utc_timestamp()) [generated in 0.00010s] () COMMIT

Integration mit Annotated

Der unter Mapping von gesamten Spaltendefinitionen auf Python-Typen eingeführte Ansatz zeigt, wie PEP 593 Annotated Objekte verwendet werden können, um ganze mapped_column() Konstrukte zur Wiederverwendung zu verpacken. Während Annotated Objekte mit der Verwendung von Dataclasses kombiniert werden können, können Dataclass-spezifische Schlüsselwortargumente leider nicht innerhalb des Annotated-Konstrukts verwendet werden. Dies schließt PEP 681-spezifische Argumente wie init, default, repr und default_factory ein, die immer in einem mapped_column() oder einem ähnlichen Konstrukt inline mit dem Klassenattribut vorhanden sein müssen.

Geändert in Version 2.0.14/2.0.22: Das Annotated Konstrukt kann, wenn es mit einem ORM-Konstrukt wie mapped_column() verwendet wird, keine Dataclass-Feldparameter wie init und repr aufnehmen. Diese Verwendung widerspricht dem Design von Python-Dataclasses und wird von PEP 681 nicht unterstützt, weshalb sie auch zur Laufzeit vom SQLAlchemy ORM abgelehnt wird. Eine Deprecation-Warnung wird nun ausgegeben und das Attribut wird ignoriert.

Als Beispiel wird der Parameter init=False unten ignoriert und zusätzlich eine Deprecation-Warnung ausgegeben

from typing import Annotated

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

# typing tools as well as SQLAlchemy will ignore init=False here
intpk = Annotated[int, mapped_column(init=False, primary_key=True)]

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"
    id: Mapped[intpk]


# typing error as well as runtime error: Argument missing for parameter "id"
u1 = User()

Stattdessen muss mapped_column() ebenfalls auf der rechten Seite vorhanden sein, mit einer expliziten Einstellung für mapped_column.init; die anderen Argumente können im Annotated Konstrukt verbleiben

from typing import Annotated

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

intpk = Annotated[int, mapped_column(primary_key=True)]

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    # init=False and other pep-681 arguments must be inline
    id: Mapped[intpk] = mapped_column(init=False)


u1 = User()

Verwendung von Mixins und abstrakten Oberklassen

Alle Mixins oder Basisklassen, die in einer MappedAsDataclass gemappten Klasse verwendet werden und Mapped Attribute enthalten, müssen selbst Teil einer MappedAsDataclass Hierarchie sein, wie im folgenden Beispiel mit einer Mixin:

class Mixin(MappedAsDataclass):
    create_user: Mapped[int] = mapped_column()
    update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)


class Base(DeclarativeBase, MappedAsDataclass):
    pass


class User(Base, Mixin):
    __tablename__ = "sys_user"

    uid: Mapped[str] = mapped_column(
        String(50), init=False, default_factory=uuid4, primary_key=True
    )
    username: Mapped[str] = mapped_column()
    email: Mapped[str] = mapped_column()

Python-Typprüfer, die PEP 681 unterstützen, betrachten Attribute aus nicht-Dataclass-Mixins andernfalls nicht als Teil der Dataclass.

Veraltet seit Version 2.0.8: Die Verwendung von Mixins und abstrakten Basen innerhalb von MappedAsDataclass oder registry.mapped_as_dataclass() Hierarchien, die selbst keine Dataclasses sind, ist veraltet, da diese Felder von PEP 681 nicht als zur Dataclass gehörend betrachtet werden. Eine Warnung wird für diesen Fall ausgegeben, die später zu einem Fehler wird.

Beziehungskonfiguration

Die Mapped Annotation in Kombination mit relationship() wird auf die gleiche Weise verwendet, wie unter Grundlegende Beziehungsmuster beschrieben. Bei der Angabe einer sammlungsbasierten relationship() als optionales Schlüsselwortargument muss der Parameter relationship.default_factory übergeben werden und er muss auf die zu verwendende Sammlungsklasse verweisen. Viele-zu-eins- und skalare Objektverweise können relationship.default verwenden, wenn der Standardwert None sein soll.

from typing import List

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

reg = registry()


@reg.mapped_as_dataclass
class Parent:
    __tablename__ = "parent"
    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(
        default_factory=list, back_populates="parent"
    )


@reg.mapped_as_dataclass
class Child:
    __tablename__ = "child"
    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
    parent: Mapped["Parent"] = relationship(default=None)

Das obige Mapping generiert eine leere Liste für Parent.children, wenn ein neues Parent() Objekt konstruiert wird, ohne children zu übergeben, und ähnlich einen None Wert für Child.parent, wenn ein neues Child() Objekt konstruiert wird, ohne parent zu übergeben.

Während die relationship.default_factory automatisch aus der angegebenen Sammlungsklasse der relationship() abgeleitet werden kann, würde dies die Kompatibilität mit Dataclasses brechen, da die Anwesenheit von relationship.default_factory oder relationship.default bestimmt, ob der Parameter beim Rendern in die __init__() Methode erforderlich oder optional ist.

Verwendung von Nicht-Mapped Dataclass-Feldern

Bei Verwendung von Declarative Dataclasses können auch nicht gemappte Felder auf der Klasse verwendet werden, die Teil des Dataclass-Konstruktionsprozesses sind, aber nicht gemappt werden. Jedes Feld, das nicht Mapped verwendet, wird vom Mapping-Prozess ignoriert. Im folgenden Beispiel sind die Felder ctrl_one und ctrl_two Teil des Instanzzustands des Objekts, werden aber vom ORM nicht persistiert.

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class Data:
    __tablename__ = "data"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    status: Mapped[str]

    ctrl_one: Optional[str] = None
    ctrl_two: Optional[str] = None

Instanzen von Data oben können wie folgt erstellt werden:

d1 = Data(status="s1", ctrl_one="ctrl1", ctrl_two="ctrl2")

Ein realistischeres Beispiel könnte die Verwendung der Dataclass InitVar Funktion in Verbindung mit der __post_init__() Funktion sein, um nur für die Initialisierung vorgesehene Felder zu empfangen, die zum Aufbau persistierter Daten verwendet werden können. Im folgenden Beispiel wird die Klasse User mit id, name und password_hash als gemappte Merkmale deklariert, verwendet aber nur für die Initialisierung vorgesehene Felder password und repeat_password, um den Benutzererstellungsprozess darzustellen (Hinweis: Um dieses Beispiel auszuführen, ersetzen Sie die Funktion your_crypt_function_here() durch eine Drittanbieter-Kryptofunktion, wie z.B. bcrypt oder argon2-cffi).

from dataclasses import InitVar
from typing import Optional

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

    password: InitVar[str]
    repeat_password: InitVar[str]

    password_hash: Mapped[str] = mapped_column(init=False, nullable=False)

    def __post_init__(self, password: str, repeat_password: str):
        if password != repeat_password:
            raise ValueError("passwords do not match")

        self.password_hash = your_crypt_function_here(password)

Das obige Objekt wird mit den Parametern password und repeat_password erstellt, die im Voraus verarbeitet werden, damit die Variable password_hash generiert werden kann.

>>> u1 = User(name="some_user", password="xyz", repeat_password="xyz")
>>> u1.password_hash
'$6$9ppc... (example crypted string....)'

Geändert in Version 2.0.0rc1: Bei Verwendung von registry.mapped_as_dataclass() oder MappedAsDataclass können Felder, die nicht die Mapped Annotation enthalten, einbezogen werden. Diese werden als Teil der resultierenden Dataclass behandelt, aber nicht gemappt, ohne dass das Klassenattribut __allow_unmapped__ explizit angegeben werden muss. Frühere 2.0 Beta-Versionen erforderten die explizite Anwesenheit dieses Attributs, obwohl der Zweck dieses Attributs nur darin bestand, die fortgesetzte Funktionalität von Legacy ORM-Typzuordnungen zu ermöglichen.

Integration mit alternativen Dataclass-Anbietern wie Pydantic

Warnung

Die Dataclass-Schicht von Pydantic ist nicht vollständig kompatibel mit der Klasseninstrumentierung von SQLAlchemy ohne zusätzliche interne Änderungen, und viele Funktionen wie zugehörige Sammlungen funktionieren möglicherweise nicht korrekt.

Für Pydantic-Kompatibilität sollten Sie das SQLModel ORM in Betracht ziehen, das mit Pydantic auf SQLAlchemy ORM aufbaut und spezielle Implementierungsdetails enthält, die explizit diese Inkompatibilitäten lösen.

SQLAlchemy's MappedAsDataclass Klasse und die Methode registry.mapped_as_dataclass() greifen direkt auf den Dekorator der Python-Standardbibliothek dataclasses.dataclass zu, nachdem der deklarative Mapping-Prozess auf die Klasse angewendet wurde. Dieser Funktionsaufruf kann durch alternative Dataclass-Anbieter, wie den von Pydantic, ersetzt werden, indem der Parameter dataclass_callable verwendet wird, der von MappedAsDataclass als Klassen-Schlüsselwortargument sowie von registry.mapped_as_dataclass() akzeptiert wird.

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
from sqlalchemy.orm import registry


class Base(
    MappedAsDataclass,
    DeclarativeBase,
    dataclass_callable=pydantic.dataclasses.dataclass,
):
    pass


class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]

Die obige User Klasse wird als Dataclass angewendet, wobei der Callable von Pydantic pydantic.dataclasses.dataclasses verwendet wird. Dieser Prozess ist sowohl für gemappte Klassen als auch für Mixins verfügbar, die von MappedAsDataclass erben oder auf die registry.mapped_as_dataclass() direkt angewendet wird.

Neu in Version 2.0.4: Hinzugefügt wurden die Klassen- und Methodenparameter dataclass_callable für MappedAsDataclass und registry.mapped_as_dataclass(), und einige interne Dataclass-Funktionen wurden angepasst, um strengere Dataclass-Funktionen wie die von Pydantic zu unterstützen.

Anwenden von ORM-Mappings auf eine existierende Dataclass (Legacy-Dataclass-Nutzung)

Legacy-Funktion

Die hier beschriebenen Ansätze werden durch die Funktion Declarative Dataclass Mapping, neu in der 2.0-Serie von SQLAlchemy, abgelöst. Diese neuere Version der Funktion baut auf der Dataclass-Unterstützung auf, die erstmals in Version 1.4 hinzugefügt wurde und in diesem Abschnitt beschrieben wird.

Um eine existierende Dataclass zu mappen, können die „Inline“-Deklarationsdirektiven von SQLAlchemy nicht direkt verwendet werden; ORM-Direktiven werden mit einer der drei Techniken zugewiesen:

Der allgemeine Prozess, mit dem SQLAlchemy Mappings auf eine Dataclass anwendet, ist derselbe wie bei einer normalen Klasse. Zusätzlich erkennt SQLAlchemy klassenweite Attribute, die Teil des Deklarationsprozesses von Dataclasses waren, und ersetzt sie zur Laufzeit durch die üblichen SQLAlchemy ORM-gemappten Attribute. Die von Dataclasses generierte `__init__`-Methode bleibt erhalten, ebenso wie alle anderen von Dataclasses generierten Methoden wie `__eq__()`, `__repr__()` usw.

Abbildung von vorab existierenden Dataclasses unter Verwendung von Declarative With Imperative Table

Ein Beispiel für ein Mapping unter Verwendung von `@dataclass` mit Declarative with Imperative Table (auch bekannt als Hybrid Declarative) ist unten aufgeführt. Ein vollständiges `Table`-Objekt wird explizit konstruiert und dem `__table__`-Attribut zugewiesen. Instanzfelder werden mit der normalen Dataclass-Syntax definiert. Zusätzliche `MapperProperty`-Definitionen wie `relationship()` werden im klassenweiten Dictionary `__mapper_args__` unter dem Schlüssel `properties` platziert und entsprechen dem Parameter `Mapper.properties`.

from __future__ import annotations

from dataclasses import dataclass, field
from typing import List, Optional

from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import registry, relationship

mapper_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"),
        }
    }


@mapper_registry.mapped
@dataclass
class Address:
    __table__ = Table(
        "address",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("user_id", Integer, ForeignKey("user.id")),
        Column("email_address", String(50)),
    )
    id: int = field(init=False)
    user_id: int = field(init=False)
    email_address: Optional[str] = None

Im obigen Beispiel sind die Attribute `User.id`, `Address.id` und `Address.user_id` als `field(init=False)` definiert. Das bedeutet, dass Parameter dafür nicht zu den `__init__()`-Methoden hinzugefügt werden, aber `Session` sie dennoch setzen kann, nachdem sie ihre Werte während des Flushes von Autoincrement oder anderen Standardwertgeneratoren erhalten haben. Um sie explizit im Konstruktor angeben zu können, würden sie stattdessen einen Standardwert von `None` erhalten.

Damit ein `relationship()` separat deklariert werden kann, muss es direkt innerhalb des `properties`-Dictionaries von `Mapper` angegeben werden, das selbst innerhalb des `__mapper_args__`-Dictionaries angegeben wird, damit es an den Konstruktor für `Mapper` übergeben wird. Eine Alternative zu diesem Ansatz ist im nächsten Beispiel dargestellt.

Warnung

Das Deklarieren eines Dataclass `field()` mit einem `default` zusammen mit `init=False` funktioniert nicht wie erwartet mit einer völlig reinen Dataclass, da die SQLAlchemy-Klasseninstrumentierung den Standardwert, der von der Dataclass-Erstellung auf der Klasse gesetzt wurde, überschreibt. Verwenden Sie stattdessen `default_factory`. Diese Anpassung erfolgt automatisch, wenn Sie Declarative Dataclass Mapping verwenden.

Abbildung von vorab existierenden Dataclasses unter Verwendung von Deklarativen Feldern

Legacy-Funktion

Dieser Ansatz für Deklaratives Mapping mit Dataclasses sollte als veraltet betrachtet werden. Er wird weiterhin unterstützt, bietet aber wahrscheinlich keine Vorteile gegenüber dem neuen Ansatz, der unter Declarative Dataclass Mapping beschrieben wird.

Beachten Sie, dass **mapped_column() bei dieser Verwendung nicht unterstützt wird**; das `Column`-Konstrukt sollte weiterhin zur Deklaration von Tabellenmetadaten im `metadata`-Feld von `dataclasses.field()` verwendet werden.

Der vollständig deklarative Ansatz erfordert, dass `Column`-Objekte als klassenweite Attribute deklariert werden, was bei Verwendung von Dataclasses mit klassenweiten Attributen kollidieren würde. Ein Ansatz, diese zu kombinieren, ist die Verwendung des `metadata`-Attributs auf dem `dataclass.field`-Objekt, wo SQLAlchemy-spezifische Mapping-Informationen bereitgestellt werden können. Deklarativ unterstützt die Extraktion dieser Parameter, wenn die Klasse das Attribut `__sa_dataclass_metadata_key__` angibt. Dies bietet auch eine prägnantere Methode zur Angabe der `relationship()`-Assoziation.

from __future__ import annotations

from dataclasses import dataclass, field
from typing import List

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import registry, relationship

mapper_registry = registry()


@mapper_registry.mapped
@dataclass
class User:
    __tablename__ = "user"

    __sa_dataclass_metadata_key__ = "sa"
    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
    name: str = field(default=None, metadata={"sa": Column(String(50))})
    fullname: str = field(default=None, metadata={"sa": Column(String(50))})
    nickname: str = field(default=None, metadata={"sa": Column(String(12))})
    addresses: List[Address] = field(
        default_factory=list, metadata={"sa": relationship("Address")}
    )


@mapper_registry.mapped
@dataclass
class Address:
    __tablename__ = "address"
    __sa_dataclass_metadata_key__ = "sa"
    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
    user_id: int = field(init=False, metadata={"sa": Column(ForeignKey("user.id"))})
    email_address: str = field(default=None, metadata={"sa": Column(String(50))})

Verwendung von Deklarativen Mixins mit vorab existierenden Dataclasses

Im Abschnitt Composing Mapped Hierarchies with Mixins werden Deklarative Mixin-Klassen eingeführt. Eine Anforderung für deklarative Mixins ist, dass bestimmte Konstrukte, die nicht einfach dupliziert werden können, als aufrufbare Objekte unter Verwendung des `declared_attr`-Dekorators bereitgestellt werden müssen, wie im Beispiel unter Mixing in Relationships.

class RefTargetMixin:
    @declared_attr
    def target_id(cls) -> Mapped[int]:
        return mapped_column("target_id", ForeignKey("target.id"))

    @declared_attr
    def target(cls):
        return relationship("Target")

Diese Form wird innerhalb des Dataclasses `field()`-Objekts unterstützt, indem eine Lambda-Funktion verwendet wird, um den SQLAlchemy-Konstrukt innerhalb des `field()` anzugeben. Die Verwendung von `declared_attr()` zur Umschließung des Lambda ist optional. Wenn wir unsere obige `User`-Klasse erstellen wollten, bei der die ORM-Felder aus einem Mixin stammen, der selbst eine Dataclass ist, wäre die Form:

@dataclass
class UserMixin:
    __tablename__ = "user"

    __sa_dataclass_metadata_key__ = "sa"

    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})

    addresses: List[Address] = field(
        default_factory=list, metadata={"sa": lambda: relationship("Address")}
    )


@dataclass
class AddressMixin:
    __tablename__ = "address"
    __sa_dataclass_metadata_key__ = "sa"
    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
    user_id: int = field(
        init=False, metadata={"sa": lambda: Column(ForeignKey("user.id"))}
    )
    email_address: str = field(default=None, metadata={"sa": Column(String(50))})


@mapper_registry.mapped
class User(UserMixin):
    pass


@mapper_registry.mapped
class Address(AddressMixin):
    pass

Neu in Version 1.4.2: Unterstützung für "declared attr"-ähnliche Mixin-Attribute, nämlich `relationship()`-Konstrukte sowie `Column`-Objekte mit Fremdschlüsseldeklarationen, wurde hinzugefügt, um sie innerhalb von "Dataclasses with Declarative Table"-Style-Mappings zu verwenden.

Abbildung von vorab existierenden Dataclasses unter Verwendung von Imperative Mapping

Wie bereits erwähnt, kann eine Klasse, die mit dem `@dataclass`-Dekorator als Dataclass eingerichtet ist, weiter mit dem `registry.mapped()`-Dekorator dekoriert werden, um deklarativen Style-Mapping auf die Klasse anzuwenden. Als Alternative zur Verwendung des `registry.mapped()`-Dekorators können wir die Klasse auch stattdessen durch die Methode `registry.map_imperatively()` leiten, sodass wir alle `Table`- und `Mapper`-Konfigurationen imperativ an die Funktion übergeben können, anstatt sie als Klassenvariablen auf der Klasse selbst zu definieren.

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import field
from typing import List

from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

mapper_registry = registry()


@dataclass
class User:
    id: int = field(init=False)
    name: str = None
    fullname: str = None
    nickname: str = None
    addresses: List[Address] = field(default_factory=list)


@dataclass
class Address:
    id: int = field(init=False)
    user_id: int = field(init=False)
    email_address: str = None


metadata_obj = MetaData()

user = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)

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

mapper_registry.map_imperatively(
    User,
    user,
    properties={
        "addresses": relationship(Address, backref="user", order_by=address.c.id),
    },
)

mapper_registry.map_imperatively(Address, address)

Die gleiche Warnung, die unter Mapping pre-existing dataclasses using Declarative With Imperative Table erwähnt wurde, gilt auch bei der Verwendung dieses Mapping-Stils.

ORM-Mappings auf eine vorhandene attrs-Klasse anwenden

Die Bibliothek `attrs` ist eine beliebte Drittanbieterbibliothek, die ähnliche Funktionen wie Dataclasses bietet, mit vielen zusätzlichen Funktionen, die bei normalen Dataclasses nicht zu finden sind.

Eine mit `attrs` erweiterte Klasse verwendet den `@define`-Dekorator. Dieser Dekorator initiiert einen Prozess zum Scannen der Klasse nach Attributen, die das Verhalten der Klasse definieren, welche dann zur Generierung von Methoden, Dokumentation und Annotationen verwendet werden.

Der SQLAlchemy ORM unterstützt das Mapping einer `attrs`-Klasse unter Verwendung von **Declarative with Imperative Table** oder **Imperative** Mapping. Die allgemeine Form dieser beiden Stile ist vollständig äquivalent zu den unter Mapping pre-existing dataclasses using Declarative-style fields und Mapping pre-existing dataclasses using Declarative With Imperative Table verwendeten Mapping-Formen mit Dataclasses, wobei die von Dataclasses oder attrs verwendeten Inline-Attributdirektiven unverändert bleiben und die tabellenorientierte Instrumentierung von SQLAlchemy zur Laufzeit angewendet wird.

Der `@define`-Dekorator von `attrs` ersetzt standardmäßig die annotierte Klasse durch eine neue `__slots__`-basierte Klasse, die nicht unterstützt wird. Bei Verwendung der alten Annotationsform `@attr.s` oder bei Verwendung von `define(slots=False)` wird die Klasse nicht ersetzt. Darüber hinaus entfernt attrs seine eigenen klassengebundenen Attribute nach Ausführung des Dekorators, sodass der Mapping-Prozess von SQLAlchemy diese Attribute ohne Probleme übernimmt. Sowohl die Dekoratoren `@attr.s` als auch `@define(slots=False)` funktionieren mit SQLAlchemy.

attrs mit Deklarativem "Imperative Table" abbilden

Im Stil "Declarative with Imperative Table" wird ein `Table`-Objekt inline mit der deklarativen Klasse deklariert. Der `@define`-Dekorator wird zuerst auf die Klasse angewendet, dann der `registry.mapped()`-Dekorator.

from __future__ import annotations

from typing import List
from typing import Optional

from attrs import define
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

mapper_registry = registry()


@mapper_registry.mapped
@define(slots=False)
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("FullName", String(50), key="fullname"),
        Column("nickname", String(12)),
    )
    id: Mapped[int]
    name: Mapped[str]
    fullname: Mapped[str]
    nickname: Mapped[str]
    addresses: Mapped[List[Address]]

    __mapper_args__ = {  # type: ignore
        "properties": {
            "addresses": relationship("Address"),
        }
    }


@mapper_registry.mapped
@define(slots=False)
class Address:
    __table__ = Table(
        "address",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("user_id", Integer, ForeignKey("user.id")),
        Column("email_address", String(50)),
    )
    id: Mapped[int]
    user_id: Mapped[int]
    email_address: Mapped[Optional[str]]

Hinweis

Die `attrs`-Option `slots=True`, die `__slots__` auf einer abgebildeten Klasse aktiviert, kann nicht mit SQLAlchemy-Mappings verwendet werden, ohne alternative Attributinstrumentierung vollständig zu implementieren, da abgebildete Klassen normalerweise auf den direkten Zugriff auf `__dict__` für die Speicherung des Zustands angewiesen sind. Das Verhalten ist undefiniert, wenn diese Option vorhanden ist.

attrs mit Imperative Mapping abbilden

Genau wie bei Dataclasses können wir auch `registry.map_imperatively()` verwenden, um eine bestehende `attrs`-Klasse abzubilden.

from __future__ import annotations

from typing import List

from attrs import define
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

mapper_registry = registry()


@define(slots=False)
class User:
    id: int
    name: str
    fullname: str
    nickname: str
    addresses: List[Address]


@define(slots=False)
class Address:
    id: int
    user_id: int
    email_address: Optional[str]


metadata_obj = MetaData()

user = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)

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

mapper_registry.map_imperatively(
    User,
    user,
    properties={
        "addresses": relationship(Address, backref="user", order_by=address.c.id),
    },
)

mapper_registry.map_imperatively(Address, address)

Die obige Form ist äquivalent zum vorherigen Beispiel mit Declarative with Imperative Table.