Mapper-Konfiguration mit Deklarativ

Der Abschnitt Wesentliche Komponenten der gemappten Klasse beschreibt die allgemeinen Konfigurationselemente eines Mapper-Konstrukts, welches die Struktur darstellt, die definiert, wie eine bestimmte vom Benutzer definierte Klasse auf eine Datenbanktabelle oder eine andere SQL-Konstruktion abgebildet wird. Die folgenden Abschnitte beschreiben spezifische Details darüber, wie das deklarative System den Mapper konstruiert.

Definieren von gemappten Eigenschaften mit Deklarativ

Die Beispiele unter Tabellenkonfiguration mit Deklarativ veranschaulichen Abbildungen auf tabellengebundene Spalten, unter Verwendung des mapped_column()-Konstrukts. Es gibt verschiedene andere Arten von ORM-gemappten Konstrukten, die konfiguriert werden können, neben tabellengebundenen Spalten, die häufigste ist das relationship()-Konstrukt. Andere Arten von Eigenschaften umfassen SQL-Ausdrücke, die mit dem column_property()-Konstrukt definiert werden, und Mehrfachspaltenabbildungen unter Verwendung des composite()-Konstrukts.

Während eine imperative Abbildung das `properties`-Wörterbuch verwendet, um alle gemappten Klassenattribute zu etablieren, werden bei der deklarativen Abbildung diese Eigenschaften alle inline mit der Klassendefinition angegeben, was im Falle einer deklarativen Tabellenabbildung inline mit den Column-Objekten geschieht, die zur Erzeugung eines Table-Objekts verwendet werden.

Unter Verwendung der Beispielabbildung von User und Address können wir eine deklarative Tabellenabbildung veranschaulichen, die nicht nur mapped_column()-Objekte, sondern auch Beziehungen und SQL-Ausdrücke enthält.

from typing import List
from typing import Optional

from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import String
from sqlalchemy import Text
from sqlalchemy.orm import column_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    firstname: Mapped[str] = mapped_column(String(50))
    lastname: Mapped[str] = mapped_column(String(50))
    fullname: Mapped[str] = column_property(firstname + " " + lastname)

    addresses: Mapped[List["Address"]] = relationship(back_populates="user")


class Address(Base):
    __tablename__ = "address"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
    email_address: Mapped[str]
    address_statistics: Mapped[Optional[str]] = mapped_column(Text, deferred=True)

    user: Mapped["User"] = relationship(back_populates="addresses")

Die obige deklarative Tabellenabbildung weist zwei Tabellen auf, jede mit einer relationship(), die auf die andere verweist, sowie einen einfachen SQL-Ausdruck, der von column_property() abgebildet wird, und eine zusätzliche mapped_column(), die angibt, dass das Laden auf "verzögerte" Weise erfolgen soll, wie durch das Schlüsselwort mapped_column.deferred definiert. Weitere Dokumentation zu diesen spezifischen Konzepten finden Sie unter Grundlegende Beziehungsmuster, Verwendung von column_property und Begrenzung, welche Spalten mit verzögertem Spaltenabruf geladen werden.

Eigenschaften können auch mit einer deklarativen Abbildung wie oben im „Hybrid-Tabellen“-Stil angegeben werden; die Column-Objekte, die direkt Teil einer Tabelle sind, werden in die Table-Definition verschoben, aber alles andere, einschließlich zusammengesetzter SQL-Ausdrücke, bleibt inline mit der Klassendefinition. Konstrukte, die sich direkt auf eine Column beziehen müssen, würden sich auf sie in Bezug auf das Table-Objekt beziehen. Um die obige Abbildung im Hybrid-Tabellenstil zu veranschaulichen

# mapping attributes using declarative with imperative table
# i.e. __table__

from sqlalchemy import Column, ForeignKey, Integer, String, Table, Text
from sqlalchemy.orm import column_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import deferred
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


class User(Base):
    __table__ = Table(
        "user",
        Base.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String),
        Column("firstname", String(50)),
        Column("lastname", String(50)),
    )

    fullname = column_property(__table__.c.firstname + " " + __table__.c.lastname)

    addresses = relationship("Address", back_populates="user")


