Änderung des Attributverhaltens

Dieser Abschnitt behandelt Funktionen und Techniken zur Modifikation des Verhaltens von ORM-zugeordneten Attributen, einschließlich derjenigen, die mit mapped_column(), relationship() und anderen zugeordnet wurden.

Einfache Validatoren

Eine schnelle Möglichkeit, eine "Validierungs"-Routine zu einem Attribut hinzuzufügen, ist die Verwendung des validates()-Dekorators. Ein Attribut-Validator kann eine Ausnahme auslösen, die den Prozess der Mutation des Attributwerts stoppt, oder den gegebenen Wert in etwas anderes ändern. Validatoren werden, wie alle Attributerweiterungen, nur vom normalen Benutzer-Code aufgerufen; sie werden nicht ausgegeben, wenn der ORM das Objekt befüllt.

from sqlalchemy.orm import validates


class EmailAddress(Base):
    __tablename__ = "address"

    id = mapped_column(Integer, primary_key=True)
    email = mapped_column(String)

    @validates("email")
    def validate_email(self, key, address):
        if "@" not in address:
            raise ValueError("failed simple email validation")
        return address

Validatoren empfangen auch Ereignisse beim Hinzufügen von Elementen zu einer Sammlung.

from sqlalchemy.orm import validates


class User(Base):
    # ...

    addresses = relationship("Address")

    @validates("addresses")
    def validate_address(self, key, address):
        if "@" not in address.email:
            raise ValueError("failed simplified email validation")
        return address

Die Validierungsfunktion wird standardmäßig nicht für Ereignisse beim Entfernen aus Sammlungen ausgegeben, da die typische Erwartung ist, dass ein verworfener Wert keine Validierung erfordert. validates() unterstützt jedoch den Empfang dieser Ereignisse durch Angabe von include_removes=True im Dekorator. Wenn dieses Flag gesetzt ist, muss die Validierungsfunktion ein zusätzliches boolesches Argument empfangen, das, wenn es True ist, angibt, dass die Operation eine Entfernung ist.

from sqlalchemy.orm import validates


class User(Base):
    # ...

    addresses = relationship("Address")

    @validates("addresses", include_removes=True)
    def validate_address(self, key, address, is_remove):
        if is_remove:
            raise ValueError("not allowed to remove items from the collection")
        else:
            if "@" not in address.email:
                raise ValueError("failed simplified email validation")
            return address

Der Fall, bei dem sich gegenseitig abhängige Validatoren über eine Rückreferenz verknüpft sind, kann ebenfalls angepasst werden, indem die Option include_backrefs=False verwendet wird; diese Option verhindert, wenn sie auf False gesetzt ist, dass eine Validierungsfunktion ausgelöst wird, wenn das Ereignis als Ergebnis einer Rückreferenz auftritt.

from sqlalchemy.orm import validates


class User(Base):
    # ...

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

    @validates("addresses", include_backrefs=False)
    def validate_address(self, key, address):
        if "@" not in address:
            raise ValueError("failed simplified email validation")
        return address

Oben würde, wenn wir Address.user zuweisen, wie in some_address.user = some_user, die Funktion validate_address() *nicht* ausgelöst werden, obwohl ein Hinzufügen zu some_user.addresses stattfindet – das Ereignis wird durch eine Rückreferenz verursacht.

Beachten Sie, dass der validates()-Dekorator eine Komfortfunktion ist, die auf Attributereignissen aufbaut. Eine Anwendung, die mehr Kontrolle über die Konfiguration des Attributänderungsverhaltens benötigt, kann dieses System nutzen, das unter AttributeEvents beschrieben wird.

Objektname Beschreibung

validates(*namen, [include_removes, include_backrefs])

Dekoriert eine Methode als „Validator“ für eine oder mehrere benannte Eigenschaften.

Funktion sqlalchemy.orm.validates(*namen: str, include_removes: bool = False, include_backrefs: bool = True) Callable[[_Fn], _Fn]

Dekoriert eine Methode als „Validator“ für eine oder mehrere benannte Eigenschaften.

Bezeichnet eine Methode als Validator, eine Methode, die den Namen des Attributs sowie einen zuzuweisenden Wert empfängt oder im Falle einer Sammlung den hinzuzufügenden Wert empfängt. Die Funktion kann dann Validierungsfehler auslösen, um den Fortgang zu stoppen (wobei die integrierten Python-ValueError und AssertionError Ausnahmen angemessene Wahlmöglichkeiten sind), oder sie kann den Wert modifizieren oder ersetzen, bevor sie fortfährt. Die Funktion sollte andernfalls den gegebenen Wert zurückgeben.

