更改Attribute的行为

Changing Attribute Behavior

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

This section will discuss features and techniques used to modify the behavior of ORM mapped attributes, including those mapped with mapped_column(), relationship(), and others.

简单验证器

Simple Validators

快速为属性添加“验证”例程的方法是使用 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

通过 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

上述代码中,如果我们将 Address.user 分配为 some_address.user = some_user,即使 some_user.addresses 发生了添加,validate_address() 函数也*不会*发出,因为事件是由 backref 引起的。

请注意,validates() 装饰器是基于属性事件构建的便捷功能。需要更多控制属性更改行为配置的应用程序可以使用此系统,如 AttributeEvents 中所述。

A quick way to add a “validation” routine to an attribute is to use the validates() decorator. An attribute validator can raise an exception, halting the process of mutating the attribute’s value, or can change the given value into something different. Validators, like all attribute extensions, are only called by normal userland code; they are not issued when the ORM is populating the object:

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

Validators also receive collection append events, when items are added to a collection:

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

The validation function by default does not get emitted for collection remove events, as the typical expectation is that a value being discarded doesn’t require validation. However, validates() supports reception of these events by specifying include_removes=True to the decorator. When this flag is set, the validation function must receive an additional boolean argument which if True indicates that the operation is a removal:

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

The case where mutually dependent validators are linked via a backref can also be tailored, using the include_backrefs=False option; this option, when set to False, prevents a validation function from emitting if the event occurs as a result of a 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

Above, if we were to assign to Address.user as in some_address.user = some_user, the validate_address() function would not be emitted, even though an append occurs to some_user.addresses - the event is caused by a backref.

Note that the validates() decorator is a convenience function built on top of attribute events. An application that requires more control over configuration of attribute change behavior can make use of this system, described at AttributeEvents.

Object Name Description

validates(*names, [include_removes, include_backrefs])

Decorate a method as a ‘validator’ for one or more named properties.

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

Decorate a method as a ‘validator’ for one or more named properties.

Designates a method as a validator, a method which receives the name of the attribute as well as a value to be assigned, or in the case of a collection, the value to be added to the collection. The function can then raise validation exceptions to halt the process from continuing (where Python’s built-in ValueError and AssertionError exceptions are reasonable choices), or can modify or replace the value before proceeding. The function should otherwise return the given value.

Note that a validator for a collection cannot issue a load of that collection within the validation routine - this usage raises an assertion to avoid recursion overflows. This is a reentrant condition which is not supported.

参数:
  • *names – list of attribute names to be validated.

  • include_removes – if True, “remove” events will be sent as well - the validation function must accept an additional argument “is_remove” which will be a boolean.

  • include_backrefs

    defaults to True; if False, the validation function will not emit if the originator is an attribute event related via a backref. This can be used for bi-directional validates() usage where only one validator should emit per attribute operation.

    在 2.0.16 版本发生变更: This paramter inadvertently defaulted to False for releases 2.0.0 through 2.0.15. Its correct default of True is restored in 2.0.16.

参见

简单验证器 - usage examples for validates()

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

Using Custom Datatypes at the Core Level

一种非ORM的方式,通过自定义数据类型来影响列的值,以适应数据在Python中和在数据库中表示方式之间的转换,可以应用于映射的 Table 元数据。在一些需要在数据存入数据库和返回时进行编码/解码的情况下,这种方式更为常见;在核心文档的 增强现有类型 部分可以阅读更多相关内容。

A non-ORM means of affecting the value of a column in a way that suits converting data between how it is represented in Python, vs. how it is represented in the database, can be achieved by using a custom datatype that is applied to the mapped Table metadata. This is more common in the case of some style of encoding / decoding that occurs both as data goes to the database and as it is returned; read more about this in the Core documentation at 增强现有类型.

使用描述符和混合

Using Descriptors and Hybrids

一种更全面的方法来为属性生成修改后的行为是使用 descriptors。这些通常在Python中使用 property() 函数。SQLAlchemy的标准描述符技术是创建一个普通描述符,并让它从一个具有不同名称的映射属性中读取/写入。下面我们用Python 2.6风格的属性来说明这一点:

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    # 使用下划线命名属性,
    # 与列名不同
    _email = mapped_column("email", String)

    # 然后创建一个 ".email" 属性
    # 来获取/设置 "._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 行为外,当在类级别使用时也提供 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()
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):
        """返回 _email 的值,直到最后十二个字符。"""
        return self._email[:-12]

    @email.setter
    def email(self, email):
        """设置 _email 的值,附加上十二个字符的值 @example.com。"""
        self._email = email + "@example.com"

    @email.expression
    def email(cls):
        """生成一个表示 _email 列值的 SQL 表达式,减去最后十二个字符。"""
        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')

更多关于 Hybrids 的内容,请参阅 混合属性

A more comprehensive way to produce modified behavior for an attribute is to use descriptors. These are commonly used in Python using the property() function. The standard SQLAlchemy technique for descriptors is to create a plain descriptor, and to have it read/write from a mapped attribute with a different name. Below we illustrate this using Python 2.6-style properties:

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

The approach above will work, but there’s more we can add. While our EmailAddress object will shuttle the value through the email descriptor and into the _email mapped attribute, the class level EmailAddress.email attribute does not have the usual expression semantics usable with Select. To provide these, we instead use the hybrid extension as follows:

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

