Hybrid-Attribute

Definieren Sie Attribute auf ORM-gemappten Klassen, die ein "hybrides" Verhalten aufweisen.

"Hybrid" bedeutet, dass das Attribut unterschiedliche Verhaltensweisen auf Klassenebene und Instanzenebene aufweist.

Die hybrid-Erweiterung bietet eine spezielle Form des Methoden-Dekorators und hat minimale Abhängigkeiten vom Rest von SQLAlchemy. Ihre grundlegende Funktionsweise kann mit jedem deskriptorbasierten Ausdruckssystem arbeiten.

Betrachten Sie ein Mapping Interval, das ganzzahlige start- und end-Werte darstellt. Wir können höherwertige Funktionen auf gemappten Klassen definieren, die auf Klassenebene SQL-Ausdrücke und auf Instanzenebene Python-Ausdrucksauswertung erzeugen. Unten kann jede Funktion, die mit hybrid_method oder hybrid_property dekoriert ist, self als Instanz der Klasse erhalten oder je nach Kontext die Klasse direkt erhalten.

from __future__ import annotations

from sqlalchemy.ext.hybrid import hybrid_method
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


class Base(DeclarativeBase):
    pass


class Interval(Base):
    __tablename__ = "interval"

    id: Mapped[int] = mapped_column(primary_key=True)
    start: Mapped[int]
    end: Mapped[int]

    def __init__(self, start: int, end: int):
        self.start = start
        self.end = end

    @hybrid_property
    def length(self) -> int:
        return self.end - self.start

    @hybrid_method
    def contains(self, point: int) -> bool:
        return (self.start <= point) & (point <= self.end)

    @hybrid_method
    def intersects(self, other: Interval) -> bool:
        return self.contains(other.start) | self.contains(other.end)

Oben gibt die length-Eigenschaft die Differenz zwischen den end- und start-Attributen zurück. Mit einer Instanz von Interval erfolgt diese Subtraktion in Python unter Verwendung normaler Python-Deskriptormechanismen.

>>> i1 = Interval(5, 10)
>>> i1.length
5

Wenn Sie sich mit der Interval-Klasse selbst befassen, wertet der hybrid_property-Deskriptor den Funktionskörper aus, wobei die Interval-Klasse als Argument übergeben wird. Wenn dies mit SQLAlchemy-Ausdrucksmechanismen ausgewertet wird, gibt dies einen neuen SQL-Ausdruck zurück.

>>> from sqlalchemy import select
>>> print(select(Interval.length))
SELECT interval."end" - interval.start AS length FROM interval
>>> print(select(Interval).filter(Interval.length > 10))
SELECT interval.id, interval.start, interval."end" FROM interval WHERE interval."end" - interval.start > :param_1

Filtermethoden wie Select.filter_by() werden auch mit Hybrid-Attributen unterstützt.

>>> print(select(Interval).filter_by(length=5))
SELECT interval.id, interval.start, interval."end" FROM interval WHERE interval."end" - interval.start = :param_1

Die Interval-Klassenbeispiel illustriert auch zwei Methoden, contains() und intersects(), die mit hybrid_method dekoriert sind. Dieser Dekorator wendet dieselbe Idee auf Methoden an, die hybrid_property auf Attribute anwendet. Die Methoden geben boolesche Werte zurück und nutzen die bitweisen Operatoren | und & von Python, um äquivalente boolesche Verhaltensweisen auf Instanz- und SQL-Ausdrucksebene zu erzeugen.

>>> i1.contains(6)
True
>>> i1.contains(15)
False
>>> i1.intersects(Interval(7, 18))
True
>>> i1.intersects(Interval(25, 29))
False

>>> print(select(Interval).filter(Interval.contains(15)))
SELECT interval.id, interval.start, interval."end" FROM interval WHERE interval.start <= :start_1 AND interval."end" > :end_1
>>> ia = aliased(Interval) >>> print(select(Interval, ia).filter(Interval.intersects(ia)))
SELECT interval.id, interval.start, interval."end", interval_1.id AS interval_1_id, interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end FROM interval, interval AS interval_1 WHERE interval.start <= interval_1.start AND interval."end" > interval_1.start OR interval.start <= interval_1."end" AND interval."end" > interval_1."end"

Definieren von Ausdrucksverhalten, das sich vom Attributverhalten unterscheidet

Im vorherigen Abschnitt war unsere Verwendung der bitweisen Operatoren & und | innerhalb der Methoden Interval.contains und Interval.intersects glücklich, da unsere Funktionen auf zwei booleschen Werten operierten, um einen neuen zu erhalten. In vielen Fällen gibt es genug Unterschiede zwischen der Konstruktion einer Funktion in Python und einem SQLAlchemy SQL-Ausdruck, dass zwei separate Python-Ausdrücke definiert werden sollten. Der hybrid-Dekorator definiert zu diesem Zweck einen Modifikator hybrid_property.expression(). Als Beispiel definieren wir den Radius des Intervalls, der die Verwendung der Absolutwertfunktion erfordert.

from sqlalchemy import ColumnElement
from sqlalchemy import Float
from sqlalchemy import func
from sqlalchemy import type_coerce