Beachten Sie, dass ein Validator für eine Sammlung *nicht* eine Ladung dieser Sammlung innerhalb der Validierungsroutine auslösen kann – diese Verwendung löst eine Assertion aus, um Rekursionsüberläufe zu vermeiden. Dies ist eine reentrant Bedingung, die nicht unterstützt wird.

Parameter:
  • *namen – Liste der zu validierenden Attributnamen.

  • include_removes – Wenn True, werden auch „Entfernungs“-Ereignisse gesendet – die Validierungsfunktion muss ein zusätzliches Argument „is_remove“ akzeptieren, das ein boolescher Wert ist.

  • include_backrefs

    Standardmäßig True; wenn False, wird die Validierungsfunktion nicht ausgelöst, wenn der Ursprung ein Attributereignis ist, das über eine Rückreferenz verbunden ist. Dies kann für bidirektionale validates()-Nutzung verwendet werden, bei der nur ein Validator pro Attributoperation ausgelöst werden soll.

    Geändert in Version 2.0.16: Dieser Parameter hatte versehentlich den Standardwert False für die Versionen 2.0.0 bis 2.0.15. Sein korrekter Standardwert von True wird in 2.0.16 wiederhergestellt.

Siehe auch

Einfache Validatoren - Anwendungsbeispiele für validates()

Verwendung benutzerdefinierter Datentypen auf Core-Ebene

Eine Nicht-ORM-Methode zur Beeinflussung des Werts einer Spalte, um Daten zwischen der Darstellung in Python und der Darstellung in der Datenbank zu konvertieren, kann durch die Verwendung eines benutzerdefinierten Datentyps erreicht werden, der auf die zugeordnete Table-Metadaten angewendet wird. Dies ist häufiger bei bestimmten Arten von Kodierung/Dekodierung der Fall, die sowohl beim Senden von Daten an die Datenbank als auch beim Zurückgeben erfolgen; lesen Sie mehr darüber in der Core-Dokumentation unter Erweiterung bestehender Typen.

Verwendung von Deskriptoren und Hybriden

Eine umfassendere Methode zur Erzeugung modifizierten Verhaltens für ein Attribut ist die Verwendung von Deskriptoren. Diese werden in Python üblicherweise mit der Funktion property() verwendet. Die Standard-SQLAlchemy-Technik für Deskriptoren besteht darin, einen einfachen Deskriptor zu erstellen und diesen von einem zugeordneten Attribut mit einem anderen Namen lesen/schreiben zu lassen. Unten illustrieren wir dies mit Eigenschaften im Stil von Python 2.6.

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    # name the attribute with an underscore,
    # different from the column name
    _email = mapped_column("email", String)

    # then create an ".email" attribute
    # to get/set "._email"
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, email):
        self._email = email

Der obige Ansatz funktioniert, aber wir können noch mehr hinzufügen. Während unser EmailAddress-Objekt den Wert über den email-Deskriptor und in das _email-zugeordnete Attribut weiterleitet, hat das EmailAddress.email-Attribut auf Klassenebene nicht die üblichen Ausdruckssemantik, die mit Select verwendbar sind. Um diese bereitzustellen, verwenden wir stattdessen die hybrid-Erweiterung wie folgt:

from sqlalchemy.ext.hybrid import hybrid_property


class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    _email = mapped_column("email", String)

    @hybrid_property
    def email(self):
        return self._email

    @email.setter
    def email(self, email):
        self._email = email

Das Attribut .email bietet nicht nur Getter-/Setter-Verhalten, wenn wir eine Instanz von EmailAddress haben, sondern bietet auch einen SQL-Ausdruck, wenn es auf Klassenebene verwendet wird, d. h. direkt von der Klasse EmailAddress.

from sqlalchemy.orm import Session
from sqlalchemy import select

session = Session()

address = session.scalars(
    select(EmailAddress).where(EmailAddress.email == "address@example.com")
).one()
SELECT address.email AS address_email, address.id AS address_id FROM address WHERE address.email = ? ('address@example.com',)
address.email = "otheraddress@example.com" session.commit()
UPDATE address SET email=? WHERE address.id = ? ('otheraddress@example.com', 1) COMMIT

Die hybrid_property ermöglicht es uns auch, das Verhalten des Attributs zu ändern, einschließlich der Definition separater Verhaltensweisen, wenn das Attribut auf Instanzebene im Gegensatz zur Klassen-/Ausdrucksebene aufgerufen wird, unter Verwendung des Modifikators hybrid_property.expression(). Wenn wir beispielsweise automatisch einen Hostnamen hinzufügen möchten, könnten wir zwei Sätze von String-Manipulationslogiken definieren:

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    _email = mapped_column("email", String)

    @hybrid_property
    def email(self):
        """Return the value of _email up until the last twelve
        characters."""

        return self._email[:-12]

    @email.setter
    def email(self, email):
        """Set the value of _email, tacking on the twelve character
        value @example.com."""

        self._email = email + "@example.com"

    @email.expression
    def email(cls):
        """Produce a SQL expression that represents the value
        of the _email column, minus the last twelve characters."""

        return func.substr(cls._email, 0, func.length(cls._email) - 12)

