Атрибуты гибрида

Определите атрибуты для ORM-сопоставленных классов, которые имеют «гибридное» поведение.

«Гибридный» означает, что атрибут имеет различное поведение, определенное на уровне класса и на уровне экземпляра.

Расширение hybrid предоставляет специальную форму декоратора методов и имеет минимальные зависимости от остальной части SQLAlchemy. Его базовая теория работы может работать с любой системой выражений, основанной на дескрипторах.

Рассмотрим отображение Interval, представляющее целочисленные значения start и end. Мы можем определить функции более высокого уровня на отображенных классах, которые производят SQL-выражения на уровне класса и оценку выражений Python на уровне экземпляра. Ниже каждая функция, украшенная символами hybrid_method или hybrid_property, может получать self как экземпляр класса, а может получать класс напрямую, в зависимости от контекста:

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)

Выше свойство length возвращает разность между атрибутами end и start. С экземпляром Interval это вычитание происходит на языке Python, используя обычную механику дескрипторов Python:

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

При работе с самим классом Interval дескриптор hybrid_property оценивает тело функции, задавая в качестве аргумента класс Interval, который при оценке с помощью механики выражений SQLAlchemy возвращает новое SQL-выражение:

>>> from sqlalchemy import select
>>> print(select(Interval.length))
{printsql}SELECT interval."end" - interval.start AS length
FROM interval{stop}


>>> print(select(Interval).filter(Interval.length > 10))
{printsql}SELECT interval.id, interval.start, interval."end"
FROM interval
WHERE interval."end" - interval.start > :param_1

Такие методы фильтрации, как Select.filter_by(), поддерживаются и с гибридными атрибутами:

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

В примере класса Interval также показаны два метода, contains() и intersects(), декорированные с помощью hybrid_method. Этот декоратор применяет к методам ту же идею, что и hybrid_property к атрибутам. Методы возвращают булевы значения и используют преимущества побитовых операторов Python | и & для получения эквивалентного поведения булевых значений на уровне экземпляра и на уровне выражения SQL:

>>> 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)))
{printsql}SELECT interval.id, interval.start, interval."end"
FROM interval
WHERE interval.start <= :start_1 AND interval."end" > :end_1{stop}

>>> ia = aliased(Interval)
>>> print(select(Interval, ia).filter(Interval.intersects(ia)))
{printsql}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"{stop}

Определение поведения выражения, отличного от поведения атрибута

В предыдущем разделе мы удачно использовали побитовые операторы & и | в методах Interval.contains и Interval.intersects, поскольку наши функции оперировали двумя булевыми значениями, возвращая новое. Во многих случаях построение функции на языке Python и SQL-выражения SQLAlchemy имеет достаточно различий, чтобы определить два отдельных Python-выражения. Для этого в декораторе hybrid определяется модификатор hybrid_property.expression(). В качестве примера определим радиус интервала, для чего необходимо использовать функцию абсолютного значения:

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)

В приведенном примере метод hybrid_property, впервые присвоенный имени Interval.radius, изменяется последующим методом Interval._radius_expression с использованием декоратора @radius.inplace.expression, который объединяет два модификатора hybrid_property.inplace и hybrid_property.expression. Использование hybrid_property.inplace указывает на то, что модификатор hybrid_property.expression() должен мутировать существующий гибридный объект по адресу Interval.radius на месте, не создавая нового объекта. Замечания по этому модификатору и его обоснование рассматриваются в следующем разделе Использование inplace для создания гибридных свойств, совместимых с pep-484. Использование @classmethod является необязательным и служит исключительно для того, чтобы дать средствам типизации подсказку, что cls в данном случае должен быть классом Interval, а не экземпляром Interval.

Примечание

hybrid_property.inplace, а также использование @classmethod для правильной поддержки типизации доступны начиная с версии SQLAlchemy 2.0.4 и не будут работать в более ранних версиях.

Поскольку Interval.radius теперь включает элемент выражения, при обращении к Interval.radius на уровне класса возвращается SQL-функция ABS():

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

Использование inplace для создания гибридных свойств, совместимых с pep-484

В предыдущем разделе показан декоратор hybrid_property, который включает в себя две отдельные декорируемые функции уровня метода, обе из которых создают один атрибут объекта, обозначаемый как Interval.radius. На самом деле существует несколько различных модификаторов, которые мы можем использовать для hybrid_property, включая hybrid_property.expression(), hybrid_property.setter() и hybrid_property.update_expression().