class Interval(Base):
    # ...

    @hybrid_property
    def radius(self) -> float:
        return abs(self.length) / 2

    @radius.inplace.expression
    @classmethod
    def _radius_expression(cls) -> ColumnElement[float]:
        return type_coerce(func.abs(cls.length) / 2, Float)

Im obigen Beispiel wird die hybrid_property, die zuerst dem Namen Interval.radius zugewiesen wird, durch eine nachfolgende Methode namens Interval._radius_expression ergänzt, unter Verwendung des Dekorators @radius.inplace.expression, der zwei Modifikatoren hybrid_property.inplace und hybrid_property.expression verkettet. Die Verwendung von hybrid_property.inplace zeigt an, dass der Modifikator hybrid_property.expression() das vorhandene Hybridobjekt unter Interval.radius vor Ort mutieren soll, ohne ein neues Objekt zu erstellen. Anmerkungen zu diesem Modifikator und seiner Begründung werden im nächsten Abschnitt Verwenden von inplace zur Erstellung von PEP-484-konformen Hybrid-Eigenschaften erläutert. Die Verwendung von @classmethod ist optional und dient lediglich dazu, Typisierungswerkzeugen einen Hinweis zu geben, dass cls in diesem Fall die Interval-Klasse und nicht eine Instanz von Interval sein soll.

Hinweis

hybrid_property.inplace sowie die Verwendung von @classmethod zur korrekten Typunterstützung sind ab SQLAlchemy 2.0.4 verfügbar und funktionieren in früheren Versionen nicht.

Mit Interval.radius, das nun ein Ausdruckselement enthält, wird beim Zugriff auf Interval.radius auf Klassenebene die SQL-Funktion ABS() zurückgegeben.

>>> from sqlalchemy import select
>>> print(select(Interval).filter(Interval.radius > 5))
SELECT interval.id, interval.start, interval."end" FROM interval WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1

Verwenden von inplace zur Erstellung von PEP-484-konformen Hybrid-Eigenschaften

Im vorherigen Abschnitt wird ein hybrid_property-Dekorator veranschaulicht, der zwei separate funktionsbasierte Methoden umfasst, die beide ein einzelnes Objektattribut namens Interval.radius erzeugen. Es gibt tatsächlich mehrere verschiedene Modifikatoren, die wir für hybrid_property verwenden können, darunter hybrid_property.expression(), hybrid_property.setter() und hybrid_property.update_expression().

SQLAlchemy's hybrid_property-Dekorator sieht vor, dass das Hinzufügen dieser Methoden auf dieselbe Weise erfolgen kann wie bei Pythons integriertem @property-Dekorator, wobei die idiomatische Verwendung darin besteht, das Attribut wiederholt neu zu definieren, unter Verwendung desselben Attributnamens jedes Mal, wie im folgenden Beispiel, das die Verwendung von hybrid_property.setter() und hybrid_property.expression() für den Interval.radius-Deskriptor veranschaulicht.

# correct use, however is not accepted by pep-484 tooling


class Interval(Base):
    # ...

    @hybrid_property
    def radius(self):
        return abs(self.length) / 2

    @radius.setter
    def radius(self, value):
        self.length = value * 2

    @radius.expression
    def radius(cls):
        return type_coerce(func.abs(cls.length) / 2, Float)

Oben gibt es drei Interval.radius-Methoden, aber da jede zuerst mit dem hybrid_property-Dekorator und dann mit dem Namen @radius selbst dekoriert wird, ist das Endergebnis, dass Interval.radius ein einzelnes Attribut mit drei verschiedenen Funktionen darin ist. Dieser Stil der Verwendung stammt aus Pythons dokumentierter Verwendung von @property. Es ist wichtig zu beachten, dass, wie sowohl @property als auch hybrid_property funktionieren, eine Kopie des Deskriptors jedes Mal erstellt wird. Das heißt, jeder Aufruf von @radius.expression, @radius.setter usw. erstellt ein komplett neues Objekt. Dies ermöglicht die Neudefinition des Attributs in Unterklassen ohne Probleme (siehe Wiederverwenden von Hybrid-Eigenschaften über Unterklassen hinweg später in diesem Abschnitt, wie dies verwendet wird).

Der obige Ansatz ist jedoch nicht mit Typisierungswerkzeugen wie mypy und pyright kompatibel. Pythons eigener @property-Dekorator hat diese Einschränkung nur, weil diese Werkzeuge das Verhalten von @property hartkodieren, was bedeutet, dass diese Syntax für SQLAlchemy unter der PEP 484-Konformität nicht verfügbar ist.

Um eine sinnvolle Syntax zu erzeugen und gleichzeitig typisierungskonform zu bleiben, ermöglicht der Dekorator hybrid_property.inplace die Wiederverwendung desselben Dekorators mit verschiedenen Methodennamen, während immer noch ein einzelner Dekorator unter einem Namen erzeugt wird.

# correct use which is also accepted by pep-484 tooling


class Interval(Base):
    # ...

    @hybrid_property
    def radius(self) -> float:
        return abs(self.length) / 2

    @radius.inplace.setter
    def _radius_setter(self, value: float) -> None:
        # for example only
        self.length = value * 2

    @radius.inplace.expression
    @classmethod
    def _radius_expression(cls) -> ColumnElement[float]:
        return type_coerce(func.abs(cls.length) / 2, Float)

