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

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

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

from sqlalchemy.orm import validates


class EmailAddress(Base):
    __tablename__ = "address"

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

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

Изменено в версии 1.0.0: - validators are no longer triggered within the flush process when the newly fetched values for primary key columns as well as some python- or server-side defaults are fetched. Prior to 1.0, validators may be triggered in those cases as well.

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

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.

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

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

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

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

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = Column(Integer, primary_key=True)

    # name the attribute with an underscore,
    # different from the column name
    _email = 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 не имеет обычной семантики выражения, используемой в Query. Чтобы обеспечить их, мы используем расширение hybrid следующим образом:

from sqlalchemy.ext.hybrid import hybrid_property


class EmailAddress(Base):
    __tablename__ = "email_address"

    id = Column(Integer, primary_key=True)

    _email = 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

session = Session()

sqladdress = session.query(EmailAddress).\
                 filter(EmailAddress.email == 'address@example.com').\
                 one()

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

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = Column(Integer, primary_key=True)

    _email = 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-функция, которая производит тот же эффект:

sqladdress = session.query(EmailAddress).filter(EmailAddress.email == 'address').one()

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

Синонимы

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

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

from sqlalchemy.orm import synonym


class MyClass(Base):
    __tablename__ = "my_table"

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

    status = synonym("job_status")

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

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

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

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

>>> 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 = Column(Integer, primary_key=True)
    status = 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 = Column(Integer, primary_key=True)
    status = Column(String(50))

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

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

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

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

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

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