Декоратор SQLAlchemy hybrid_property предполагает, что добавление этих методов может осуществляться так же, как и встроенный в Python декоратор @property, где идиоматическое использование заключается в том, чтобы продолжать переопределять атрибут многократно, используя каждый раз одно и то же имя атрибута, как в примере ниже, иллюстрирующем использование hybrid_property.setter() и hybrid_property.expression() для дескриптора Interval.radius:

# 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)

Выше приведены три метода Interval.radius, но поскольку каждый из них украшен сначала декоратором hybrid_property, а затем самим именем @radius, то в итоге получается, что Interval.radius - это один атрибут с тремя различными функциями, содержащимися в нем. Этот стиль использования заимствован из Python’s documented use of @property. Важно отметить, что при работе как @property, так и hybrid_property каждый раз создается копия дескриптора. То есть при каждом вызове @radius.expression, @radius.setter и т.д. создается полностью новый объект. Это позволяет без проблем переопределять атрибут в подклассах (о том, как это используется, см. раздел Повторное использование гибридных свойств в подклассах далее в этой главе).

Однако указанный подход несовместим с такими средствами типизации, как mypy и pyright. Собственный декоратор Python @property не имеет этого ограничения только потому, что these tools hardcode the behavior of @property, то есть этот синтаксис недоступен для SQLAlchemy при соответствии PEP 484.

Для того чтобы обеспечить приемлемый синтаксис и при этом не нарушить требования к набору текста, декоратор hybrid_property.inplace позволяет повторно использовать один и тот же декоратор с разными именами методов, но при этом создавать один декоратор под одним именем:

# 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)

Использование hybrid_property.inplace дополнительно квалифицирует использование декоратора, что новая копия не должна создаваться, тем самым сохраняя имя Interval.radius и позволяя дополнительным методам Interval._radius_setter и Interval._radius_expression иметь другое имя.

Добавлено в версии 2.0.4: Добавлена функция hybrid_property.inplace, позволяющая менее многословно строить составные объекты hybrid_property и не использовать повторяющиеся имена методов. Дополнительно разрешено использовать @classmethod внутри hybrid_property.expression, hybrid_property.update_expression и hybrid_property.comparator для того, чтобы средства набора текста могли идентифицировать cls как класс, а не экземпляр в сигнатуре метода.

Определение задатчиков

Модификатор hybrid_property.setter() позволяет построить пользовательский метод-сеттер, который может изменять значения на объекте:

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(self, value) вызывается по команде set:

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

Разрешение массового обновления ORM

Гибрид может определить пользовательский обработчик «UPDATE» для использования обновлений с поддержкой ORM, что позволяет использовать гибрид в предложении SET обновления.

Обычно при использовании гибрида с update() в качестве столбца, являющегося целью SET, используется SQL-выражение. Если бы в нашем классе Interval был гибрид start_point, который ссылался бы на Interval.start, то его можно было бы подставить непосредственно:

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

Однако при использовании составного гибрида, например Interval.length, этот гибрид представляет собой более одного столбца. С помощью декоратора hybrid_property.update_expression() мы можем создать обработчик, который будет принимать значение, переданное в выражении VALUES, которое может повлиять на это. Обработчик, работающий аналогично нашему сеттеру, будет выглядеть так:

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)
        ]

Выше, если мы используем Interval.length в выражении UPDATE, то получаем гибридное выражение SET:

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

Это выражение SET подстраивается под ORM автоматически.

См.также

Операции INSERT, UPDATE и DELETE с поддержкой ORM - включает справочную информацию по операторам UPDATE с поддержкой ORM

Работа с отношениями

При создании гибридов, работающих со связанными объектами, существенной разницы нет, в отличие от данных, основанных на столбцах. Потребность в отдельных выражениях, как правило, выше. Мы рассмотрим два варианта: гибрид «зависимый от соединения» и гибрид «коррелированный подзапрос».

Гибрид отношений «присоединение-зависимость

Рассмотрим следующее декларативное отображение, которое связывает User с SavingsAccount:

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)

Приведенное выше гибридное свойство balance работает с первой записью SavingsAccount в списке учетных записей для данного пользователя. Методы in-Python getter/setter могут рассматривать accounts как список Python, доступный на self.

Совет

Геттер User.balance в приведенном примере обращается к коллекции self.acccounts, которая обычно загружается с помощью стратегии загрузчика selectinload(), настроенной на User.balance relationship(). Если на relationship() не указано иное, то по умолчанию используется стратегия загрузчика lazyload(), которая выдает SQL по требованию. При использовании asyncio загрузчики по требованию, такие как lazyload(), не поддерживаются, поэтому при использовании asyncio следует позаботиться о том, чтобы коллекция self.accounts была доступна для этого гибридного аксессора.

