Integration with dataclasses and attrs¶
バージョン2.0のSQLAlchemyは”ネイティブなデータクラス”の統合を特徴としており、マッピングされたクラスに単一のミックスインまたはデコレータを追加することで、 Annotated Declarative Table マッピングをPython dataclass に変換することができます。
New in version 2.0: ORM宣言クラスを使用したデータクラスの作成の統合
また、既存のデータ・クラスをマップできるパターンや、attrs サードパーティ統合ライブラリーによってインスツルメントされたクラスをマップできるパターンもあります。
Declarative Dataclass Mapping¶
SQLAlchemy Annotated Declarative Table マッピングは、追加のミックスインクラスまたはデコレータディレクティブで拡張することができます。これは、マッピングが完了した後、ORM固有の instrumentation をクラスに適用するマッピングプロセスを完了する前に、マッピングされたクラスを in-place でPython dataclass に変換する追加のステップを宣言プロセスに追加します。これが提供する最も顕著な動作上の追加は、位置引数とキーワード引数をデフォルトの有無にかかわらずきめ細かく制御できる __init__()
メソッドの生成と、 __repr__()
や __eq__()
のようなメソッドの生成です。
PEP 484 型定義の観点から見ると、そのクラスはDataclass特有の振る舞いをするものとして認識されます。特に PEP 681 の”Dataclass Transforms”を利用することで、型定義ツールはそのクラスが明示的に @dataclasses.dataclass
デコレータを使って修飾されているかのように扱うことができます。
Note
2023年4月4日 の時点でのタイピングツールにおける PEP 681 のサポートは制限されており、現在のところ Pyright および バージョン1.2 の Mypy でサポートされていることがわかっています。Mypy 1.1.1では PEP 681 サポートが導入されましたが、Python記述子に正しく対応していなかったため、SQLAlchemyのORMマッピングスキームを使用するとエラーが発生することに注意してください。
See also
https://peps.python.org/pep-0681/#the-dataclass-transform-decorator-SQLAlchemy - SQLAlchemyのようなライブラリが PEP 681 サポートを可能にする背景
データクラス変換は、 MappedAsDataclass
ミックスインを DeclarativeBase
クラス階層に追加するか、デコレータマッピングのために registry.mapped_as_dataclass()
クラスデコレータを使用することで、任意の宣言型クラスに追加できます。
MappedAsDataclass
ミックスインは、以下の例のように、宣言型の Base
クラスにも、任意のスーパークラスにも適用できます。:
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
class Base(MappedAsDataclass, DeclarativeBase):
"""subclasses will be converted to dataclasses"""
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
あるいは、宣言ベースから拡張されたクラスに直接適用することもできます。:
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
class Base(DeclarativeBase):
pass
class User(MappedAsDataclass, Base):
"""User class will be converted to a dataclass"""
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
デコレータ形式を使用する場合は、 registry.mapped_as_dataclass()
デコレータのみがサポートされます。:
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
reg = registry()
@reg.mapped_as_dataclass
class User:
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
Class level feature configuration¶
データクラス機能のサポートは部分的です。現在 サポートされている 機能は、Python 3.10+でサポートされている init
、 repr
、 eq
、 order
、 unsafe_hash
です。 match_args
、 kw_only
はサポートされています。現在**サポートされていない**機能は、 frozen
、 slots
です。
MappedAsDataclass
でミックスインクラスフォームを使用する場合、クラス設定引数はクラスレベルのパラメータとして渡されます:
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
class Base(DeclarativeBase):
pass
class User(MappedAsDataclass, Base, repr=False, unsafe_hash=True):
"""User class will be converted to a dataclass"""
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
デコレータ形式を registry.mapped_as_dataclass()
で使用すると、クラス設定引数がデコレータに直接渡されます:
from sqlalchemy.orm import registry
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
reg = registry()
@reg.mapped_as_dataclass(unsafe_hash=True)
class User:
"""User class will be converted to a dataclass"""
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
dataclass クラスオプションの背景については、 @dataclasses.dataclass を参照してください。
Attribute Configuration¶
SQLAlchemyのネイティブなデータクラスが通常のデータクラスと異なるのは、マップされる属性が Mapped
汎用アノテーションコンテナを使って記述される点です。マッピングは Declarative Table with mapped_column() に記述されているものと同じ形式に従い、 mapped_column()
と Mapped
のすべての機能がサポートされています。
さらに、 mapped_column()
,:func:_orm.relationship,:func:_orm.composite などのORM属性設定構造体は、 属性ごとのフィールドオプション をサポートしています。これには、 init
, default
, default_factory
, repr
,などが含まれます。これらの引数の名前は PEP 681 で指定されたとおりに固定されています。機能はデータクラスと同等です:
mapped_column.init
やrelationship.init
のように、Falseの場合はフィールドが__init__()
メソッドの一部であってはならないことを示します。
mapped_column.default
のように、 :paramref: _orm.relationship.default はフィールドのデフォルト値を示します。これは、__init__()
メソッドのキーワード引数として与えられます。
mapped_column.default_factory
やrelationship.default_factory
のようなdefault_factory
は、明示的に__init__()
メソッドに渡されなければ、パラメータの新しいデフォルト値を生成するために呼び出される呼び出し可能な関数を示します。
repr
デフォルトでは真で、フィールドが生成された__repr__()
メソッドの一部であることを示します。
データクラスとのもう1つの重要な違いは、属性のデフォルト値は 必ず ORM構文の default
パラメータを使って設定しなければならないことです。例えば、 mapped_column(default=None)
のように設定します。 @dataclases.field()
を使わずに単純なPython値をデフォルトとして受け入れるデータクラス構文に似た構文はサポートされていません。
mapped_column()
を使った例として、以下のマッピングでは、フィールド name
と fullname
のみを受け付ける __init__()
メソッドが生成されます。ここで、 name
は必須であり、位置的に渡すことができ、 fullname
はオプションです。データベースで生成されるはずの id
フィールドは、コンストラクタの一部ではありません:
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
reg = registry()
@reg.mapped_as_dataclass
class User:
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
fullname: Mapped[str] = mapped_column(default=None)
# 'fullname' is optional keyword argument
u1 = User("name")
Column Defaults¶
Column
構文の既存の Column.default
パラメータと default
引数の名前の重複に対応するために、 mapped_column()
構文は、新しいパラメータ mapped_column.insert_default
を追加することで2つの名前を明確にします。このパラメータは、データクラスの設定に常に使用される mapped_column.default
で設定されるものとは無関係に、 Column
の Column.default
パラメータに直接入力されます。たとえば、 Column.default
をSQL関数の func.utc_timestamp()
に設定してdatetime列を設定しますが、このパラメータはコンストラクタではオプションです。:
from datetime import datetime
from sqlalchemy import func
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
reg = registry()
@reg.mapped_as_dataclass
class User:
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
created_at: Mapped[datetime] = mapped_column(
insert_default=func.utc_timestamp(), default=None
)
上記のマッピングでは、新しい User
オブジェクトの INSERT
は、 created_at
のパラメータが渡されない場合、次のように処理されます。:
>>> with Session(e) as session:
... session.add(User())
... session.commit()
BEGIN (implicit)
INSERT INTO user_account (created_at) VALUES (utc_timestamp())
[generated in 0.00010s] ()
COMMIT
Integration with Annotated¶
Mapping Whole Column Declarations to Python Types で紹介されたアプローチは、再利用のために mapped_column()
構文全体をパッケージ化するために、 PEP 593 Annotated
オブジェクトを使用する方法を示しています。この機能はデータクラス機能でサポートされています。ただし、この機能の1つの側面では、タイピングツールを使用するときに回避策が必要です。それは、タイピングツールが属性を正しく解釈するために、 PEP 681 固有の引数である init
、 default
、 repr
、 default_factory
が右側にあり、明示的な mapped_column()
構文にパッケージ化されている 必要がある* ということです。例として、以下のアプローチは実行時には完全にうまく動作しますが、タイピングツールは`init=False`パラメータが存在しないので、 User()
構文を無効と見なします:
from typing import Annotated
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
# typing tools will ignore init=False here
intpk = Annotated[int, mapped_column(init=False, primary_key=True)]
reg = registry()
@reg.mapped_as_dataclass
class User:
__tablename__ = "user_account"
id: Mapped[intpk]
# typing error: Argument missing for parameter "id"
u1 = User()
代わりに mapped_column()
を右側に置き、 mapped_column.init
を明示的に設定する必要があります。他の引数は Annotated
構文内に残すことができます:
from typing import Annotated
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
intpk = Annotated[int, mapped_column(primary_key=True)]
reg = registry()
@reg.mapped_as_dataclass
class User:
__tablename__ = "user_account"
# init=False and other pep-681 arguments must be inline
id: Mapped[intpk] = mapped_column(init=False)
u1 = User()
Using mixins and abstract superclasses¶
MappedAsDataclass
マップクラスで使用され、 Mapped
属性を含むミックスインまたは基底クラスは、以下のミックスインを使用した例のように、それ自体が MappedAsDataclass
階層の一部である必要があります。:
class Mixin(MappedAsDataclass):
create_user: Mapped[int] = mapped_column()
update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)
class Base(DeclarativeBase, MappedAsDataclass):
pass
class User(Base, Mixin):
__tablename__ = "sys_user"
uid: Mapped[str] = mapped_column(
String(50), init=False, default_factory=uuid4, primary_key=True
)
username: Mapped[str] = mapped_column()
email: Mapped[str] = mapped_column()
PEP 681 をサポートするPythonの型チェッカーは、それ以外の場合、非データクラスミックスインからの属性をデータクラスの一部と見なしません。
Deprecated since version 2.0.8: MappedAsDataclass
または registry.mapped_as_dataclass()
階層内で、それ自体はデータクラスではないmixinと抽象ベースを使用することは非推奨です。なぜなら、これらのフィールドは PEP 681 ではデータクラスに属するものとしてサポートされていないからです。この場合は警告が表示され、後でエラーになります。
Relationship Configuration¶
Mapped
アノテーションと relationship()
の組み合わせは、 Basic Relationship Patterns で説明されているのと同じ方法で使用されます。コレクションベースの relationship()
をオプションのキーワード引数として指定する場合、 relationship.default_factory
パラメータを渡す必要があり、それは使用されるコレクションクラスを参照する必要があります。多対1およびスカラーオブジェクト参照は、デフォルト値が None
の場合、 relationship.default
を利用できます:
from typing import List
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship
reg = registry()
@reg.mapped_as_dataclass
class Parent:
__tablename__ = "parent"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(
default_factory=list, back_populates="parent"
)
@reg.mapped_as_dataclass
class Child:
__tablename__ = "child"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
parent: Mapped["Parent"] = relationship(default=None)
The above mapping will generate an empty list for Parent.children
when a new Parent()
object is constructed without passing children
, and similarly a None
value for Child.parent
when a new Child()
object is constructed without passing parent
.
上記のマッピングでは、新しい Parent()
オブジェクトが children
を渡さずに構築されると、 Parent.children
に空のリストが生成されます。同様に、新しい Child()
オブジェクトが parent
を渡さずに構築されると、 Child.parent
に None
という値が生成されます。
relationship.default_factory
は relationship()
自体の与えられたコレクションクラスから自動的に派生させることができますが、 relationship.default_factory
または relationship.default
の存在は、パラメータが __init__()
メソッドにレンダリングされるときに必要かオプションかを決定するものであるため、これはデータクラスとの互換性を損なうことになります。
Using Non-Mapped Dataclass Fields¶
宣言型データクラスを使用する場合、マップされていないフィールドもクラスで使用できます。これはデータクラス構築プロセスの一部になりますが、マップされません。 Mapped
を使用しないフィールドは、マッピングプロセスによって無視されます。次の例では、フィールド ctrl_one
と ctrl_two
はオブジェクトのインスタンスレベルの状態の一部になりますが、ORMによって保持されません:
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
reg = registry()
@reg.mapped_as_dataclass
class Data:
__tablename__ = "data"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
status: Mapped[str]
ctrl_one: Optional[str] = None
ctrl_two: Optional[str] = None
上記の Data
のインスタンスは以下のように作成できます:
d1 = Data(status="s1", ctrl_one="ctrl1", ctrl_two="ctrl2")
もっと現実的な例としては、Dataclassesの InitVar
機能を __post_init__()
機能と組み合わせて使用して、永続化されたデータの作成に使用できるinit専用フィールドを受け取ることが考えられます。以下の例では、 User
クラスはマッピング機能として id
、 name
、 password_hash
を使用して宣言されていますが、ユーザー作成プロセスを表すために init専用
の password
フィールドと repeat_password
フィールドを使用しています(注:この例を実行するには、関数 your_crypt_function_here()
を bcrypt や argon2-cffi などのサードパーティのcrypt関数に置き換えてください)。:
from dataclasses import InitVar
from typing import Optional
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
reg = registry()
@reg.mapped_as_dataclass
class User:
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
password: InitVar[str]
repeat_password: InitVar[str]
password_hash: Mapped[str] = mapped_column(init=False, nullable=False)
def __post_init__(self, password: str, repeat_password: str):
if password != repeat_password:
raise ValueError("passwords do not match")
self.password_hash = your_crypt_function_here(password)
上記のオブジェクトはパラメータ password
と repeat_password
で作成されます。これらのパラメータは、 password_hash
変数が生成されるように事前に消費されます。
>>> u1 = User(name="some_user", password="xyz", repeat_password="xyz")
>>> u1.password_hash
'$6$9ppc... (example crypted string....)'
Changed in version 2.0.0rc1: registry.mapped_as_dataclass()
または MappedAsDataclass
を使用する場合、 Mapped
アノテーションを含まないフィールドを含めることができます。これは結果のデータクラスの一部として扱われますが、マップされません。クラス属性 __allow_unmapped__
を指定する必要はありません。以前の2.0ベータ版では、この属性の目的が従来のORM型マッピングを引き続き機能させることだけであったとしても、この属性を明示的に指定する必要がありました。
Integrating with Alternate Dataclass Providers such as Pydantic¶
Warning
Pydanticのデータクラスレイヤは、追加の内部変更なしにはSQLAlchemyのクラスインストルメンテーションと 完全には互換性が なく、関連するコレクションなどの多くの機能が正しく動作しない可能性があります。
Pydanticとの互換性については、SQLAlchemy ORMの上にPydanticで構築された SQLModel ORMを検討してください。これには、これらの非互換性を 明示的に解決 する特別な実装の詳細が含まれています。
SQLAlchemyの MappedAsDataclass
クラスと registry.mapped_as_dataclass()
メソッドは、宣言的なマッピングプロセスがクラスに適用された後、Python標準ライブラリの dataclasses.dataclass
クラスデコレータを直接呼び出します。この関数呼び出しは、 MappedAsDataclass
や registry.mapped_as_dataclass()
がクラスキーワード引数として受け入れる dataclass_callable
パラメータを使って、Pydanticのような代替的なデータクラスプロバイダにスワップアウトすることができます:
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
from sqlalchemy.orm import registry
class Base(
MappedAsDataclass,
DeclarativeBase,
dataclass_callable=pydantic.dataclasses.dataclass,
):
pass
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
上記の User
クラスは、Pydanticの pydantic.dataclasses.dataclasses
呼び出し可能クラスを使用して、データクラスとして適用されます。このプロセスは、マップされたクラスと、 MappedAsDataclass
から拡張されたミックスイン、または registry.mapped_as_dataclass()
が直接適用されたミックスインの両方で利用できます。
New in version 2.0.4: mapped_as_dataclass
のクラスとメソッドのパラメータに dataclass_callable
を追加し、Pydanticのようなより厳密なデータクラス関数に対応するためにデータクラスの内部を調整しました。
Applying ORM Mappings to an existing dataclass (legacy dataclass use)¶
Legacy Feature
ここで説明するアプローチは、SQLAlchemyの2.0シリーズで新しく追加された Declarative Dataclass Mapping 機能に置き換えられます。この機能の新しいバージョンは、このセクションで説明するバージョン1.4で最初に追加されたデータクラスサポートに基づいて構築されています。
既存のデータクラスをマップするために、SQLAlchemyの”インライン”宣言ディレクティブを直接使用することはできません。ORMディレクティブは、次の3つのテクニックのいずれかを使用して割り当てられます。:
“Declarative with Imperative Table” を使用すると、マップされるテーブル/列は、クラスの
__table__
属性に割り当てられたTable
オブジェクトを使用して定義されます。関係は__mapper_args__
辞書内で定義されます。クラスはregistry.mapped()
デコレータを使用してマップされます。以下の Mapping pre-existing dataclasses using Declarative With Imperative Table に例があります。
完全な”Declarative”を使用すると、
Column
,relationship()
のような宣言的に解釈された指示は、宣言的なプロセスによって消費される、dataclasses.field()
構文の.metadata
辞書に追加されます。クラスは再びregistry.mapped()
デコレータを使ってマップされます。以下の Mapping pre-existing dataclasses using Declarative-style fields の例を参照してください。
“Imperative” マッピングは、 Imperative Mapping で説明されているのとまったく同じ方法でマッピングを生成するために、
registry.map_imperative()
メソッドを使用して既存のデータクラスに適用できます。これは以下の: ref:orm_imperative_dataclasses で説明されています。
SQLAlchemyがデータクラスにマッピングを適用する一般的なプロセスは、通常のクラスのプロセスと同じですが、SQLAlchemyがデータクラス宣言プロセスの一部であったクラスレベルの属性を検出し、実行時にそれらを通常のSQLAlchemy ORMマップ属性に置き換えることも含まれています。データクラスによって生成されたはずの __init__
メソッドはそのまま残り、 __eq__()
や __repr__()
など、データクラスが生成する他のすべてのメソッドも同じです。
Mapping pre-existing dataclasses using Declarative With Imperative Table¶
Declarative with Imperative Table (a.k.a. Hybrid Declarative) を使った @dataclass
を使ったマッピングの例を以下に示します。完全な Table
オブジェクトは明示的に構築され、 __table__
属性に割り当てられます。インスタンスフィールドは通常のデータクラス構文を使って定義されます。 relationship()
のような追加の MapperProperty
定義は、 Mapper.properties
パラメータに対応して、 __mapper_args__ クラスレベル辞書の properties
キーの下に置かれます:
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional
from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import registry, relationship
mapper_registry = registry()
@mapper_registry.mapped
@dataclass
class User:
__table__ = Table(
"user",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
id: int = field(init=False)
name: Optional[str] = None
fullname: Optional[str] = None
nickname: Optional[str] = None
addresses: List[Address] = field(default_factory=list)
__mapper_args__ = { # type: ignore
"properties": {
"addresses": relationship("Address"),
}
}
@mapper_registry.mapped
@dataclass
class Address:
__table__ = Table(
"address",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("user_id", Integer, ForeignKey("user.id")),
Column("email_address", String(50)),
)
id: int = field(init=False)
user_id: int = field(init=False)
email_address: Optional[str] = None
上の例では、 User.id
Address.id
Address.user_id
属性は field(init=False)
として定義されています。これは、これらのパラメータが __init__()
メソッドに追加されないことを意味しますが、 Session
は、フラッシュ中にオートインクリメントやその他のデフォルト値ジェネレータから値を取得した後でも、これらを設定することができます。これらをコンストラクタで明示的に指定できるようにするには、代わりに None
のデフォルト値を与えます。
relationship()
を別々に宣言するには、 Mapper.properties
辞書の中で直接指定する必要があります。この辞書自体は、 Mapper
のコンストラクタに渡されるように、 __mapper_args__
辞書の中で指定されています。この方法に代わる方法を次の例に示します。
Warning
init=False
と一緒に default
を設定してデータクラス field()
を宣言すると、完全にプレーンなデータクラスで期待されるようには動作しません。というのも、SQLAlchemyクラスのインストルメンテーションは、データクラスの作成プロセスによってSQLAlchemyクラスのインストルメンテーションが設定したデフォルト値を置き換えるからです。代わりに default_factory
を使用してください。Declarative Dataclass Mapping を使用すると、この適応は自動的に行われます。
Mapping pre-existing dataclasses using Declarative-style fields¶
Legacy Feature
データクラスを使用した宣言的マッピングに対するこのアプローチは、レガシーと考える必要があります。これは引き続きサポートされますが、 Declarative Dataclass Mapping で詳しく説明されている新しいアプローチに対して利点を提供する可能性はありません。
mapped_column()はこの使用法ではサポートされていないことに注意してください; Column
構文は、引き続き dataclasses.field()
の metadata
フィールド内でテーブルメタデータを宣言するために使用されるべきです。
完全な宣言型のアプローチでは、 Column
オブジェクトがクラス属性として宣言されている必要があります。これは、データクラスを使用すると、データクラスレベルの属性と競合します。これらを組み合わせるアプローチは、SQLAlchemy固有のマッピング情報が提供される可能性のある dataclass.field
オブジェクトの metadata
属性を利用することです。宣言型は、クラスが属性 __sa_dataclass_metadata_key__
を指定するときに、これらのパラメータの抽出をサポートします。これはまた、 relationship()
の関連付けを示すより簡潔な方法を提供します:
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import registry, relationship
mapper_registry = registry()
@mapper_registry.mapped
@dataclass
class User:
__tablename__ = "user"
__sa_dataclass_metadata_key__ = "sa"
id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
name: str = field(default=None, metadata={"sa": Column(String(50))})
fullname: str = field(default=None, metadata={"sa": Column(String(50))})
nickname: str = field(default=None, metadata={"sa": Column(String(12))})
addresses: List[Address] = field(
default_factory=list, metadata={"sa": relationship("Address")}
)
@mapper_registry.mapped
@dataclass
class Address:
__tablename__ = "address"
__sa_dataclass_metadata_key__ = "sa"
id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
user_id: int = field(init=False, metadata={"sa": Column(ForeignKey("user.id"))})
email_address: str = field(default=None, metadata={"sa": Column(String(50))})
Using Declarative Mixins with pre-existing dataclasses¶
Composing Mapped Hierarchies with Mixins セクションでは、宣言型Mixinクラスが紹介されています。宣言型ミックスインの要件の1つは、簡単に複製できない特定の構成体を、 Mixing in Relationships の例のように、 declared_attr
デコレータを使って呼び出し可能として指定しなければならないことです:
class RefTargetMixin:
@declared_attr
def target_id(cls) -> Mapped[int]:
return mapped_column("target_id", ForeignKey("target.id"))
@declared_attr
def target(cls):
return relationship("Target")
この形式は、Dataclassesの field()
オブジェクト内でサポートされており、 field()
内のSQLAlchemy構文を示すためにラムダを使用します。ラムダを囲むために declared_attr()
を使用することはオプションです。ORMフィールドがそれ自体がデータクラスであるミックスインから来た場所の上に User
クラスを生成したい場合、形式は次のようになります:
@dataclass
class UserMixin:
__tablename__ = "user"
__sa_dataclass_metadata_key__ = "sa"
id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
addresses: List[Address] = field(
default_factory=list, metadata={"sa": lambda: relationship("Address")}
)
@dataclass
class AddressMixin:
__tablename__ = "address"
__sa_dataclass_metadata_key__ = "sa"
id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
user_id: int = field(
init=False, metadata={"sa": lambda: Column(ForeignKey("user.id"))}
)
email_address: str = field(default=None, metadata={"sa": Column(String(50))})
@mapper_registry.mapped
class User(UserMixin):
pass
@mapper_registry.mapped
class Address(AddressMixin):
pass
New in version 1.4.2: “宣言されたattr”スタイルのミックスイン属性、すなわち relationship()
構文と外部キー宣言を持つ Column
オブジェクトのサポートが追加され、”Dataclasses with Declarative Table”スタイルマッピング内で使用されるようになりました。
Mapping pre-existing dataclasses using Imperative Mapping¶
前述したように、 @dataclass
デコレータを使用してデータクラスとして設定されたクラスは、宣言型のマッピングをクラスに適用するために、 registry.mapped()
デコレータを使用してさらにデコレートすることができます。 registry.mapped()
デコレータを使用する代わりに、代わりに registry.map_mandatory()
メソッドを使用してクラスを渡すこともできます。これにより、すべての Table
および Mapper
設定をクラス変数としてクラス自体に定義するのではなく、関数に強制的に渡すことができます。:
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import field
from typing import List
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship
mapper_registry = registry()
@dataclass
class User:
id: int = field(init=False)
name: str = None
fullname: str = None
nickname: str = None
addresses: List[Address] = field(default_factory=list)
@dataclass
class Address:
id: int = field(init=False)
user_id: int = field(init=False)
email_address: str = None
metadata_obj = MetaData()
user = Table(
"user",
metadata_obj,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
address = Table(
"address",
metadata_obj,
Column("id", Integer, primary_key=True),
Column("user_id", Integer, ForeignKey("user.id")),
Column("email_address", String(50)),
)
mapper_registry.map_imperatively(
User,
user,
properties={
"addresses": relationship(Address, backref="user", order_by=address.c.id),
},
)
mapper_registry.map_imperatively(Address, address)
Mapping pre-existing dataclasses using Declarative With Imperative Table で述べたのと同じ警告が、このマッピングスタイルを使用する場合にも当てはまります。
Applying ORM mappings to an existing attrs class¶
attrs libraryは、データクラスと同様の機能を提供する一般的なサードパーティライブラリであり、通常のデータクラスにはない多くの追加機能が提供されています。
attrs で補強されたクラスは、 @define
デコレータを使用します。このデコレータは、クラスの振る舞いを定義する属性を探すためにクラスをスキャンするプロセスを開始します。この属性は、メソッド、ドキュメント、および注釈の生成に使用されます。
SQLAlchemy ORMは、 Declarative with Imperative Table または Imperative マッピングを使用したattrs_classのマッピングをサポートしています。これら2つのスタイルの一般的な形式は、データクラスで使用される Mapping pre-existing dataclasses using Declarative-style fields および Mapping pre-existing dataclasses using Declarative With Imperative Table マッピング形式と完全に同等です。この場合、データクラスまたはattrsで使用されるインライン属性ディレクティブは変更されず、SQLAlchemyのテーブル指向のインストルメンテーションが実行時に適用されます。
attrs_の @define
デコレータはデフォルトで、注釈付きクラスを新しい__slots__ベースのクラスに置き換えますが、これはサポートされていません。古いスタイルの注釈 @attr.s
または define(slots=False)
を使用している場合、クラスは置き換えられません。さらに、デコレータが実行された後、attrsはそれ自身のクラスにバインドされた属性を削除するので、SQLAlchemyのマッピングプロセスは何の問題もなくこれらの属性を引き継ぎます。 @attr.s
と @define(slots=False)
の両方のデコレータはSQLAlchemyで動作します。
Mapping attrs with Declarative “Imperative Table”¶
“Declarative with Imperative Table”スタイルでは、 Table
オブジェクトは宣言クラスのインラインで宣言されます。最初にクラスに対して、最初に @define
デコレータがクラスに適用され、次に registry.mapped()
デコレータが適用されます:
from __future__ import annotations
from typing import List
from typing import Optional
from attrs import define
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship
mapper_registry = registry()
@mapper_registry.mapped
@define(slots=False)
class User:
__table__ = Table(
"user",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("FullName", String(50), key="fullname"),
Column("nickname", String(12)),
)
id: Mapped[int]
name: Mapped[str]
fullname: Mapped[str]
nickname: Mapped[str]
addresses: Mapped[List[Address]]
__mapper_args__ = { # type: ignore
"properties": {
"addresses": relationship("Address"),
}
}
@mapper_registry.mapped
@define(slots=False)
class Address:
__table__ = Table(
"address",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("user_id", Integer, ForeignKey("user.id")),
Column("email_address", String(50)),
)
id: Mapped[int]
user_id: Mapped[int]
email_address: Mapped[Optional[str]]
Note
マップされたクラスで __slots__
を有効にする attrs
slots=True
オプションは、 attribute instrumentation を完全に実装しないと、SQLAlchemyマッピングでは使えません。マップされたクラスは通常、状態の保存のために __dict__
への直接アクセスに依存しているからです。このオプションがある場合の動作は定義されていません。
Mapping attrs with Imperative Mapping¶
データクラスの場合と同じように、 registry.map_mandatory()
を使って既存の attrs
クラスをマップすることもできます:
from __future__ import annotations
from typing import List
from attrs import define
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship
mapper_registry = registry()
@define(slots=False)
class User:
id: int
name: str
fullname: str
nickname: str
addresses: List[Address]
@define(slots=False)
class Address:
id: int
user_id: int
email_address: Optional[str]
metadata_obj = MetaData()
user = Table(
"user",
metadata_obj,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
address = Table(
"address",
metadata_obj,
Column("id", Integer, primary_key=True),
Column("user_id", Integer, ForeignKey("user.id")),
Column("email_address", String(50)),
)
mapper_registry.map_imperatively(
User,
user,
properties={
"addresses": relationship(Address, backref="user", order_by=address.c.id),
},
)
mapper_registry.map_imperatively(Address, address)
上記の形式は、前の例でDeclarative with Imperative Tableを使用した場合と同じです。