Изменение поведения атрибутов¶
Простые валидаторы¶
Быстрый способ добавить процедуру «валидации» к атрибуту - использовать декоратор 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()
SELECT address.email AS address_email, address.id AS address_id
FROM address
WHERE address.email = ?
('address@example.com',)
address.email = "otheraddress@example.com"
{sql}session.commit()
UPDATE address SET email=? WHERE address.id = ?
('otheraddress@example.com', 1)
COMMIT
Модификатор 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()
SELECT address.email AS address_email, address.id AS address_id
FROM address
WHERE substr(address.email, ?, length(address.email) - ?) = ?
(0, 12, 'address')
Подробнее о гибридах читайте на Атрибуты гибрида.
Синонимы¶
Синонимы - это конструкция на уровне отображения, которая позволяет любому атрибуту класса «отражать» другой атрибут, который отображается.
В самом основном смысле синоним - это простой способ сделать определенный атрибут доступным по дополнительному имени:
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
.