Die Verwendung von hybrid_property.inplace qualifiziert die Verwendung des Dekorators weiter, dass keine neue Kopie erstellt werden soll, wodurch der Name Interval.radius erhalten bleibt und gleichzeitig zusätzliche Methoden Interval._radius_setter und Interval._radius_expression unterschiedlich benannt werden können.

Neu in Version 2.0.4: hybrid_property.inplace hinzugefügt, um eine weniger ausführliche Konstruktion von zusammengesetzten hybrid_property-Objekten zu ermöglichen, ohne wiederholte Methodennamen verwenden zu müssen. Außerdem wurde die Verwendung von @classmethod innerhalb von hybrid_property.expression, hybrid_property.update_expression und hybrid_property.comparator erlaubt, damit Typisierungswerkzeuge cls als Klasse und nicht als Instanz in der Methodensignatur identifizieren können.

Definieren von Settern

Der Modifikator hybrid_property.setter() ermöglicht die Konstruktion einer benutzerdefinierten Setter-Methode, die Werte im Objekt ändern kann.

class Interval(Base):
    # ...

    @hybrid_property
    def length(self) -> int:
        return self.end - self.start

    @length.inplace.setter
    def _length_setter(self, value: int) -> None:
        self.end = self.start + value

Die Methode length(self, value) wird nun beim Set aufgerufen.

>>> i1 = Interval(5, 10)
>>> i1.length
5
>>> i1.length = 12
>>> i1.end
17

Erlauben von Massen-ORM-Updates

Ein Hybrid kann einen benutzerdefinierten "UPDATE"-Handler definieren, wenn ORM-fähige Updates verwendet werden, wodurch der Hybrid in der SET-Klausel des Updates verwendet werden kann.

Normalerweise, wenn ein Hybrid mit update() verwendet wird, wird der SQL-Ausdruck als Spalte verwendet, die das Ziel des SET ist. Wenn unsere Interval-Klasse einen hybriden start_point hätte, der auf Interval.start verweist, könnte dies direkt ersetzt werden.

from sqlalchemy import update

stmt = update(Interval).values({Interval.start_point: 10})

Wenn jedoch ein zusammengesetzter Hybrid wie Interval.length verwendet wird, repräsentiert dieser Hybrid mehr als eine Spalte. Wir können einen Handler einrichten, der einen Wert im VALUES-Ausdruck berücksichtigt, der dies beeinflussen kann, indem er den Dekorator hybrid_property.update_expression() verwendet. Ein Handler, der ähnlich wie unser Setter funktioniert, wäre:

from typing import List, Tuple, Any


class Interval(Base):
    # ...

    @hybrid_property
    def length(self) -> int:
        return self.end - self.start

    @length.inplace.setter
    def _length_setter(self, value: int) -> None:
        self.end = self.start + value

    @length.inplace.update_expression
    def _length_update_expression(
        cls, value: Any
    ) -> List[Tuple[Any, Any]]:
        return [(cls.end, cls.start + value)]

Wenn wir Interval.length in einem UPDATE-Ausdruck verwenden, erhalten wir einen hybriden SET-Ausdruck.

>>> from sqlalchemy import update
>>> print(update(Interval).values({Interval.length: 25}))
UPDATE interval SET "end"=(interval.start + :start_1)

Dieser SET-Ausdruck wird automatisch vom ORM verarbeitet.

Siehe auch

ORM-fähige INSERT-, UPDATE- und DELETE-Anweisungen - enthält Hintergründe zu ORM-fähigen UPDATE-Anweisungen

Arbeiten mit Beziehungen

Es gibt keinen wesentlichen Unterschied beim Erstellen von Hybriden, die mit verwandten Objekten arbeiten, im Gegensatz zu spaltenbasierten Daten. Die Notwendigkeit für unterschiedliche Ausdrücke ist tendenziell größer. Die beiden Varianten, die wir veranschaulichen werden, sind der "join-abhängige" Hybrid und der "korrelierte Subquery"-Hybrid.

Join-abhängige Beziehungs-Hybride

Betrachten Sie die folgende deklarative Zuordnung, die einen User mit einem SavingsAccount verknüpft.

from __future__ import annotations

from decimal import Decimal
from typing import cast
from typing import List
from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Numeric
from sqlalchemy import String
from sqlalchemy import SQLColumnExpression
from sqlalchemy.ext.hybrid import hybrid_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 SavingsAccount(Base):
    __tablename__ = "account"
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
    balance: Mapped[Decimal] = mapped_column(Numeric(15, 5))

    owner: Mapped[User] = relationship(back_populates="accounts")


class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100))

    accounts: Mapped[List[SavingsAccount]] = relationship(
        back_populates="owner", lazy="selectin"
    )

    @hybrid_property
    def balance(self) -> Optional[Decimal]:
        if self.accounts:
            return self.accounts[0].balance
        else:
            return None

    @balance.inplace.setter
    def _balance_setter(self, value: Optional[Decimal]) -> None:
        assert value is not None

        if not self.accounts:
            account = SavingsAccount(owner=self)
        else:
            account = self.accounts[0]
        account.balance = value

    @balance.inplace.expression
    @classmethod
    def _balance_expression(cls) -> SQLColumnExpression[Optional[Decimal]]:
        return cast(
            "SQLColumnExpression[Optional[Decimal]]",
            SavingsAccount.balance,
        )