class Address(Base):
    __table__ = Table(
        "address",
        Base.metadata,
        Column("id", Integer, primary_key=True),
        Column("user_id", ForeignKey("user.id")),
        Column("email_address", String),
        Column("address_statistics", Text),
    )

    address_statistics = deferred(__table__.c.address_statistics)

    user = relationship("User", back_populates="addresses")

Zu beachten ist Folgendes:

  • Die Table `address` enthält eine Spalte namens address_statistics. Wir bilden diese Spalte jedoch unter demselben Attributnamen neu ab, um sie unter der Kontrolle eines deferred()-Konstrukts zu stellen.

  • Bei deklarativen Tabellen- und Hybrid-Tabellenabbildungen, wenn wir ein ForeignKey-Konstrukt definieren, benennen wir die Zieltabelle immer mit dem **Tabellennamen** und nicht mit dem Namen der gemappten Klasse.

  • Wenn wir relationship()-Konstrukte definieren, da diese Konstrukte eine Verknüpfung zwischen zwei gemappten Klassen erstellen, bei der eine notwendigerweise vor der anderen definiert wird, können wir die entfernte Klasse anhand ihres String-Namens referenzieren. Diese Funktionalität erstreckt sich auch auf andere Argumente, die für die relationship() angegeben werden, wie z. B. die Argumente „primary join“ und „order by“. Details dazu finden Sie im Abschnitt Späte Auswertung von Beziehungsargumenten.

Mapper-Konfigurationsoptionen mit Deklarativ

Bei allen Abbildungsformen wird die Abbildung der Klasse über Parameter konfiguriert, die Teil des Mapper-Objekts werden. Die Funktion, die diese Argumente letztendlich empfängt, ist die Mapper-Funktion, und sie werden ihr von einer der externen Abbildungsfunktionen auf dem registry-Objekt übergeben.

Für die deklarative Form der Abbildung werden Mapper-Argumente über die deklarative Klassenvariable __mapper_args__ angegeben, ein Wörterbuch, das als Schlüsselwortargumente an die Mapper-Funktion übergeben wird. Einige Beispiele

Spezifische Primärschlüsselspalten zuordnen

Das folgende Beispiel veranschaulicht deklarative Einstellungen für den Parameter Mapper.primary_key, der bestimmte Spalten als Teil dessen festlegt, was der ORM als Primärschlüssel für die Klasse betrachten soll, unabhängig von Schema-weiten Primärschlüsselbeschränkungen.

class GroupUsers(Base):
    __tablename__ = "group_users"

    user_id = mapped_column(String(40))
    group_id = mapped_column(String(40))

    __mapper_args__ = {"primary_key": [user_id, group_id]}

Siehe auch

Zuordnung zu einem expliziten Satz von Primärschlüsselspalten - weitere Hintergründe zur ORM-Zuordnung von expliziten Spalten als Primärschlüsselspalten.

Versions-ID-Spalte

Das folgende Beispiel veranschaulicht deklarative Einstellungen für die Parameter Mapper.version_id_col und Mapper.version_id_generator, die einen vom ORM verwalteten Versionszähler konfigurieren, der während des Flush-Prozesses der Unit of Work aktualisiert und überprüft wird.

from datetime import datetime


class Widget(Base):
    __tablename__ = "widgets"

    id = mapped_column(Integer, primary_key=True)
    timestamp = mapped_column(DateTime, nullable=False)

    __mapper_args__ = {
        "version_id_col": timestamp,
        "version_id_generator": lambda v: datetime.now(),
    }

Siehe auch

Konfiguration eines Versionszählers - Hintergründe zur ORM-Versionszählerfunktion.

Single Table Inheritance

Das folgende Beispiel veranschaulicht deklarative Einstellungen für die Parameter Mapper.polymorphic_on und Mapper.polymorphic_identity, die bei der Konfiguration einer Single-Table-Inheritance-Abbildung verwendet werden.

class Person(Base):
    __tablename__ = "person"

    person_id = mapped_column(Integer, primary_key=True)
    type = mapped_column(String, nullable=False)

    __mapper_args__ = dict(
        polymorphic_on=type,
        polymorphic_identity="person",
    )


class Employee(Person):
    __mapper_args__ = dict(
        polymorphic_identity="employee",
    )

Siehe auch

Single Table Inheritance - Hintergründe zur ORM-Single-Table-Inheritance-Abbildungsfunktion.

