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
andAssertionError
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
; ifFalse
, the validation function will not emit if the originator is an attribute event related via a backref. This can be used for bi-directionalvalidates()
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 ofTrue
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
, thesynonym()
construct will locate theColumn
object upon the mapped table that would normally be associated with the attribute name of this synonym, and produce a newColumnProperty
that instead maps thisColumn
to the alternate name given as the “name” argument of the synonym; in this way, the usual step of redefining the mapping of theColumn
to be under a different name is unnecessary. This is usually intended to be used when aColumn
is to be replaced with an attribute that also uses a descriptor, that is, in conjunction with thesynonym.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 thejob_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 DeclarativeUsing 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
のドキュメントを参照してください。