The .email attribute, in addition to providing getter/setter behavior when we have an instance of EmailAddress, also provides a SQL expression when used at the class level, that is, from the EmailAddress class directly:

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

The hybrid_property also allows us to change the behavior of the attribute, including defining separate behaviors when the attribute is accessed at the instance level versus at the class/expression level, using the hybrid_property.expression() modifier. Such as, if we wanted to add a host name automatically, we might define two sets of string manipulation logic:

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)

Above, accessing the email property of an instance of EmailAddress will return the value of the _email attribute, removing or adding the hostname @example.com from the value. When we query against the email attribute, a SQL function is rendered which produces the same effect:

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

Read more about Hybrids at 混合属性.

同义词

Synonyms

同义词是一种映射器级别的结构,允许类上的任何属性“镜像”另一个被映射的属性。

在最基本的意义上,同义词是一种通过额外的名称使某个属性可用的简便方法:

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() 还可以引用用户定义的 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)

当使用声明式时,上述模式可以更简洁地表示,使用 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功能的注入,但在复杂情况下,hybrid 更加直接易用。

Synonyms are a mapper-level construct that allow any attribute on a class to “mirror” another attribute that is mapped.

In the most basic sense, the synonym is an easy way to make a certain attribute available by an additional name:

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

The above class MyClass has two attributes, .job_status and .status that will behave as one attribute, both at the expression level:

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

and at the instance level:

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

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

The synonym() can be used for any kind of mapped attribute that subclasses MapperProperty, including mapped columns and relationships, as well as synonyms themselves.

Beyond a simple mirror, synonym() can also be made to reference a user-defined descriptor. We can supply our status synonym with a @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)

When using Declarative, the above pattern can be expressed more succinctly using the synonym_for() decorator:

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

While the synonym() is useful for simple mirroring, the use case of augmenting attribute behavior with descriptors is better handled in modern usage using the hybrid attribute feature, which is more oriented towards Python descriptors. Technically, a synonym() can do everything that a hybrid_property can do, as it also supports injection of custom SQL capabilities, but the hybrid is more straightforward to use in more complex situations.

Object Name Description

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

Denote an attribute name as a synonym to a mapped property, in that the attribute will mirror the value and expression behavior of another attribute.

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]

Denote an attribute name as a synonym to a mapped property, in that the attribute will mirror the value and expression behavior of another attribute.

e.g.:

class MyClass(Base):
    __tablename__ = "my_table"

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

    status = synonym("job_status")
参数:
  • name – the name of the existing mapped property. This can refer to the string name ORM-mapped attribute configured on the class, including column-bound attributes and relationships.

  • descriptor – a Python descriptor that will be used as a getter (and potentially a setter) when this attribute is accessed at the instance level.

  • map_column

    For classical mappings and mappings against an existing Table object only. if True, the synonym() construct will locate the Column object upon the mapped table that would normally be associated with the attribute name of this synonym, and produce a new ColumnProperty that instead maps this Column to the alternate name given as the “name” argument of the synonym; in this way, the usual step of redefining the mapping of the Column to be under a different name is unnecessary. This is usually intended to be used when a Column is to be replaced with an attribute that also uses a descriptor, that is, in conjunction with the synonym.descriptor parameter:

    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,
            )
        },
    )

    Above, the attribute named _job_status is automatically mapped to the job_status column:

    >>> j1 = MyClass()
    >>> j1._job_status = "employed"
    >>> j1.job_status
    Status: employed

    When using Declarative, in order to provide a descriptor in conjunction with a synonym, use the sqlalchemy.ext.declarative.synonym_for() helper. However, note that the hybrid properties feature should usually be preferred, particularly when redefining attribute behavior.

  • info – Optional data dictionary which will be populated into the InspectionAttr.info attribute of this object.

  • comparator_factory

    A subclass of PropComparator that will provide custom comparison behavior at the SQL expression level.

    备注

    For the use case of providing an attribute which redefines both Python-level and SQL-expression level behavior of an attribute, please refer to the Hybrid attribute introduced at 使用描述符和混合 for a more effective technique.

参见

同义词 - Overview of synonyms

synonym_for() - a helper oriented towards Declarative

使用描述符和混合 - The Hybrid Attribute extension provides an updated approach to augmenting attribute behavior more flexibly than can be achieved with synonyms.

运算符自定义

Operator Customization

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

ORM 级别的函数如 column_property()relationship()composite() 也通过将 PropComparator 子类传递给每个函数的 comparator_factory 参数来提供在 ORM 级别重新定义操作符的功能。在此级别自定义操作符的用例较为罕见。有关概述,请参阅 PropComparator 的文档。

The “operators” used by the SQLAlchemy ORM and Core expression language are fully customizable. For example, the comparison expression User.name == 'ed' makes usage of an operator built into Python itself called operator.eq - the actual SQL construct which SQLAlchemy associates with such an operator can be modified. New operations can be associated with column expressions as well. The operators which take place for column expressions are most directly redefined at the type level - see the section 重新定义和创建新运算符 for a description.

ORM level functions like column_property(), relationship(), and composite() also provide for operator redefinition at the ORM level, by passing a PropComparator subclass to the comparator_factory argument of each function. Customization of operators at this level is a rare use case. See the documentation at PropComparator for an overview.