Die obige hybride Eigenschaft balance arbeitet mit dem ersten SavingsAccount-Eintrag in der Liste der Konten für diesen Benutzer. Die In-Python-Getter-/Setter-Methoden können accounts als Python-Liste behandeln, die auf self verfügbar ist.

Tipp

Der Getter User.balance im obigen Beispiel greift auf die self.accounts-Sammlung zu, die normalerweise über die auf der User.balance relationship() konfigurierte selectinload()-Ladestrategie geladen wird. Die Standard-Ladestrategie, wenn auf relationship() nichts anderes angegeben ist, ist lazyload(), die SQL bei Bedarf ausgibt. Bei der Verwendung von asyncio werden On-Demand-Loader wie lazyload() nicht unterstützt, daher ist Vorsicht geboten, um sicherzustellen, dass die self.accounts-Sammlung für diesen hybriden Zugriff verfügbar ist, wenn asyncio verwendet wird.

Auf Ausdrucsebene wird erwartet, dass die User-Klasse in einem geeigneten Kontext verwendet wird, sodass ein entsprechender Join zu SavingsAccount vorhanden ist.

>>> from sqlalchemy import select
>>> print(
...     select(User, User.balance)
...     .join(User.accounts)
...     .filter(User.balance > 5000)
... )
SELECT "user".id AS user_id, "user".name AS user_name, account.balance AS account_balance FROM "user" JOIN account ON "user".id = account.user_id WHERE account.balance > :balance_1

Beachten Sie jedoch, dass sich dieses Problem, während die Instanz-Level-Accessor berücksichtigen müssen, ob self.accounts überhaupt vorhanden ist, auf der SQL-Ausdrucksebene anders äußert, wo wir im Grunde einen äußeren Join verwenden würden.

>>> from sqlalchemy import select
>>> from sqlalchemy import or_
>>> print(
...     select(User, User.balance)
...     .outerjoin(User.accounts)
...     .filter(or_(User.balance < 5000, User.balance == None))
... )
SELECT "user".id AS user_id, "user".name AS user_name, account.balance AS account_balance FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id WHERE account.balance < :balance_1 OR account.balance IS NULL

Korrelierte Subquery-Beziehungs-Hybride

Wir können natürlich davon absehen, von der Verwendung von Joins in der umschließenden Abfrage abhängig zu sein, zugunsten der korrelierten Subquery, die portabel in einen einzelnen Spaltenausdruck verpackt werden kann. Eine korrelierte Subquery ist portabler, aber oft schlechter auf SQL-Ebene. Unter Verwendung derselben Technik, die unter Verwenden von column_property veranschaulicht wird, können wir unser SavingsAccount-Beispiel anpassen, um die Salden für *alle* Konten zu aggregieren und eine korrelierte Subquery für den Spaltenausdruck zu verwenden.

from __future__ import annotations

from decimal import Decimal
from typing import List

from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import Numeric
from sqlalchemy import select
from sqlalchemy import SQLColumnExpression
from sqlalchemy import String
from sqlalchemy.ext.hybrid import hybrid_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 SavingsAccount(Base):
    __tablename__ = "account"
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
    balance: Mapped[Decimal] = mapped_column(Numeric(15, 5))

    owner: Mapped[User] = relationship(back_populates="accounts")


class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100))

    accounts: Mapped[List[SavingsAccount]] = relationship(
        back_populates="owner", lazy="selectin"
    )

    @hybrid_property
    def balance(self) -> Decimal:
        return sum(
            (acc.balance for acc in self.accounts), start=Decimal("0")
        )

    @balance.inplace.expression
    @classmethod
    def _balance_expression(cls) -> SQLColumnExpression[Decimal]:
        return (
            select(func.sum(SavingsAccount.balance))
            .where(SavingsAccount.user_id == cls.id)
            .label("total_balance")
        )

Das obige Rezept liefert uns die balance-Spalte, die eine korrelierte SELECT-Anweisung rendert.

>>> from sqlalchemy import select
>>> print(select(User).filter(User.balance > 400))
SELECT "user".id, "user".name FROM "user" WHERE ( SELECT sum(account.balance) AS sum_1 FROM account WHERE account.user_id = "user".id ) > :param_1

Erstellen von benutzerdefinierten Komparatoren

Die Hybrid-Eigenschaft enthält auch eine Hilfsfunktion, die die Erstellung von benutzerdefinierten Komparatoren ermöglicht. Ein Komparatorobjekt ermöglicht die individuelle Anpassung des Verhaltens jedes SQLAlchemy-Ausdrucksoperators. Sie sind nützlich beim Erstellen benutzerdefinierter Typen, die auf der SQL-Seite ein sehr eigenartiges Verhalten aufweisen.

Hinweis

Der in diesem Abschnitt eingeführte Dekorator hybrid_property.comparator() ersetzt die Verwendung des Dekorators hybrid_property.expression(). Sie können nicht zusammen verwendet werden.

