Changing Attribute Behavior

このセクションでは、 mapped_column()relationship() などでマップされた属性を含め、ORMでマップされた属性の動作を変更するための機能とテクニックについて説明します。

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

上の例では、 some_address.user=some_user のように Address.user に代入した場合、 some_user.addresses への追加が発生しても、 validate_address() 関数は発行されません。イベントはbackrefによって発生します。

validates() デコレータは、属性イベントの上に構築された便利な関数であることに注意してください。属性の変更動作の設定をより制御する必要があるアプリケーションは、 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.

Parameters:
  • *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.

    Changed in version 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.

See also

Simple Validators - usage examples for validates()

Using Custom Datatypes at the Core Level

Pythonでの表現方法とデータベースでの表現方法の間でデータを変換するのに適した方法で列の値に影響を与えるORM以外の方法は、マップされた Table メタデータに適用されるカスタムデータ型を使用することで実現できます。これは、データがデータベースに送られるときと返されるときの両方で発生する何らかのスタイルのエンコード/デコードの場合によく見られます。詳細については、 Augmenting Existing Types のコアドキュメントを参照してください。

Using Descriptors and Hybrids

属性の変更された動作を生成するためのより包括的な方法は、 descriptors を使用することです。これらは一般的に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 == "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() 修飾子を使って、属性がインスタンスレベルでアクセスされた場合と、クラス/式レベルでアクセスされた場合の別々の振る舞いを定義することも含まれます。例えば、ホスト名を自動的に追加したい場合、文字列操作ロジックの2つのセットを定義することができます:

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

ハイブリッドについての詳細は Hybrid Attributes を参照してください。

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 には2つの属性、 .job_status.status があり、どちらも式レベルで1つの属性として動作します。:

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

Declarativeを使用する場合、上記のパターンは 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記述子を対象とした hybrid attribute 機能を使用して、最新の使用法でより適切に処理されます。技術的には、 synonym() はカスタムSQL機能の注入もサポートしているので、 hybrid_property ができることはすべてできますが、このハイブリッドはより複雑な状況でより簡単に使用できます。

Object Name Description

synonym(name, *, [map_column, descriptor, comparator_factory, init, repr, default, default_factory, compare, kw_only, 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, 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")
Parameters:
  • 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.

    Note

    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 Using Descriptors and Hybrids for a more effective technique.

See also

Synonyms - Overview of synonyms

synonym_for() - a helper oriented towards Declarative

Using Descriptors and Hybrids - The Hybrid Attribute extension provides an updated approach to augmenting attribute behavior more flexibly than can be achieved with synonyms.

Operator Customization


SQLAlchemyのORMとCore式言語で使用される”演算子”は完全にカスタマイズ可能です。例えば、比較式 User.name == 'ed' は、Python自身に組み込まれた operator.eq という名前の演算子を使用します。SQLAlchemyがこのような演算子に関連付ける実際のSQL構文は変更できます。新しい演算は列式にも関連付けることができます。列式で使用される演算子は、型レベルで最も直接的に再定義されます。詳細については Redefining and Creating New Operators を参照してください。

column_property(),:func:_orm.relationship ,および composite() のようなORMレベルの関数も、 PropComparator サブクラスを各関数の comparator_factory 引数に渡すことで、ORMレベルでの演算子の再定義を提供します。このレベルでの演算子のカスタマイズは稀なユースケースです。概要については PropComparator のドキュメントを参照してください。