На уровне выражений ожидается, что класс User будет использоваться в соответствующем контексте так, что будет присутствовать соответствующее присоединение к SavingsAccount:

>>> from sqlalchemy import select
>>> print(select(User, User.balance).
...       join(User.accounts).filter(User.balance > 5000))
{printsql}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

Заметим, однако, что в то время как аксессоры на уровне экземпляра должны быть озабочены тем, присутствует ли вообще self.accounts, на уровне SQL-выражений эта проблема выражается иначе, где мы в основном используем внешнее соединение:

>>> from sqlalchemy import select
>>> from sqlalchemy import or_
>>> print (select(User, User.balance).outerjoin(User.accounts).
...         filter(or_(User.balance < 5000, User.balance == None)))
{printsql}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

Гибрид взаимосвязанных подзапросов

Конечно, мы можем отказаться от зависимости от использования объединений во вложенном запросе в пользу коррелированного подзапроса, который может быть упакован в выражение с одним столбцом. Коррелированный подзапрос является более переносимым, но часто имеет более низкую производительность на уровне SQL. Используя тот же прием, что и в примере Использование свойства_столбца, мы можем изменить наш пример SavingsAccount, чтобы агрегировать остатки по всем счетам, и использовать коррелированный подзапрос для выражения столбца:

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")
        )

Приведенный выше рецепт даст нам столбец balance, который выдает коррелированный SELECT:

>>> from sqlalchemy import select
>>> print(select(User).filter(User.balance > 400))
{printsql}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

Построение пользовательских компараторов

Свойство hybrid также включает в себя помощник, позволяющий создавать пользовательские компараторы. Объект компаратора позволяет настраивать поведение каждого оператора выражения SQLAlchemy в отдельности. Они полезны при создании пользовательских типов, которые имеют весьма идиосинкразическое поведение на стороне SQL.

Примечание

Декоратор hybrid_property.comparator(), представленный в этом разделе, заменяет использование декоратора hybrid_property.expression(). Их совместное использование невозможно.

Приведенный ниже пример класса позволяет проводить сравнение без учета регистра на атрибуте с именем 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)

Выше SQL-выражения относительно word_insensitive будут применять SQL-функцию LOWER() к обеим сторонам:

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

Приведенный выше CaseInsensitiveComparator реализует часть интерфейса ColumnOperators. Операция «принуждения», такая как выделение нижнего регистра, может быть применена ко всем операциям сравнения (т.е. eq, lt, gt и т.д.) с помощью Operators.operate():

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

Повторное использование гибридных свойств в подклассах

На гибрид можно ссылаться из суперкласса, что позволяет использовать модифицирующие методы типа hybrid_property.getter(), hybrid_property.setter() для переопределения этих методов в подклассе. Это похоже на то, как работает стандартный объект Python @property:

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)

Выше класс FirstNameLastName обращается к гибриду из FirstNameOnly.name, чтобы перепрофилировать его геттер и сеттер для подкласса.

При переопределении hybrid_property.expression() и hybrid_property.comparator() только в качестве первой ссылки на суперкласс эти имена конфликтуют с одноименными аксессорами на объекте QueryableAttribute, возвращаемом на уровне класса. Чтобы переопределить эти методы при обращении непосредственно к дескриптору родительского класса, добавьте специальный квалификатор hybrid_property.overrides, который де-ссылается на инструментальный атрибут обратно на гибридный объект:

class FirstNameLastName(FirstNameOnly):
    # ...

    last_name: Mapped[str]

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

Гибридные объекты стоимости

Обратите внимание, что в нашем предыдущем примере, если бы мы сравнивали атрибут word_insensitive экземпляра SearchWord с обычной строкой Python, то обычная строка Python не была бы приведена к нижнему регистру - построенная нами CaseInsensitiveComparator, возвращаемая @word_insensitive.comparator, относится только к стороне SQL.

Более полной формой пользовательского компаратора является построение гибридного объекта значения. В этом случае целевое значение или выражение применяется к объекту значения, который затем возвращается аксессором во всех случаях. Объект значения позволяет контролировать все операции над значением, а также то, как обрабатываются сравниваемые значения, как со стороны SQL-выражения, так и со стороны Python-значения. Замена предыдущего класса CaseInsensitiveComparator новым классом CaseInsensitiveWord:

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"