Die unten stehende Beispielklasse ermöglicht case-insensitive Vergleiche des Attributs mit dem Namen word_insensitive.

from __future__ import annotations

from typing import Any

from sqlalchemy import ColumnElement
from sqlalchemy import func
from sqlalchemy.ext.hybrid import Comparator
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


class Base(DeclarativeBase):
    pass


class CaseInsensitiveComparator(Comparator[str]):
    def __eq__(self, other: Any) -> ColumnElement[bool]:  # type: ignore[override]  # noqa: E501
        return func.lower(self.__clause_element__()) == func.lower(other)


class SearchWord(Base):
    __tablename__ = "searchword"

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

    @hybrid_property
    def word_insensitive(self) -> str:
        return self.word.lower()

    @word_insensitive.inplace.comparator
    @classmethod
    def _word_insensitive_comparator(cls) -> CaseInsensitiveComparator:
        return CaseInsensitiveComparator(cls.word)

Oben wendet die SQL-Ausdrucksauswertung gegen word_insensitive die SQL-Funktion LOWER() auf beide Seiten an.

>>> from sqlalchemy import select
>>> print(select(SearchWord).filter_by(word_insensitive="Trucks"))
SELECT searchword.id, searchword.word FROM searchword WHERE lower(searchword.word) = lower(:lower_1)

Die obige CaseInsensitiveComparator implementiert einen Teil der ColumnOperators-Schnittstelle. Eine "Kooperations"-Operation wie die Kleinschreibung kann mit Operators.operate() auf alle Vergleichsoperationen (d.h. eq, lt, gt usw.) angewendet werden.

class CaseInsensitiveComparator(Comparator):
    def operate(self, op, other, **kwargs):
        return op(
            func.lower(self.__clause_element__()),
            func.lower(other),
            **kwargs,
        )

Wiederverwenden von Hybrid-Eigenschaften über Unterklassen hinweg

Ein Hybrid kann von einer Oberklasse referenziert werden, um Methoden wie hybrid_property.getter() und hybrid_property.setter() zu modifizieren, um diese Methoden in einer Unterklasse neu zu definieren. Dies ähnelt der Funktionsweise des standardmäßigen Python @property-Objekts.

class FirstNameOnly(Base):
    # ...

    first_name: Mapped[str]

    @hybrid_property
    def name(self) -> str:
        return self.first_name

    @name.inplace.setter
    def _name_setter(self, value: str) -> None:
        self.first_name = value


class FirstNameLastName(FirstNameOnly):
    # ...

    last_name: Mapped[str]

    # 'inplace' is not used here; calling getter creates a copy
    # of FirstNameOnly.name that is local to FirstNameLastName
    @FirstNameOnly.name.getter
    def name(self) -> str:
        return self.first_name + " " + self.last_name

    @name.inplace.setter
    def _name_setter(self, value: str) -> None:
        self.first_name, self.last_name = value.split(" ", 1)

Oben verweist die Klasse FirstNameLastName auf den Hybrid von FirstNameOnly.name, um dessen Getter und Setter für die Unterklasse neu zu belegen.

Beim Überschreiben von hybrid_property.expression() und hybrid_property.comparator() allein als erste Referenz zur Oberklasse, kollidieren diese Namen mit den gleichnamigen Accessoren des QueryableAttribute-Objekts auf Klassenebene, das auf Klassenebene zurückgegeben wird. Um diese Methoden beim direkten Verweis auf den übergeordneten Klassendeskriptor zu überschreiben, fügen Sie den speziellen Qualifikator hybrid_property.overrides hinzu, der das instrumentierte Attribut zurück zum Hybridobjekt dereferenziert.

class FirstNameLastName(FirstNameOnly):
    # ...

    last_name: Mapped[str]

    @FirstNameOnly.name.overrides.expression
    @classmethod
    def name(cls):
        return func.concat(cls.first_name, " ", cls.last_name)

Hybrid-Wertobjekte

Beachten Sie in unserem vorherigen Beispiel, dass, wenn wir das Attribut word_insensitive einer Instanz von SearchWord mit einem einfachen Python-String vergleichen würden, der einfache Python-String nicht in Kleinschreibung konvertiert würde – die von uns erstellte CaseInsensitiveComparator, die von @word_insensitive.comparator zurückgegeben wird, gilt nur für die SQL-Seite.

Eine umfassendere Form des benutzerdefinierten Komparators ist die Konstruktion eines Hybrid Value Object. Diese Technik wendet den Zielwert oder -ausdruck auf ein Wertobjekt an, das dann von dem Accessor in allen Fällen zurückgegeben wird. Das Wertobjekt ermöglicht die Kontrolle über alle Operationen auf dem Wert sowie darüber, wie verglichene Werte behandelt werden, sowohl auf der SQL-Ausdrucksseite als auch auf der Python-Wertseite. Ersetzen der vorherigen CaseInsensitiveComparator-Klasse durch eine neue CaseInsensitiveWord-Klasse.