Oben gibt der Zugriff auf die email-Eigenschaft einer Instanz von EmailAddress den Wert des _email-Attributs zurück und entfernt oder fügt den Hostnamen @example.com aus dem Wert hinzu oder entfernt ihn. Wenn wir gegen das email-Attribut abfragen, wird eine SQL-Funktion gerendert, die dasselbe bewirkt.

address = session.scalars(
    select(EmailAddress).where(EmailAddress.email == "address")
).one()
SELECT address.email AS address_email, address.id AS address_id FROM address WHERE substr(address.email, ?, length(address.email) - ?) = ? (0, 12, 'address')

Lesen Sie mehr über Hybride unter Hybride Attribute.

Synonyme

Synonyme sind ein Mapper-Konstrukt, das es jedem Attribut einer Klasse ermöglicht, ein anderes zugeordnetes Attribut zu "spiegeln".

Im Grunde ist das Synonym eine einfache Möglichkeit, ein bestimmtes Attribut unter einem zusätzlichen Namen verfügbar zu machen.

from sqlalchemy.orm import synonym


class MyClass(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    job_status = mapped_column(String(50))

    status = synonym("job_status")

Die obige Klasse MyClass hat zwei Attribute, .job_status und .status, die sich wie ein Attribut verhalten werden, sowohl auf Ausdrucksebene.

>>> print(MyClass.job_status == "some_status")
my_table.job_status = :job_status_1
>>> print(MyClass.status == "some_status")
my_table.job_status = :job_status_1

als auch auf Instanzebene.

>>> m1 = MyClass(status="x")
>>> m1.status, m1.job_status
('x', 'x')

>>> m1.job_status = "y"
>>> m1.status, m1.job_status
('y', 'y')

Die synonym() kann für jede Art von zugeordnetem Attribut verwendet werden, das von MapperProperty erbt, einschließlich zugeordneter Spalten und Beziehungen, sowie Synonyme selbst.

Über ein einfaches Spiegeln hinaus kann synonym() auch auf einen vom Benutzer definierten Deskriptor verweisen. Wir können unserem status-Synonym eine @property zuweisen.

class MyClass(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    status = mapped_column(String(50))

    @property
    def job_status(self):
        return "Status: " + self.status

    job_status = synonym("status", descriptor=job_status)

Bei der Verwendung von Declarative kann das obige Muster mithilfe des synonym_for()-Dekorators kürzer ausgedrückt werden.

from sqlalchemy.ext.declarative import synonym_for


class MyClass(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    status = mapped_column(String(50))

    @synonym_for("status")
    @property
    def job_status(self):
        return "Status: " + self.status

Während synonym() für einfaches Spiegeln nützlich ist, wird der Anwendungsfall der Erweiterung des Attributverhaltens mit Deskriptoren im modernen Gebrauch besser durch die Funktion für hybride Attribute gehandhabt, die stärker auf Python-Deskriptoren ausgerichtet ist. Technisch gesehen kann ein synonym() alles tun, was eine hybrid_property kann, da es auch die Injektion benutzerdefinierter SQL-Funktionen unterstützt, aber das Hybrid ist in komplexeren Situationen einfacher zu verwenden.

Objektname Beschreibung

synonym(name, *, [map_column, descriptor, comparator_factory, init, repr, default, default_factory, compare, kw_only, hash, info, doc])

Bezeichnet einen Attributnamen als Synonym für eine zugeordnete Eigenschaft, sodass das Attribut das Wert- und Ausdrucksverhalten eines anderen Attributs widerspiegelt.

Funktion sqlalchemy.orm.synonym(name: str, *, map_column: bool | None = None, descriptor: Any | None = None, comparator_factory: Type[PropComparator[_T]] | None = None, init: _NoArg | bool = _NoArg.NO_ARG, repr: _NoArg | bool = _NoArg.NO_ARG, default: _NoArg | _T = _NoArg.NO_ARG, default_factory: _NoArg | Callable[[], _T] = _NoArg.NO_ARG, compare: _NoArg | bool = _NoArg.NO_ARG, kw_only: _NoArg | bool = _NoArg.NO_ARG, hash: _NoArg | bool | None = _NoArg.NO_ARG, info: _InfoType | None = None, doc: str | None = None) Synonym[Any]

Bezeichnet einen Attributnamen als Synonym für eine zugeordnete Eigenschaft, sodass das Attribut das Wert- und Ausdrucksverhalten eines anderen Attributs widerspiegelt.

z. B.

class MyClass(Base):
    __tablename__ = "my_table"

    id = Column(Integer, primary_key=True)
    job_status = Column(String(50))

    status = synonym("job_status")
Parameter:
  • name – Der Name der vorhandenen zugeordneten Eigenschaft. Dies kann sich auf den String-Namen eines ORM-zugeordneten Attributs auf der Klasse beziehen, einschließlich spaltengebundener Attribute und Beziehungen.

  • descriptor – Ein Python-Deskriptor, der als Getter (und potenziell als Setter) verwendet wird, wenn auf dieses Attribut auf Instanzebene zugegriffen wird.

  • map_column

    Nur für klassische Zuordnungen und Zuordnungen zu einem vorhandenen Table-Objekt. Wenn True, sucht die synonym()-Konstruktion die Column auf der zugeordneten Tabelle, die normalerweise mit dem Attributnamen dieses Synonyms verbunden wäre, und erzeugt eine neue ColumnProperty, die stattdessen diese Column dem alternativen Namen zuordnet, der als „name“-Argument des Synonyms angegeben wurde; auf diese Weise ist der übliche Schritt, die Zuordnung der Column zu einem anderen Namen neu zu definieren, nicht mehr notwendig. Dies ist normalerweise beabsichtigt, wenn eine Column durch ein Attribut ersetzt werden soll, das ebenfalls einen Deskriptor verwendet, d. h. in Verbindung mit dem Parameter synonym.descriptor.

    my_table = Table(
        "my_table",
        metadata,
        Column("id", Integer, primary_key=True),
        Column("job_status", String(50)),
    )
    
    
    class MyClass:
        @property
        def _job_status_descriptor(self):
            return "Status: %s" % self._job_status
    
    
    mapper(
        MyClass,
        my_table,
        properties={
            "job_status": synonym(
                "_job_status",
                map_column=True,
                descriptor=MyClass._job_status_descriptor,
            )
        },
    )

    Oben wird das Attribut mit dem Namen _job_status automatisch der Spalte job_status zugeordnet.

    >>> j1 = MyClass()
    >>> j1._job_status = "employed"
    >>> j1.job_status
    Status: employed

    Bei der Verwendung von Declarative, um einen Deskriptor in Verbindung mit einem Synonym bereitzustellen, verwenden Sie die Hilfsfunktion sqlalchemy.ext.declarative.synonym_for(). Beachten Sie jedoch, dass die Funktion für hybride Eigenschaften in der Regel bevorzugt werden sollte, insbesondere bei der Neudefinition des Attributverhaltens.

  • info – Optionales Datenwörterbuch, das in das Attribut InspectionAttr.info dieses Objekts aufgenommen wird.

  • comparator_factory

    Eine Unterklasse von PropComparator, die ein benutzerdefiniertes Vergleichsverhalten auf SQL-Ausdrucksebene bereitstellt.

    Hinweis

    Für den Anwendungsfall, ein Attribut bereitzustellen, das sowohl das Verhalten auf Python-Ebene als auch das Verhalten auf SQL-Ausdrucksebene eines Attributs neu definiert, beachten Sie bitte die unter Verwendung von Deskriptoren und Hybriden eingeführte Hybrid-Attributfunktion als effektivere Technik.

Siehe auch

Synonyme - Überblick über Synonyme

synonym_for() - ein Helfer für Declarative

Verwendung von Deskriptoren und Hybriden - Die Hybrid-Attributerweiterung bietet einen aktualisierten Ansatz zur Erweiterung des Attributverhaltens, der flexibler ist als mit Synonymen erreichbar.

Operator-Anpassung

Die "Operatoren", die von der SQLAlchemy ORM und der Core Expression Language verwendet werden, sind vollständig anpassbar. Zum Beispiel macht der Vergleichsausdruck User.name == 'ed' Gebrauch von einem in Python selbst eingebauten Operator namens operator.eq – die tatsächliche SQL-Konstruktion, die SQLAlchemy mit einem solchen Operator assoziiert, kann modifiziert werden. Neue Operationen können auch Spaltenausdrücken zugeordnet werden. Die Operatoren, die für Spaltenausdrücke gelten, werden am direktesten auf Typenebene neu definiert – siehe Abschnitt Neudefinition und Erstellung neuer Operatoren für eine Beschreibung.

ORM-Funktionen wie column_property(), relationship() und composite() bieten ebenfalls die Neudefinition von Operatoren auf ORM-Ebene, indem eine Unterklasse von PropComparator an das Argument comparator_factory jeder Funktion übergeben wird. Die Anpassung von Operatoren auf dieser Ebene ist ein seltener Anwendungsfall. Siehe die Dokumentation unter PropComparator für einen Überblick.