Выше объект CaseInsensitiveWord представляет self.word, который может быть SQL-функцией, а может быть и Python-функцией. Переопределив operate() и __clause_element__() для работы в терминах self.word, все операции сравнения будут работать с «преобразованной» формой word, будь то на стороне SQL или на стороне Python. Теперь наш класс SearchWord может безоговорочно предоставлять объект CaseInsensitiveWord из одного гибридного вызова:

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)

Атрибут word_insensitive теперь повсеместно имеет поведение сравнения без учета регистра, включая сравнение выражений SQL с выражениями Python (обратите внимание, что значение Python преобразуется в нижний регистр на стороне Python):

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

SQL-выражение против SQL-выражения:

>>> 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
...     )
... )
{printsql}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)

Только в языке Python выражение:

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

Шаблон Hybrid Value очень полезен для любых значений, которые могут иметь несколько представлений, таких как временные метки, временные дельты, единицы измерения, валюты и зашифрованные пароли.

См.также

Hybrids and Value Agnostic Types - в блоге techspot.zzzeek.org

Value Agnostic Types, Part II - в блоге techspot.zzzeek.org

Справочник по API

Object Name Description

Comparator

Вспомогательный класс, позволяющий легко создавать собственные классы PropComparator для использования в гибридах.

hybrid_method

Декоратор, позволяющий определить метод объекта Python с поведением как на уровне экземпляра, так и на уровне класса.

hybrid_property

Декоратор, позволяющий определить дескриптор Python с поведением как на уровне экземпляров, так и на уровне классов.

HybridExtensionType

Перечисление.

class sqlalchemy.ext.hybrid.hybrid_method

Декоратор, позволяющий определить метод объекта Python с поведением как на уровне экземпляра, так и на уровне класса.

Классическая подпись.

класс sqlalchemy.ext.hybrid.hybrid_method (sqlalchemy.orm.base.InspectionAttrInfo, typing.Generic)

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

Создайте новый hybrid_method.

Использование обычно осуществляется через декоратор:

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)
method sqlalchemy.ext.hybrid.hybrid_method.expression(expr: Callable[[Concatenate[Any, _P]], SQLCoreOperations[_R]]) hybrid_method[_P, _R]

Предоставьте модифицирующий декоратор, определяющий метод генерации SQL-выражений.

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

Тип расширения, если таковое имеется. По умолчанию NotExtension.NOT_EXTENSION

attribute sqlalchemy.ext.hybrid.hybrid_method.inplace

Возвращает inplace-мутатор для данного hybrid_method.

Класс hybrid_method уже выполняет мутацию «на месте» при вызове декоратора hybrid_method.expression(), поэтому этот атрибут возвращает значение Self.

Добавлено в версии 2.0.4.

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

True, если данный объект является Python descriptor.

Он может относиться к одному из многих типов. Обычно это QueryableAttribute, который обрабатывает события атрибутов от имени MapperProperty. Но может быть и типом расширения, таким как AssociationProxy или hybrid_property. При этом InspectionAttr.extension_type будет ссылаться на константу, идентифицирующую конкретный подтип.

См.также

Mapper.all_orm_descriptors

class sqlalchemy.ext.hybrid.hybrid_property

Декоратор, позволяющий определить дескриптор Python с поведением как на уровне экземпляров, так и на уровне классов.

Классическая подпись.

класс sqlalchemy.ext.hybrid.hybrid_property (sqlalchemy.orm.base.InspectionAttrInfo, sqlalchemy.orm.base.ORMDescriptor)

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

Создайте новый hybrid_property.

Использование обычно осуществляется через декоратор:

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
method sqlalchemy.ext.hybrid.hybrid_property.comparator(comparator: _HybridComparatorCallableType[_T]) hybrid_property[_T]

Предоставьте модифицирующий декоратор, определяющий пользовательский метод создания компаратора.

Возвращаемое значение декорированного метода должно представлять собой экземпляр Comparator.

Примечание

Декоратор hybrid_property.comparator() заменяет использование декоратора hybrid_property.expression(). Их совместное использование невозможно.

При вызове гибрида на уровне класса объект Comparator, переданный здесь, заворачивается внутрь специализированного QueryableAttribute, который является объектом того же типа, который используется ORM для представления других отображаемых атрибутов. Это делается для того, чтобы в возвращаемой структуре можно было хранить другие атрибуты уровня класса, такие как документальные строки и ссылку на сам гибрид, без каких-либо изменений в исходном объекте компаратора.

Примечание