class CaseInsensitiveWord(Comparator):
    "Hybrid value representing a lower case representation of a word."

    def __init__(self, word):
        if isinstance(word, basestring):
            self.word = word.lower()
        elif isinstance(word, CaseInsensitiveWord):
            self.word = word.word
        else:
            self.word = func.lower(word)

    def operate(self, op, other, **kwargs):
        if not isinstance(other, CaseInsensitiveWord):
            other = CaseInsensitiveWord(other)
        return op(self.word, other.word, **kwargs)

    def __clause_element__(self):
        return self.word

    def __str__(self):
        return self.word

    key = "word"
    "Label to apply to Query tuple results"

Oben repräsentiert das CaseInsensitiveWord-Objekt self.word, das eine SQL-Funktion oder ein natives Python-Objekt sein kann. Durch die Überschreibung von operate() und __clause_element__(), um im Sinne von self.word zu arbeiten, werden alle Vergleichsoperationen auf der "konvertierten" Form von word basieren, sei es auf der SQL- oder der Python-Seite. Unsere SearchWord-Klasse kann nun das CaseInsensitiveWord-Objekt bedingungslos von einem einzigen Hybridaufruf liefern.

class SearchWord(Base):
    __tablename__ = "searchword"
    id: Mapped[int] = mapped_column(primary_key=True)
    word: Mapped[str]

    @hybrid_property
    def word_insensitive(self) -> CaseInsensitiveWord:
        return CaseInsensitiveWord(self.word)

Das Attribut word_insensitive hat nun universell ein case-insensitive Vergleichsverhalten, einschließlich SQL-Ausdruck gegen Python-Ausdruck (beachten Sie, dass der Python-Wert hier auf der Python-Seite in Kleinschreibung konvertiert wird).

>>> print(select(SearchWord).filter_by(word_insensitive="Trucks"))
SELECT searchword.id AS searchword_id, searchword.word AS searchword_word FROM searchword WHERE lower(searchword.word) = :lower_1

SQL-Ausdruck gegen SQL-Ausdruck

>>> from sqlalchemy.orm import aliased
>>> sw1 = aliased(SearchWord)
>>> sw2 = aliased(SearchWord)
>>> print(
...     select(sw1.word_insensitive, sw2.word_insensitive).filter(
...         sw1.word_insensitive > sw2.word_insensitive
...     )
... )
SELECT lower(searchword_1.word) AS lower_1, lower(searchword_2.word) AS lower_2 FROM searchword AS searchword_1, searchword AS searchword_2 WHERE lower(searchword_1.word) > lower(searchword_2.word)

Nur-Python-Ausdruck

>>> ws1 = SearchWord(word="SomeWord")
>>> ws1.word_insensitive == "sOmEwOrD"
True
>>> ws1.word_insensitive == "XOmEwOrX"
False
>>> print(ws1.word_insensitive)
someword

Das Hybrid Value-Muster ist sehr nützlich für jede Art von Wert, die mehrere Darstellungen haben kann, wie z.B. Zeitstempel, Zeitdifferenzen, Maßeinheiten, Währungen und verschlüsselte Passwörter.

Siehe auch

Hybride und werteagnostische Typen – im techspot.zzzeek.org Blog

Werteagnostische Typen, Teil II – im techspot.zzzeek.org Blog

API-Referenz

Objektname Beschreibung

Comparator

Eine Hilfsklasse, die die einfache Erstellung von benutzerdefinierten PropComparator-Klassen für die Verwendung mit Hybriden ermöglicht.

hybrid_method

Ein Dekorator, der die Definition einer Python-Objektmethode mit Instanz- und Klassenebene ermöglicht.

hybrid_property

Ein Dekorator, der die Definition eines Python-Deskriptors mit Instanz- und Klassenebene ermöglicht.

HybridExtensionType

Eine Aufzählung.

class sqlalchemy.ext.hybrid.hybrid_method

Ein Dekorator, der die Definition einer Python-Objektmethode mit Instanz- und Klassenebene ermöglicht.

method sqlalchemy.ext.hybrid.hybrid_method.__init__(func: Callable[[Concatenate[Any, _P]], _R], expr: Callable[[Concatenate[Any, _P]], SQLCoreOperations[_R]] | None = None)

Erstellt ein neues hybrid_method.

Die Verwendung erfolgt typischerweise über einen Dekorator.

from sqlalchemy.ext.hybrid import hybrid_method


class SomeClass:
    @hybrid_method
    def value(self, x, y):
        return self._value + x + y

    @value.expression
    @classmethod
    def value(cls, x, y):
        return func.some_function(cls._value, x, y)
methode sqlalchemy.ext.hybrid.hybrid_method.expression(expr: Callable[[Concatenate[Any, _P]], SQLCoreOperations[_R]]) hybrid_method[_P, _R]

Stellt einen modifizierenden Dekorator bereit, der eine SQL-Ausdruck erzeugende Methode definiert.

attribut sqlalchemy.ext.hybrid.hybrid_method.extension_type: InspectionAttrExtensionType = 'HYBRID_METHOD'

Der Erweiterungstyp, falls vorhanden. Standard ist NotExtension.NOT_EXTENSION

attribut sqlalchemy.ext.hybrid.hybrid_method.inplace

Gibt den In-Place-Mutator für diese hybrid_method zurück.

Die Klasse hybrid_method führt bereits eine "In-Place"-Mutation durch, wenn der Dekorator hybrid_method.expression() aufgerufen wird, daher gibt dieses Attribut Self zurück.

