Изменение поведения атрибутов

В этом разделе будут рассмотрены возможности и техники, используемые для изменения поведения отображаемых атрибутов ORM, включая те, которые отображаются с помощью mapped_column(), relationship() и других.

Простые валидаторы

Быстрый способ добавить процедуру «валидации» к атрибуту - использовать декоратор validates(). Валидатор атрибута может вызвать исключение, остановив процесс изменения значения атрибута, или изменить данное значение на другое. Валидаторы, как и все расширения атрибутов, вызываются только обычным пользовательским кодом; они не выдаются, когда ORM заполняет объект:

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

Валидаторы также получают события добавления коллекции, когда элементы добавляются в коллекцию:

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

Функция проверки по умолчанию не выдается для событий удаления коллекции, поскольку обычно ожидается, что отбрасываемое значение не требует проверки. Однако validates() поддерживает прием этих событий, указывая include_removes=True в декораторе. Когда этот флаг установлен, функция проверки должна получить дополнительный булев аргумент, который, если True, указывает, что операция является удалением:

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

Случай, когда взаимозависимые валидаторы связаны обратной ссылкой, также может быть адаптирован с помощью опции include_backrefs=False; эта опция, когда она установлена в False, предотвращает выброс функции валидации, если событие происходит в результате обратной ссылки:

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

Выше, если бы мы присвоили Address.user, как в some_address.user = some_user, функция validate_address() не была бы испущена, даже если бы произошло добавление к some_user.addresses - событие вызвано обратной ссылкой.

Обратите внимание, что декоратор validates() является удобной функцией, построенной поверх событий атрибутов. Приложение, которому требуется больший контроль над конфигурацией поведения при изменении атрибутов, может воспользоваться этой системой, описанной в AttributeEvents.

Object Name Description

validates(*names, [include_removes, include_backrefs])

Украсьте метод как «валидатор» для одного или нескольких именованных свойств.

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

Украсьте метод как «валидатор» для одного или нескольких именованных свойств.

Назначает метод как валидатор, метод, который получает имя атрибута, а также значение, которое должно быть присвоено, или, в случае коллекции, значение, которое должно быть добавлено в коллекцию. Затем функция может вызвать исключения валидации, чтобы остановить процесс от продолжения (где встроенные в Python исключения ValueError и AssertionError являются разумным выбором), или может изменить или заменить значение перед продолжением. В противном случае функция должна вернуть заданное значение.

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

Параметры:
  • *names – список имен атрибутов, подлежащих проверке.

  • include_removes – если True, будут также отправляться события «удаления» - функция проверки должна принимать дополнительный аргумент «is_remove», который будет булевым значением.

  • include_backrefs – по умолчанию True; если False, функция валидации не будет испускаться, если инициатором является событие атрибута, связанное через обратную ссылку. Это может использоваться для двунаправленного использования validates(), когда только один валидатор должен испускаться на каждую операцию с атрибутом. … versionchanged:: 2.0.16 Этот параметр по ошибке имел значение по умолчанию False `` для релизов с 2.0.0 по 2.0.15.  Его правильное значение по умолчанию ``True восстановлено в версии 2.0.16.

См.также

Простые валидаторы - примеры использования для validates()

Использование пользовательских типов данных на уровне ядра

Нестандартное средство воздействия на значение столбца таким образом, чтобы преобразовать данные между тем, как они представлены в Python, и тем, как они представлены в базе данных, может быть достигнуто путем использования пользовательского типа данных, который применяется к сопоставленным метаданным Table. Это более распространено в случае некоторого стиля кодирования / декодирования, которое происходит как при передаче данных в базу данных, так и при их возврате; подробнее об этом можно прочитать в документации Core по адресу Дополнение существующих типов.

Использование дескрипторов и гибридов

Более полный способ создать модифицированное поведение для атрибута - использовать descriptors. В Python они обычно используются с помощью функции property(). Стандартная техника SQLAlchemy для дескрипторов заключается в создании обычного дескриптора и его чтении/записи из сопоставленного атрибута с другим именем. Ниже мы проиллюстрируем это с помощью свойств в стиле 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

Приведенный выше подход будет работать, но мы можем добавить еще кое-что. Хотя наш объект EmailAddress будет передавать значение через дескриптор email и в сопоставленный атрибут _email, атрибут уровня класса EmailAddress.email не имеет обычной семантики выражения, используемой в Select. Чтобы обеспечить их, мы используем расширение hybrid следующим образом:

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

Атрибут .email, помимо обеспечения поведения getter/setter, когда у нас есть экземпляр EmailAddress, также обеспечивает SQL-выражение при использовании на уровне класса, то есть непосредственно из класса EmailAddress:

from sqlalchemy.orm import Session
from sqlalchemy import select

session = Session()

address = session.scalars(
    select(EmailAddress).where(EmailAddress.email == "address@example.com")
).one()
{execsql}SELECT address.email AS address_email, address.id AS address_id
FROM address
WHERE address.email = ?
('address@example.com',)
{stop}

address.email = "otheraddress@example.com"
session.commit()
{execsql}UPDATE address SET email=? WHERE address.id = ?
('otheraddress@example.com', 1)
COMMIT
{stop}

Модификатор hybrid_property также позволяет нам изменить поведение атрибута, включая определение раздельного поведения при обращении к атрибуту на уровне экземпляра и на уровне класса/выражения, используя модификатор hybrid_property.expression(). Например, если мы хотим автоматически добавлять имя хоста, мы можем определить два набора логики манипулирования строками:

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)

