更改属性行为

本节将讨论用于修改 ORM 映射属性行为的特性和技术,包括使用 mapped_column()relationship() 和其他方法映射的属性。

简单验证器

向属性添加“验证”例程的快速方法是使用 validates() 装饰器。属性验证器可以引发异常,阻止属性值更改的过程,或者可以将给定值更改为其他值。与所有属性扩展一样,验证器仅由正常的 userland 代码调用;当 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

通过 backref 链接的相互依赖的验证器的情况也可以定制,使用 include_backrefs=False 选项;当此选项设置为 False 时,如果事件是由于 backref 引起的,则阻止验证函数发出。

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

上面,如果我们像 some_address.user = some_user 那样分配给 Address.user,则 不会 发出 validate_address() 函数,即使 some_user.addresses 发生了追加 - 该事件是由 backref 引起的。

请注意,validates() 装饰器是构建在属性事件之上的便捷函数。需要更多控制属性更改行为配置的应用程序可以使用此系统,该系统在 AttributeEvents 中描述。

对象名称 描述

validates(*names, [include_removes, include_backrefs])

将方法装饰为一个或多个命名属性的“验证器”。

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

将方法装饰为一个或多个命名属性的“验证器”。

将方法指定为验证器,该方法接收属性的名称以及要分配的值,或者在集合的情况下,接收要添加到集合的值。然后,该函数可以引发验证异常以阻止进程继续(其中 Python 的内置 ValueErrorAssertionError 异常是合理的选择),或者可以在继续之前修改或替换值。否则,该函数应返回给定值。

请注意,集合的验证器 不能 在验证例程中发出该集合的加载 - 此用法会引发断言以避免递归溢出。这是一种不支持的可重入条件。

参数:
  • *names – 要验证的属性名称列表。

  • include_removes – 如果为 True,则也会发送“remove”事件 - 验证函数必须接受一个额外的参数“is_remove”,该参数将是一个布尔值。

  • include_backrefs

    默认为 True;如果为 False,则如果发起者是通过 backref 相关的属性事件,则验证函数将不会发出。这可以用于双向 validates() 用法,其中每个属性操作只应发出一个验证器。

    Changed in version 2.0.16: 此参数在 2.0.0 到 2.0.15 版本中意外地默认为 False。其正确的默认值 True 在 2.0.16 中恢复。

另请参阅

简单验证器 - validates() 的用法示例

在核心层使用自定义数据类型

影响列值的一种非 ORM 方法,使其适合在 Python 中的表示形式与数据库中的表示形式之间转换数据,可以通过使用应用于映射的 Table 元数据的自定义数据类型来实现。这在数据进入数据库和返回时发生的某种样式的编码/解码情况下更为常见;在核心文档中的 增强现有类型 中阅读有关此内容的更多信息。

使用描述符和混合属性

为属性生成修改行为的更全面的方法是使用 描述符。这些通常在 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

当我们在 EmailAddress 类级别(即直接从 EmailAddress 类)使用时,.email 属性除了在拥有 EmailAddress 实例时提供 getter/setter 行为外,还提供 SQL 表达式。

from sqlalchemy.orm import Session
from sqlalchemy import select

session = Session()

address = session.scalars(
    select(EmailAddress).where(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" 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 = 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)

上面,访问 EmailAddress 实例的 email 属性将返回 _email 属性的值,从值中删除或添加主机名 @example.com。当我们查询 email 属性时,会渲染一个 SQL 函数,产生相同的效果。

address = session.scalars(
    select(EmailAddress).where(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 = 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")
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() 也可以设置为引用用户定义的 描述符。我们可以为我们的 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)

当使用声明式时,可以使用 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() 对于简单的镜像很有用,但在现代用法中,使用描述符增强属性行为的用例最好使用 混合属性 功能来处理,该功能更面向 Python 描述符。从技术上讲,synonym() 可以完成 hybrid_property 可以完成的所有操作,因为它也支持注入自定义 SQL 功能,但在更复杂的情况下,混合属性更易于使用。

对象名称 描述

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

将属性名称表示为映射属性的同义词,该属性将镜像另一个属性的值和表达式行为。

function sqlalchemy.orm.synonym(name: str, *, map_column: bool | None = None, descriptor: Any | None = None, comparator_factory: Type[PropComparator[_T]] | None = None, init: _NoArg | bool = _NoArg.NO_ARG, repr: _NoArg | bool = _NoArg.NO_ARG, default: _NoArg | _T = _NoArg.NO_ARG, default_factory: _NoArg | Callable[[], _T] = _NoArg.NO_ARG, compare: _NoArg | bool = _NoArg.NO_ARG, kw_only: _NoArg | bool = _NoArg.NO_ARG, hash: _NoArg | bool | None = _NoArg.NO_ARG, info: _InfoType | None = None, doc: str | None = 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 描述符,当在实例级别访问此属性时,将用作 getter(以及可能的 setter)。

  • map_column

    仅适用于经典映射和针对现有 Table 对象的映射。如果为 True,则 synonym() 构造将找到映射表上的 Column 对象,该对象通常与此同义词的属性名称关联,并生成一个新的 ColumnProperty,该属性将此 Column 映射到作为同义词的“name”参数给出的备用名称;通过这种方式,通常的重新定义 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
    Status: employed

    当使用声明式时,为了结合同义词提供描述符,请使用 sqlalchemy.ext.declarative.synonym_for() 助手。但是,请注意,通常应优先使用 混合属性 功能,尤其是在重新定义属性行为时。

  • info – 可选数据字典,将填充到此对象的 InspectionAttr.info 属性中。

  • comparator_factory

    一个 PropComparator 的子类,将在 SQL 表达式级别提供自定义比较行为。

    注意

    对于提供重新定义属性的 Python 级别和 SQL 表达式级别行为的属性的用例,请参阅 使用描述符和混合属性 中介绍的混合属性,以获得更有效的技术。

另请参阅

同义词 - 同义词概述

synonym_for() - 面向声明式的助手

使用描述符和混合属性 - 混合属性扩展提供了一种更新的方法来增强属性行为,比使用同义词可以实现的更灵活。

运算符自定义

SQLAlchemy ORM 和核心表达式语言使用的“运算符”是完全可自定义的。例如,比较表达式 User.name == 'ed' 使用 Python 本身内置的运算符 operator.eq - SQLAlchemy 与此类运算符关联的实际 SQL 构造可以修改。新的操作也可以与列表达式关联。列表达式发生的运算符最直接地在类型级别重新定义 - 请参阅 重新定义和创建新运算符 部分以获取描述。

ORM 级别函数(如 column_property()relationship()composite())也通过将 PropComparator 子类传递给每个函数的 comparator_factory 参数来提供 ORM 级别的运算符重新定义。在此级别自定义运算符是一种罕见的用例。请参阅 PropComparator 中的文档以获取概述。