Neu seit Version 2.0.4.

attribut sqlalchemy.ext.hybrid.hybrid_method.is_attribute = True

True, wenn dieses Objekt ein Python Deskriptor ist.

Dies kann sich auf viele Typen beziehen. Normalerweise ein QueryableAttribute, der Attributereignisse im Auftrag einer MapperProperty behandelt. Kann aber auch ein Erweiterungstyp sein, wie AssociationProxy oder hybrid_property. Der InspectionAttr.extension_type bezieht sich auf eine Konstante, die den spezifischen Untertyp identifiziert.

Klasse sqlalchemy.ext.hybrid.hybrid_property

Ein Dekorator, der die Definition eines Python-Deskriptors mit Instanz- und Klassenebene ermöglicht.

Klassensignatur

Klasse sqlalchemy.ext.hybrid.hybrid_property (sqlalchemy.orm.base.InspectionAttrInfo, sqlalchemy.orm.base.ORMDescriptor)

methode sqlalchemy.ext.hybrid.hybrid_property.__init__(fget: _HybridGetterType[_T], fset: _HybridSetterType[_T] | None = None, fdel: _HybridDeleterType[_T] | None = None, expr: _HybridExprCallableType[_T] | None = None, custom_comparator: Comparator[_T] | None = None, update_expr: _HybridUpdaterType[_T] | None = None)

Erstellt eine neue hybrid_property.

Die Verwendung erfolgt typischerweise über einen Dekorator.

from sqlalchemy.ext.hybrid import hybrid_property


class SomeClass:
    @hybrid_property
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        self._value = value
methode sqlalchemy.ext.hybrid.hybrid_property.comparator(comparator: _HybridComparatorCallableType[_T]) hybrid_property[_T]

Stellt einen modifizierenden Dekorator bereit, der eine benutzerdefinierte Vergleichsmethoden-erzeugende Methode definiert.

Der Rückgabewert der dekorierten Methode sollte eine Instanz von Comparator sein.

Hinweis

Der Dekorator hybrid_property.comparator() **ersetzt** die Verwendung des Dekorators hybrid_property.expression(). Sie können nicht zusammen verwendet werden.

Wenn ein Hybrid auf Klassenebene aufgerufen wird, wird das hier angegebene Comparator-Objekt in ein spezialisiertes QueryableAttribute verpackt, das die gleiche Art von Objekt ist, die vom ORM zur Darstellung anderer gemappter Attribute verwendet wird. Der Grund dafür ist, dass andere klassenbezogene Attribute wie Docstrings und ein Verweis auf den Hybrid selbst innerhalb der zurückgegebenen Struktur beibehalten werden können, ohne das ursprüngliche übergebene Comparator-Objekt zu modifizieren.

Hinweis

Wenn auf eine Hybrid-Eigenschaft von einer besitzenden Klasse verwiesen wird (z. B. SomeClass.some_hybrid), wird eine Instanz von QueryableAttribute zurückgegeben, die den Ausdruck oder das Comparator-Objekt sowie dieses Hybrid-Objekt darstellt. Dieses Objekt selbst verfügt jedoch über Zugriffsfunktionen namens expression und comparator; wenn Sie versuchen, diese Dekoratoren in einer Unterklasse zu überschreiben, müssen Sie sie möglicherweise zuerst mit dem Modifikator hybrid_property.overrides qualifizieren. Weitere Details finden Sie unter diesem Modifikator.

methode sqlalchemy.ext.hybrid.hybrid_property.deleter(fdel: _HybridDeleterType[_T]) hybrid_property[_T]

Stellt einen modifizierenden Dekorator bereit, der eine Löschmethode definiert.

methode sqlalchemy.ext.hybrid.hybrid_property.expression(expr: _HybridExprCallableType[_T]) hybrid_property[_T]

Stellt einen modifizierenden Dekorator bereit, der eine SQL-Ausdruck erzeugende Methode definiert.

Wenn ein Hybrid auf Klassenebene aufgerufen wird, wird der hier angegebene SQL-Ausdruck in ein spezialisiertes QueryableAttribute verpackt, das die gleiche Art von Objekt ist, die vom ORM zur Darstellung anderer gemappter Attribute verwendet wird. Der Grund dafür ist, dass andere klassenbezogene Attribute wie Docstrings und ein Verweis auf den Hybrid selbst innerhalb der zurückgegebenen Struktur beibehalten werden können, ohne den ursprünglichen übergebenen SQL-Ausdruck zu modifizieren.

Hinweis

Wenn auf eine Hybrid-Eigenschaft von einer besitzenden Klasse verwiesen wird (z. B. SomeClass.some_hybrid), wird eine Instanz von QueryableAttribute zurückgegeben, die den Ausdruck oder das Comparator-Objekt sowie dieses Hybrid-Objekt darstellt. Dieses Objekt selbst verfügt jedoch über Zugriffsfunktionen namens expression und comparator; wenn Sie versuchen, diese Dekoratoren in einer Unterklasse zu überschreiben, müssen Sie sie möglicherweise zuerst mit dem Modifikator hybrid_property.overrides qualifizieren. Weitere Details finden Sie unter diesem Modifikator.