Выше, обращение к свойству email экземпляра EmailAddress вернет значение атрибута _email, удалив или добавив имя хоста @example.com из значения. Когда мы делаем запрос к атрибуту email, отображается SQL-функция, которая производит тот же эффект:

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

Подробнее о гибридах читайте на Атрибуты гибрида.

Синонимы

Синонимы - это конструкция на уровне отображения, которая позволяет любому атрибуту класса «отражать» другой атрибут, который отображается.

В самом основном смысле синоним - это простой способ сделать определенный атрибут доступным по дополнительному имени:

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

Приведенный выше класс MyClass имеет два атрибута, .job_status и .status, которые будут вести себя как один атрибут, оба на уровне выражения:

>>> print(MyClass.job_status == "some_status")
{printsql}my_table.job_status = :job_status_1{stop}

>>> print(MyClass.status == "some_status")
{printsql}my_table.job_status = :job_status_1{stop}

и на уровне экземпляра:

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

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

synonym() может использоваться для любого вида сопоставленных атрибутов, которые подклассы MapperProperty, включая сопоставленные колонки и отношения, а также сами синонимы.

Помимо простого зеркала, synonym() можно также сделать так, чтобы он ссылался на определенный пользователем descriptor. Мы можем снабдить наш синоним status синонимом @property:

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)

При использовании Declarative вышеприведенный шаблон может быть выражен более лаконично с помощью декоратора synonym_for():

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

Хотя synonym() полезен для простого зеркалирования, в современном использовании для дополнения поведения атрибутов дескрипторами лучше использовать функцию hybrid attribute, которая больше ориентирована на дескрипторы Python. Технически, synonym() может делать все то же, что и hybrid_property, поскольку он также поддерживает внедрение пользовательских возможностей SQL, но гибрид более прост в использовании в более сложных ситуациях.

Object Name Description

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

Обозначьте имя атрибута как синоним сопоставленного свойства, в том смысле, что атрибут будет отражать значение и поведение выражения другого атрибута.

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

Обозначьте имя атрибута как синоним сопоставленного свойства, в том смысле, что атрибут будет отражать значение и поведение выражения другого атрибута.

например:

class MyClass(Base):
    __tablename__ = 'my_table'

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

    status = synonym("job_status")
Параметры:
  • name – имя существующего сопоставленного свойства. Это может относиться к строковому имени ORM-сопоставленного атрибута, настроенного на класс, включая связанные с колонками атрибуты и отношения.

  • descriptor – Python descriptor, который будет использоваться в качестве getter (и потенциально setter) при обращении к этому атрибуту на уровне экземпляра.

  • map_column – ** Только для классических отображений и отображений на существующий объект Table**. Если True, конструкция synonym() найдет объект Column на отображенной таблице, который обычно ассоциируется с именем атрибута этого синонима, и создаст новый ColumnProperty, который вместо этого отобразит этот Column на альтернативное имя, указанное в качестве аргумента «имя» синонима; таким образом, обычный шаг переопределения отображения Column на другое имя становится ненужным. Обычно это используется, когда Column нужно заменить атрибутом, который также использует дескриптор, то есть в сочетании с параметром 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) } ) Выше, атрибут с именем _job_status автоматически отображается на колонку job_status:: >>> j1 = MyClass() >>> j1._job_status = «employed» >>> j1.job_status Статус: employed При использовании Declarative, чтобы предоставить дескриптор в сочетании с синонимом, используйте помощник sqlalchemy.ext.declarative.synonym_for(). Однако обратите внимание, что обычно предпочтение отдается функции hybrid properties, особенно при переопределении

  • info – Необязательный словарь данных, который будет заполнен в атрибут InspectionAttr.info этого объекта.

  • comparator_factory – Подкласс PropComparator, обеспечивающий пользовательское поведение сравнения на уровне SQL-выражения. … примечание:: Для случая использования атрибута, который переопределяет поведение атрибута как на уровне Python, так и на уровне SQL-выражений, пожалуйста, обратитесь к атрибуту Hybrid, представленному в Использование дескрипторов и гибридов для более эффективной техники.

См.также

Синонимы - Обзор синонимов

synonym_for() - помощник, ориентированный на декларативность

Использование дескрипторов и гибридов - Расширение Hybrid Attribute обеспечивает обновленный подход к расширению поведения атрибутов более гибко, чем это может быть достигнуто с помощью синонимов.

Настройка оператора

Операторы», используемые SQLAlchemy ORM и языком выражений Core, являются полностью настраиваемыми. Например, выражение сравнения User.name == 'ed' использует встроенный в Python оператор operator.eq - фактическая конструкция SQL, которую SQLAlchemy ассоциирует с таким оператором, может быть изменена. Новые операции также могут быть связаны с выражениями столбцов. Операторы, которые имеют место для выражений столбцов, наиболее непосредственно переопределяются на уровне типов - описание см. в разделе Переопределение и создание новых операторов.

Функции уровня ORM, такие как column_property(), relationship() и composite(), также обеспечивают переопределение оператора на уровне ORM, передавая подкласс PropComparator в аргумент comparator_factory каждой функции. Настройка операторов на этом уровне является редким случаем использования. Обзор см. в документации по адресу PropComparator.

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