Dynamisches Erstellen von Mapper-Argumenten

Das `__mapper_args__`-Wörterbuch kann statt eines festen Wörterbuchs aus einer klassenbezogenen Deskriptormethode generiert werden, indem das declared_attr()-Konstrukt verwendet wird. Dies ist nützlich, um Argumente für Mapper zu erstellen, die programmatisch aus der Tabellenkonfiguration oder anderen Aspekten der gemappten Klasse abgeleitet werden. Ein dynamisches `__mapper_args__`-Attribut ist typischerweise nützlich bei der Verwendung eines deklarativen Mixins oder einer abstrakten Basisklasse.

Um beispielsweise Spalten, die einen speziellen Column.info-Wert haben, von der Abbildung auszuschließen, kann ein Mixin eine `__mapper_args__`-Methode verwenden, die diese Spalten aus dem `cls.__table__`-Attribut scannt und sie an die Mapper.exclude_properties-Sammlung übergibt.

from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr


class ExcludeColsWFlag:
    @declared_attr
    def __mapper_args__(cls):
        return {
            "exclude_properties": [
                column.key
                for column in cls.__table__.c
                if column.info.get("exclude", False)
            ]
        }


class Base(DeclarativeBase):
    pass


class SomeClass(ExcludeColsWFlag, Base):
    __tablename__ = "some_table"

    id = mapped_column(Integer, primary_key=True)
    data = mapped_column(String)
    not_needed = mapped_column(String, info={"exclude": True})

Oben bietet der Mixin `ExcludeColsWFlag` einen `__mapper_args__`-Hook auf Klassenebene, der nach Column-Objekten sucht, die den Schlüssel/Wert `'exclude': True` enthalten, der an den Column.info-Parameter übergeben wird, und fügt dann ihren String-„Schlüssel“-Namen zur Mapper.exclude_properties-Sammlung hinzu, was verhindert, dass der resultierende Mapper diese Spalten für SQL-Operationen berücksichtigt.

Andere deklarative Abbildungsdirektiven

__declare_last__()

Der `__declare_last__()`-Hook ermöglicht die Definition einer Funktion auf Klassenebene, die automatisch vom `MapperEvents.after_configured()`-Ereignis aufgerufen wird, welches eintritt, nachdem angenommen wird, dass die Abbildungen abgeschlossen sind und der „configure“-Schritt beendet wurde.

class MyClass(Base):
    @classmethod
    def __declare_last__(cls):
        """ """
        # do something with mappings

__declare_first__()

Wie `__declare_last__()`, wird aber zu Beginn der Mapper-Konfiguration über das `MapperEvents.before_configured()`-Ereignis aufgerufen.

class MyClass(Base):
    @classmethod
    def __declare_first__(cls):
        """ """
        # do something before mappings are configured

metadata

Die `MetaData`-Sammlung, die normalerweise zum Zuweisen einer neuen Table verwendet wird, ist das `registry.metadata`-Attribut, das dem verwendeten `registry`-Objekt zugeordnet ist. Bei Verwendung einer deklarativen Basisklasse wie der, die von der `DeclarativeBase`-Oberklasse sowie älteren Funktionen wie `declarative_base()` und `registry.generate_base()` erzeugt wird, ist diese `MetaData` normalerweise auch als Attribut mit dem Namen `.metadata` direkt auf der Basisklasse und somit durch Vererbung auch auf der gemappten Klasse vorhanden. Deklarativ verwendet dieses Attribut, falls vorhanden, um die Ziel-`MetaData`-Sammlung zu bestimmen, oder falls nicht vorhanden, verwendet es die `MetaData`, die direkt dem `registry` zugeordnet ist.

Dieses Attribut kann auch zugewiesen werden, um die `MetaData`-Sammlung zu beeinflussen, die für jede gemappte Hierarchie für eine einzelne Basis und/oder ein einzelnes `registry` verwendet werden soll. Dies gilt sowohl für den Fall, dass eine deklarative Basisklasse verwendet wird, als auch für den Fall, dass der `registry.mapped()`-Decorator direkt verwendet wird, was Muster wie das Metadaten-pro-abstrakte-Basis-Beispiel im nächsten Abschnitt, __abstract__, ermöglicht. Ein ähnliches Muster kann wie folgt unter Verwendung von `registry.mapped()` veranschaulicht werden:

reg = registry()


class BaseOne:
    metadata = MetaData()


class BaseTwo:
    metadata = MetaData()


@reg.mapped
class ClassOne:
    __tablename__ = "t1"  # will use reg.metadata

    id = mapped_column(Integer, primary_key=True)


@reg.mapped
class ClassTwo(BaseOne):
    __tablename__ = "t1"  # will use BaseOne.metadata

    id = mapped_column(Integer, primary_key=True)


@reg.mapped
class ClassThree(BaseTwo):
    __tablename__ = "t1"  # will use BaseTwo.metadata

    id = mapped_column(Integer, primary_key=True)

Siehe auch

__abstract__

__abstract__

__abstract__ bewirkt, dass Deklarativ die Erzeugung einer Tabelle oder eines Mappers für die Klasse vollständig überspringt. Eine Klasse kann innerhalb einer Hierarchie auf die gleiche Weise wie ein Mixin (siehe Mixin und benutzerdefinierte Basisklassen) hinzugefügt werden, wodurch Unterklassen nur von der speziellen Klasse erben können.

class SomeAbstractBase(Base):
    __abstract__ = True

    def some_helpful_method(self):
        """ """

    @declared_attr
    def __mapper_args__(cls):
        return {"helpful mapper arguments": True}


class MyMappedClass(SomeAbstractBase):
    pass

Eine mögliche Verwendung von `__abstract__` ist die Verwendung einer separaten `MetaData` für verschiedene Basen.

class Base(DeclarativeBase):
    pass


class DefaultBase(Base):
    __abstract__ = True
    metadata = MetaData()


class OtherBase(Base):
    __abstract__ = True
    metadata = MetaData()

Oben verwenden Klassen, die von `DefaultBase` erben, eine `MetaData` als Registry für Tabellen, und die, die von `OtherBase` erben, verwenden eine andere. Die Tabellen selbst können dann vielleicht in verschiedenen Datenbanken erstellt werden.

DefaultBase.metadata.create_all(some_engine)
OtherBase.metadata.create_all(some_other_engine)

Siehe auch

Aufbau tieferer Hierarchien mit polymorphic_abstract - eine alternative Form der „abstrakten“ gemappten Klasse, die für Vererbungshierarchien geeignet ist.

__table_cls__

Ermöglicht die Anpassung des aufrufbaren / der Klasse, die zur Erzeugung einer Table verwendet wird. Dies ist ein sehr offener Haken, der besondere Anpassungen an einer hier erzeugten Table ermöglichen kann.

class MyMixin:
    @classmethod
    def __table_cls__(cls, name, metadata_obj, *arg, **kw):
        return Table(f"my_{name}", metadata_obj, *arg, **kw)

Der obige Mixin würde dazu führen, dass alle erzeugten Table-Objekte das Präfix "my_" enthalten, gefolgt von dem Namen, der normalerweise mit dem `__tablename__`-Attribut angegeben wird.

`__table_cls__` unterstützt auch den Fall, dass `None` zurückgegeben wird, was dazu führt, dass die Klasse als Single-Table-Inheritance im Vergleich zu ihrer Unterklasse betrachtet wird. Dies kann bei einigen Anpassungsschemata nützlich sein, um zu bestimmen, dass Single-Table-Inheritance basierend auf den Argumenten für die Tabelle selbst stattfinden soll, z. B. als Single-Inheritance definieren, wenn kein Primärschlüssel vorhanden ist.

class AutoTable:
    @declared_attr
    def __tablename__(cls):
        return cls.__name__

    @classmethod
    def __table_cls__(cls, *arg, **kw):
        for obj in arg[1:]:
            if (isinstance(obj, Column) and obj.primary_key) or isinstance(
                obj, PrimaryKeyConstraint
            ):
                return Table(*arg, **kw)

        return None


class Person(AutoTable, Base):
    id = mapped_column(Integer, primary_key=True)


class Employee(Person):
    employee_name = mapped_column(String)

Die obige `Employee`-Klasse würde als Single-Table-Inheritance gegenüber `Person` abgebildet werden; die Spalte `employee_name` würde als Mitglied der `Person`-Tabelle hinzugefügt werden.