attribut sqlalchemy.ext.hybrid.hybrid_property.extension_type: InspectionAttrExtensionType = 'HYBRID_PROPERTY'

Der Erweiterungstyp, falls vorhanden. Standard ist NotExtension.NOT_EXTENSION

methode sqlalchemy.ext.hybrid.hybrid_property.getter(fget: _HybridGetterType[_T]) hybrid_property[_T]

Stellt einen modifizierenden Dekorator bereit, der eine Getter-Methode definiert.

Neu seit Version 1.2.

attribut sqlalchemy.ext.hybrid.hybrid_property.inplace

Gibt den In-Place-Mutator für diese hybrid_property zurück.

Dies ermöglicht die In-Place-Mutation des Hybrids, wodurch die erste Hybrid-Methode eines bestimmten Namens wiederverwendet werden kann, um weitere Methoden hinzuzufügen, ohne diese Methoden gleich benennen zu müssen, z. B.

class Interval(Base):
    # ...

    @hybrid_property
    def radius(self) -> float:
        return abs(self.length) / 2

    @radius.inplace.setter
    def _radius_setter(self, value: float) -> None:
        self.length = value * 2

    @radius.inplace.expression
    def _radius_expression(cls) -> ColumnElement[float]:
        return type_coerce(func.abs(cls.length) / 2, Float)

Neu seit Version 2.0.4.

attribut sqlalchemy.ext.hybrid.hybrid_property.is_attribute = True

True, wenn dieses Objekt ein Python Deskriptor ist.

Dies kann sich auf viele Typen beziehen. Normalerweise ein QueryableAttribute, der Attributereignisse im Auftrag einer MapperProperty behandelt. Kann aber auch ein Erweiterungstyp sein, wie AssociationProxy oder hybrid_property. Der InspectionAttr.extension_type bezieht sich auf eine Konstante, die den spezifischen Untertyp identifiziert.

attribut sqlalchemy.ext.hybrid.hybrid_property.overrides

Präfix für eine Methode, die ein vorhandenes Attribut überschreibt.

Der Zugriff hybrid_property.overrides gibt einfach dieses Hybrid-Objekt zurück, das, wenn es auf Klassenebene von einer Elternklasse aufgerufen wird, das "instrumentierte Attribut", das normalerweise auf dieser Ebene zurückgegeben wird, de-referenziert und es den modifizierenden Dekoratoren wie hybrid_property.expression() und hybrid_property.comparator() ermöglicht, ohne Konflikte mit den gleichnamigen Attributen zu haben, die normalerweise auf dem QueryableAttribute vorhanden sind.

class SuperClass:
    # ...

    @hybrid_property
    def foobar(self):
        return self._foobar


class SubClass(SuperClass):
    # ...

    @SuperClass.foobar.overrides.expression
    def foobar(cls):
        return func.subfoobar(self._foobar)

Neu seit Version 1.2.

methode sqlalchemy.ext.hybrid.hybrid_property.setter(fset: _HybridSetterType[_T]) hybrid_property[_T]

Stellt einen modifizierenden Dekorator bereit, der eine Setter-Methode definiert.

methode sqlalchemy.ext.hybrid.hybrid_property.update_expression(meth: _HybridUpdaterType[_T]) hybrid_property[_T]

Stellt einen modifizierenden Dekorator bereit, der eine UPDATE-Tupel erzeugende Methode definiert.

Die Methode akzeptiert einen einzelnen Wert, der der Wert ist, der in die SET-Klausel einer UPDATE-Anweisung gerendert werden soll. Die Methode sollte diesen Wert dann in einzelne Spaltenausdrücke verarbeiten, die in die endgültige SET-Klausel passen, und sie als Sequenz von 2-Tupeln zurückgeben. Jedes Tupel enthält einen Spaltenausdruck als Schlüssel und einen Wert zum Rendern.

Z. B.

class Person(Base):
    # ...

    first_name = Column(String)
    last_name = Column(String)

    @hybrid_property
    def fullname(self):
        return first_name + " " + last_name

    @fullname.update_expression
    def fullname(cls, value):
        fname, lname = value.split(" ", 1)
        return [(cls.first_name, fname), (cls.last_name, lname)]

Neu seit Version 1.2.

Klasse sqlalchemy.ext.hybrid.Comparator

Eine Hilfsklasse, die die einfache Erstellung von benutzerdefinierten PropComparator-Klassen für die Verwendung mit Hybriden ermöglicht.

Klasse sqlalchemy.ext.hybrid.HybridExtensionType

Eine Aufzählung.

attribut sqlalchemy.ext.hybrid.HybridExtensionType.HYBRID_METHOD = 'HYBRID_METHOD'

Symbol, das eine InspectionAttr vom Typ hybrid_method anzeigt.

Wird dem Attribut InspectionAttr.extension_type zugewiesen.

Siehe auch

Mapper.all_orm_attributes

attribut sqlalchemy.ext.hybrid.HybridExtensionType.HYBRID_PROPERTY = 'HYBRID_PROPERTY'
Symbol, das eine InspectionAttr anzeigt, die

vom Typ hybrid_method ist.

Wird dem Attribut InspectionAttr.extension_type zugewiesen.

Siehe auch

Mapper.all_orm_attributes