更改属性行为

本节将讨论用于修改 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 时,如果事件是由反向引用引起的,则会阻止验证函数发出。当使用双向 validates() 时,可以使用此选项确保每次属性操作只发出一个验证器。

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 中发生追加 - 此事件是由反向引用引起的。

请注意,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,则还会发送“移除”事件 - 验证函数必须接受一个额外的参数“is_remove”,它将是一个布尔值。

  • include_backrefs

    默认值为 True;如果为 False,则如果事件的来源是通过反向引用关联的属性事件,则验证函数不会发出。当使用双向 validates() 时,可以使用此选项确保每次属性操作只发出一个验证器。

    在版本 2.0.16 中更改: 此参数在 2.0.0 到 2.0.15 版本中不慎默认为 False。在 2.0.16 版本中恢复了其正确的默认值 True

另请参阅

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

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

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

使用描述符和混合属性

一个更全面的方法来修改属性的行为是使用描述符。在 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 属性除了在拥有 EmailAddress 实例时提供 getter/setter 行为外,还在类级别使用时(即直接从 EmailAddress 类使用)提供一个 SQL 表达式。

from sqlalchemy.orm import Session
from sqlalchemy import select

session = Session()

address = session.scalars(
    select(EmailAddress).where(EmailAddress.email == "[email protected]")
).one()
SELECT address.email AS address_email, address.id AS address_id FROM address WHERE address.email = ? ('[email protected]',)
address.email = "[email protected]" session.commit()
UPDATE address SET email=? WHERE address.id = ? ('[email protected]', 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]

将一个属性名指定为映射属性的同义词,这样该属性就会镜像另一个属性的值和表达式行为。

e.g.

class MyClass(Base):
    __tablename__ = 'my_table'

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

    status = synonym("job_status")
参数:
  • name – 已映射属性的名称。它可以指代在类上配置的 ORM 映射属性的字符串名称,包括列绑定属性和关系。

  • descriptor – 当在实例级别访问此属性时,将用作 getter(以及可能的 setter)的 Python 描述符

  • 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

    使用 Declarative 时,为了提供与同义词结合使用的描述符,请使用 sqlalchemy.ext.declarative.synonym_for() 助手。但是,请注意,混合属性 功能通常应该优先考虑,特别是在重新定义属性行为时。

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

  • comparator_factory

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

    注意

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

另请参阅

同义词 - 同义词概述

synonym_for() - 面向 Declarative 的助手

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

运算符自定义

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

column_property()relationship()composite() 这样的 ORM 级别函数也通过将 PropComparator 子类传递给每个函数的 comparator_factory 参数来提供在 ORM 级别重新定义运算符的功能。在该级别自定义运算符是一个很少见的使用案例。有关概述,请参阅 PropComparator 的文档。