При обращении к гибридному свойству из класса-владельца (например, SomeClass.some_hybrid) возвращается экземпляр QueryableAttribute, представляющий объект выражения или компаратора в виде этого гибридного объекта. Однако сам этот объект имеет аксессоры expression и comparator, поэтому при попытке переопределить эти декораторы в подклассе может потребоваться сначала квалифицировать его с помощью модификатора hybrid_property.overrides. Подробнее об этом модификаторе см.

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

Предоставьте модифицирующий декоратор, определяющий метод удаления.

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

Предоставьте модифицирующий декоратор, определяющий метод генерации SQL-выражений.

Когда гибрид вызывается на уровне класса, приведенное здесь SQL-выражение оборачивается внутрь специализированного QueryableAttribute, который представляет собой объект того же типа, который используется ORM для представления других отображаемых атрибутов. Это делается для того, чтобы в возвращаемой структуре можно было сохранить другие атрибуты уровня класса, такие как документальные строки и ссылку на сам гибрид, без каких-либо изменений в исходном SQL-выражении.

Примечание

При обращении к гибридному свойству из класса-владельца (например, SomeClass.some_hybrid) возвращается экземпляр QueryableAttribute, представляющий объект выражения или компаратора, а также данный гибридный объект. Однако сам этот объект имеет аксессоры expression и comparator, поэтому при попытке переопределить эти декораторы в подклассе может потребоваться сначала квалифицировать его с помощью модификатора hybrid_property.overrides. Подробнее об этом модификаторе см.

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

Тип расширения, если таковое имеется. По умолчанию NotExtension.NOT_EXTENSION

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

Предоставьте модифицирующий декоратор, определяющий метод getter.

Добавлено в версии 1.2.

attribute sqlalchemy.ext.hybrid.hybrid_property.inplace

Возвращает inplace-мутатор для данного hybrid_property.

Это сделано для того, чтобы обеспечить возможность мутации гибрида на месте, позволяя повторно использовать первый гибридный метод с определенным именем для добавления других методов без необходимости называть эти методы одинаково, например:

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)

Добавлено в версии 2.0.4.

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

True, если данный объект является Python descriptor.

Он может относиться к одному из многих типов. Обычно это QueryableAttribute, который обрабатывает события атрибутов от имени MapperProperty. Но может быть и типом расширения, таким как AssociationProxy или hybrid_property. При этом InspectionAttr.extension_type будет ссылаться на константу, идентифицирующую конкретный подтип.

См.также

Mapper.all_orm_descriptors

attribute sqlalchemy.ext.hybrid.hybrid_property.overrides

Префикс для метода, переопределяющего существующий атрибут.

Аксессор hybrid_property.overrides просто возвращает этот гибридный объект, который при вызове на уровне класса из родительского класса отменяет ссылку на «инструментальный атрибут», обычно возвращаемый на этом уровне, и позволяет использовать модифицирующие декораторы типа hybrid_property.expression() и hybrid_property.comparator() без конфликта с одноименными атрибутами, обычно присутствующими на QueryableAttribute:

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)

Добавлено в версии 1.2.

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

Предоставьте модифицирующий декоратор, определяющий метод setter.

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

Предоставьте модифицирующий декоратор, определяющий метод создания кортежа UPDATE.

Метод принимает одно значение, которое является значением, выводимым в предложение SET оператора UPDATE. Затем метод должен обработать это значение в отдельные столбцовые выражения, которые вписываются в конечный пункт SET, и вернуть их в виде последовательности из двух кортежей. Каждый кортеж содержит выражение столбца в качестве ключа и значение, которое должно быть выведено на экран.

Например:

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)
        ]

Добавлено в версии 1.2.

class sqlalchemy.ext.hybrid.Comparator

Вспомогательный класс, позволяющий легко создавать собственные классы PropComparator для использования в гибридах.

Классическая подпись.

класс sqlalchemy.ext.hybrid.Comparator (sqlalchemy.orm.PropComparator)

class sqlalchemy.ext.hybrid.HybridExtensionType

Перечисление.

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

Символ, указывающий на InspectionAttr, имеющий тип hybrid_method.

Присваивается атрибуту InspectionAttr.extension_type.

См.также

Mapper.all_orm_attributes

attribute sqlalchemy.ext.hybrid.HybridExtensionType.HYBRID_PROPERTY = 'HYBRID_PROPERTY'
Символ, указывающий на InspectionAttr, который

типа hybrid_method.

Присваивается атрибуту InspectionAttr.extension_type.

См.также

Mapper.all_orm_attributes

Вернуться на верх