Composing Mapped Hierarchies with Mixins

Declarative スタイルを使用してクラスをマッピングする際の一般的なニーズは、特定の列、テーブルまたはマッパーのオプション、名前付けスキーム、またはその他のマッピングされたプロパティなどの共通の機能を多くのクラスで共有することです。宣言型マッピングを使用する場合、このイディオムは mixin classes を使用することによって、また宣言型基底クラス自体を拡張することによってサポートされます。

..tip:: mixinクラスに加えて、共通の列オプションは PEP 593 Annotated 型を使って多くのクラスで共有することもできます。これらのSQLAlchemy 2.0機能の背景については Mapping Multiple Type Configurations to Python TypesMapping Whole Column Declarations to Python Types を参照してください。

一般的に混在するイディオムの例を以下に示します。:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class CommonMixin:
    """define a series of common elements that may be applied to mapped
    classes using this class as a mixin class."""

    @declared_attr.directive
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

    __table_args__ = {"mysql_engine": "InnoDB"}
    __mapper_args__ = {"eager_defaults": True}

    id: Mapped[int] = mapped_column(primary_key=True)

class HasLogRecord:
    """mark classes that have a many-to-one relationship to the
    ``LogRecord`` class."""

    log_record_id: Mapped[int] = mapped_column(ForeignKey("logrecord.id"))

    @declared_attr
    def log_record(self) -> Mapped["LogRecord"]:
        return relationship("LogRecord")

class LogRecord(CommonMixin, Base):
    log_info: Mapped[str]

class MyModel(CommonMixin, HasLogRecord, Base):
    name: Mapped[str]

上の例は、ベースに2つのミックスイン CommonMixinHasLogRecord を含むクラス MyModel と、同じく CommonMixin を含む補助クラス`LogRecord`を示しており、ミックスインとベースクラスでサポートされている以下のようなさまざまな構成を示しています。

  • mapped_column()Mapped または Column を使って宣言された列は、ミックスインまたは基本クラスから、マップされるターゲットクラスにコピーされます。上の例では、列属性 CommonMixin.idHasLogRecord.log_record_id を使って示されています。

  • __table_args____mapper_args__ のような宣言的な指示子は、ミックスインやベースクラスに割り当てることができ、ミックスインやベースから継承するすべてのクラスに対して自動的に有効になります。上の例では、 __table_args____mapper_args__ 属性を使ってこれを説明しています。

  • すべての宣言型ディレクティブは、 __tablename____table____table_args____mapper_args__ を含め、 declared_attr デコレータ(具体的には declared_attr.directive サブメンバー、これについては後ほど詳しく説明します)で修飾されたユーザ定義のクラスメソッドを使って実装できます。上では、 Table 名を動的に生成する def__tablename__(cls) クラスメソッドを使って説明しました。このメソッドが MyModel クラスに適用されると、テーブル名は MyModel として生成され、 LogRecord クラスに適用されると、テーブル名は LogRecord として生成されます。

  • relationship() のような他のORMプロパティは、同じく declared_attr デコレータでデコレートされたユーザ定義のクラスメソッドを使って、マップされる対象のクラスで生成できます。上では、これを LogRecord と呼ばれるマップされたオブジェクトに対して多対1の relationship() を生成することで説明しました。

上記の機能はすべて、 select() の例を使って説明することができます。

>>> from sqlalchemy import select
>>> print(select(MyModel).join(MyModel.log_record))
SELECT mymodel.name, mymodel.id, mymodel.log_record_id FROM mymodel JOIN logrecord ON logrecord.id = mymodel.log_record_id

Tip

declared_attr の例は、それぞれのメソッドの例に対して正しい PEP 484 アノテーションを説明しようとします。 declared_attr 関数でのアノテーションの使用は 完全にオプション であり、Declarativeでは使用されません。しかし、これらのアノテーションはMypyの --strict 型チェックに合格するために必要です。

Additionally, the declared_attr.directive sub-member illustrated above is optional as well, and is only significant for PEP 484 typing tools, as it adjusts for the expected return type when creating methods to override Declarative directives such as __tablename__, __mapper_args__ and __table_args__.

さらに、上で説明した declared_attr.directive サブメンバーもオプションであり、 PEP 484 型定義ツールでのみ重要です。これは、 __tablename____mapper_args____table_args__ などの宣言型ディレクティブをオーバーライドするメソッドを作成するときに、期待される戻り値の型を調整するためです。

New in version 2.0: SQLAlchemy ORMの PEP 484 型定義サポートの一部として、 declared_attr 属性と宣言型設定属性を区別するために、 declared_attr.directivedeclared_attr に追加されました。

ミックスインとベース・クラスの順序には、決まった規則はありません。通常のPythonメソッドの解決規則が適用され、上記の例は以下と同様に機能します。:

class MyModel(Base, HasLogRecord, CommonMixin):
    name: Mapped[str] = mapped_column()

これがうまくいくのは、ここでは BaseCommonMixinHasLogRecord が定義する変数、つまり __tablename____table_args__id などを定義していないからです。 Base が同じ名前の属性を定義している場合、継承リストの最初に置かれたクラスが、新しく定義されたクラスで使用される属性を決定します。

Tip

上記の例では、 Mapped アノテーションクラスに基づいた Annotated Declarative Table 形式を使用していますが、mixinクラスは、 mapped_column() の代わりに Column を直接使用する場合のように、アノテーションのないレガシーな宣言形式でも完璧に動作します。

Changed in version 2.0: SQLAlchemyの1.4シリーズから来たユーザで、 mypy plugin を使っていた可能性のある人は、mypyプラグインがもう使われていなければ、宣言型ミックスインをマークするための declarative_mixin() クラスデコレータはもう必要ありません。

Augmenting the Base

純粋なミックスインを使用することに加えて、このセクションのほとんどのテクニックは、特定のベースから派生したすべてのクラスに適用されるべきパターンのために、ベースクラスに直接適用することもできます。以下の例は、前のセクションの例のいくつかを Base クラスの観点から示しています。:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    """define a series of common elements that may be applied to mapped
    classes using this class as a base class."""

    @declared_attr.directive
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

    __table_args__ = {"mysql_engine": "InnoDB"}
    __mapper_args__ = {"eager_defaults": True}

    id: Mapped[int] = mapped_column(primary_key=True)

class HasLogRecord:
    """mark classes that have a many-to-one relationship to the
    ``LogRecord`` class."""

    log_record_id: Mapped[int] = mapped_column(ForeignKey("logrecord.id"))

    @declared_attr
    def log_record(self) -> Mapped["LogRecord"]:
        return relationship("LogRecord")

class LogRecord(Base):
    log_info: Mapped[str]

class MyModel(HasLogRecord, Base):
    name: Mapped[str]

上記の場合、 MyModelLogRecord も、 Base から派生して、クラス名から派生したテーブル名、 id という名前の主キー列、および Base.__table_args__Base.__mapper_args__ で定義された上記のテーブルとマッパー引数を持ちます。

レガシーの declarative_base() または registry.generate_base() を使用する場合、以下の注釈のない例に示すように、 declarative_base.cls パラメータを次のように使用して同等の効果を生成することができます。:

# legacy declarative_base() use

from sqlalchemy import Integer, String
from sqlalchemy import ForeignKey
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base:
    """define a series of common elements that may be applied to mapped
    classes using this class as a base class."""

    @declared_attr.directive
    def __tablename__(cls):
        return cls.__name__.lower()

    __table_args__ = {"mysql_engine": "InnoDB"}
    __mapper_args__ = {"eager_defaults": True}

    id = mapped_column(Integer, primary_key=True)

Base = declarative_base(cls=Base)

class HasLogRecord:
    """mark classes that have a many-to-one relationship to the
    ``LogRecord`` class."""

    log_record_id = mapped_column(ForeignKey("logrecord.id"))

    @declared_attr
    def log_record(self):
        return relationship("LogRecord")

class LogRecord(Base):
    log_info = mapped_column(String)

class MyModel(HasLogRecord, Base):
    name = mapped_column(String)

Mixing in Columns

Declarative table スタイルの設定が( imperative table 設定とは対照的に)使用されていることを前提に、mixinで列を指定することができます。これにより、mixinで宣言された列をコピーして、宣言プロセスが生成する Table の一部にすることができます。 mapped_column()MappedColumn の3つの構成体はすべて、宣言型mixinでインライン宣言することができます:

class TimestampMixin:
    created_at: Mapped[datetime] = mapped_column(default=func.now())
    updated_at: Mapped[datetime]

class MyModel(TimestampMixin, Base):
    __tablename__ = "test"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]

上記の場合、クラスベースに TimestampMixin を含むすべての宣言型クラスは、すべての行挿入にタイムスタンプを適用する created_at 列と、例の目的のためにデフォルトを含まない updated_at 列を自動的に含みます。 (if it did, we would use the Column.onupdate parameter

これは mapped_column() で受け付けられます)。これらの列構成体は常に 元のミックスインまたは基底クラス からコピーされるので、同じミックスイン/基底クラスを、それぞれが独自の列構成体を持つ任意の数のターゲットクラスに適用できます。

mixinでは、次のようなすべての宣言型列フォームがサポートされています。:

  • 注釈付き属性 - mapped_column() がある場合とない場合:

    class TimestampMixin:
        created_at: Mapped[datetime] = mapped_column(default=func.now())
        updated_at: Mapped[datetime]
  • mapped_column- Mapped がある場合とない場合:

    class TimestampMixin:
        created_at = mapped_column(default=func.now())
        updated_at: Mapped[datetime] = mapped_column()
  • - 従来の宣言形式:

    class TimestampMixin:
        created_at = Column(DateTime, default=func.now())
        updated_at = Column(DateTime)

上記の各フォームでは、Declarativeが構文の コピー を作成し、ターゲット・クラスに適用することによって、mixinクラスの列ベースの属性を処理します。

Changed in version 2.0: 宣言型APIは、 declared_attr() を使わなくても、mixinを使うときに Column オブジェクトや mapped_column() 構造体を扱うことができるようになりました。 ForeignKey 要素を持つ列をmixinで直接使うことを妨げていた以前の制限が取り除かれました。

Mixing in Relationships

relationship() で作成された関係は、 declared_attr アプローチを排他的に使用した宣言的なmixinクラスで提供され、関係とその列にバインドされた内容をコピーするときに発生する可能性のあるあいまいさを排除します。以下は外部キー列と関係を組み合わせた例で、2つのクラス FooBar の両方が多対1で共通のターゲットクラスを参照するように設定できます。:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class RefTargetMixin:
    target_id: Mapped[int] = mapped_column(ForeignKey("target.id"))

    @declared_attr
    def target(cls) -> Mapped["Target"]:
        return relationship("Target")

class Foo(RefTargetMixin, Base):
    __tablename__ = "foo"
    id: Mapped[int] = mapped_column(primary_key=True)

class Bar(RefTargetMixin, Base):
    __tablename__ = "bar"
    id: Mapped[int] = mapped_column(primary_key=True)

class Target(Base):
    __tablename__ = "target"
    id: Mapped[int] = mapped_column(primary_key=True)

上記のマッピングでは、 FooBar のそれぞれに、 .target 属性に沿ってアクセスされる Target との関係が含まれています。

>>> from sqlalchemy import select
>>> print(select(Foo).join(Foo.target))
SELECT foo.id, foo.target_id FROM foo JOIN target ON target.id = foo.target_id
>>> print(select(Bar).join(Bar.target))
SELECT bar.id, bar.target_id FROM bar JOIN target ON target.id = bar.target_id

relationship.primaryjoin のような特別な引数も混合クラスのメソッド内で使うことができます。これはしばしばマップされているクラスを参照する必要があります。ローカルにマップされた列を参照する必要があるスキームの場合、通常これらの列はDeclarativeによってマップされたクラスの属性として利用可能になります。この属性はデコレートされたクラスのメソッドに cls 引数として渡されます。この機能を使うと、例えば RefTargetMixin.target メソッドを、 Targetcls の両方で保留中のマップされた列を参照する明示的なプライマリ結合を使って書き直すことができます。:

class Target(Base):
    __tablename__ = "target"
    id: Mapped[int] = mapped_column(primary_key=True)

class RefTargetMixin:
    target_id: Mapped[int] = mapped_column(ForeignKey("target.id"))

    @declared_attr
    def target(cls) -> Mapped["Target"]:
        # illustrates explicit 'primaryjoin' argument
        return relationship("Target", primaryjoin=Target.id == cls.target_id)

Mixing in column_property() and other MapperProperty classes

relationship() と同様に、 column_property() のような他の MapperProperty サブクラスも、ミックスインで使用されるときに生成されたクラスローカルなコピーを持つ必要があるので、 declared_attr で修飾された関数内でも宣言されます。この関数内では、 mapped_column()Mapped 、または Column で宣言された他の通常のマップされた列が、新しい属性を構成するために使用できるように、以下の例のように2つの列を一緒に追加するように、 cls 引数から利用できるようになります。:

from sqlalchemy.orm import column_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class SomethingMixin:
    x: Mapped[int]
    y: Mapped[int]

    @declared_attr
    def x_plus_y(cls) -> Mapped[int]:
        return column_property(cls.x + cls.y)

class Something(SomethingMixin, Base):
    __tablename__ = "something"

    id: Mapped[int] = mapped_column(primary_key=True)

上記では、完全な式を生成する文で Something.x_plus_y を使用することができます。

>>> from sqlalchemy import select
>>> print(select(Something.x_plus_y))
SELECT something.x + something.y AS anon_1 FROM something

Tip

declared_attr デコレータは、デコレートされた呼び出し可能オブジェクトをクラスメソッドとまったく同じように動作させます。しかし、Pylance_のような型定義ツールはこれを認識できない可能性があり、関数の本体内の変数 cls へのアクセスに関して警告を発することがあります。この問題が発生したときに解決するために、 @classmethod デコレータを declared_attr と次のように直接組み合わせることができます:

class SomethingMixin:
    x: Mapped[int]
    y: Mapped[int]

    @declared_attr
    @classmethod
    def x_plus_y(cls) -> Mapped[int]:
        return column_property(cls.x + cls.y)

New in version 2.0: - declared_attr can accommodate a function decorated with @classmethod to help with PEP 484 integration where needed.

New in version 2.0: - declared_attr は必要に応じて PEP 484 との統合を支援するために、 @classmethod で修飾された関数に対応できます。

Using Mixins and Base Classes with Mapped Inheritance Patterns

Mapping Class Inheritance Hierarchies で説明されているように、マッパーの継承パターンを扱う場合、 declared_attr をミックスインクラスで使用したり、クラス階層内のマップされたスーパークラスとマップされていないスーパークラスの両方を拡張したりするときに、いくつかの追加機能があります。

declared_attr で修飾された関数をミックスインや基本クラスで定義し、マップされた継承階層のサブクラスで解釈されるようにする場合、Declarativeで使用される特別な名前を生成する関数(例えば __tablename____mapper_args__ )と、通常のマップされた属性を生成する関数(例えば mapped_column()relationship() )との間には重要な違いがあります。 Declarative directive を定義する関数は**階層内の各サブクラスに対して 呼び出されます 。一方、 マップされた属性 を生成する関数は 階層内の最初にマップされたスーパークラスに対してのみ 呼び出されます。

この動作の違いの理論的根拠は、マップされたプロパティはすでにクラスによって継承可能であるという事実に基づいています。たとえば、スーパークラスのマップされたテーブルの特定の列は、サブクラスの列にも複製されるべきではありません。一方、特定のクラスまたはそのマップされたテーブルに固有の要素(ローカルにマップされたテーブルの名前など)は継承できません。

この2つのユースケースの動作の違いを、次の2つのセクションで説明します。

Using declared_attr() with inheriting Table and Mapper arguments

ミックスインの一般的な方法は、マップされた Table の名前を動的に生成する def__tablename__(cls) 関数を作成することです。

このレシピは、以下の例のように、継承するマッパー階層のテーブル名を生成するために使用できます。この例では、クラス名に基づいてすべてのクラスに単純なテーブル名を与えるミックスインを作成します。以下のレシピでは、マップされたクラス PersonPerson のサブクラス Engineer に対してテーブル名が生成されますが、 Person のサブクラス Manager に対しては生成されません。:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class Tablename:
    @declared_attr.directive
    def __tablename__(cls) -> Optional[str]:
        return cls.__name__.lower()

class Person(Tablename, Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    id: Mapped[int] = mapped_column(ForeignKey("person.id"), primary_key=True)

    primary_language: Mapped[str]

    __mapper_args__ = {"polymorphic_identity": "engineer"}

class Manager(Person):
    @declared_attr.directive
    def __tablename__(cls) -> Optional[str]:
        """override __tablename__ so that Manager is single-inheritance to Person"""

        return None

    __mapper_args__ = {"polymorphic_identity": "manager"}

上の例では、 Person 基本クラスと、新しいテーブル名を生成する Tablename mixinクラスのサブクラスである Engineer クラスの両方が、生成された __tablename__ 属性を持ちます。これは、Declarativeに対して、各クラスが生成された独自の Table にマップされることを示します。 Engineer サブクラスの場合、適用される継承のスタイルは joined table inheritance です。これは、ベースの person テーブルに結合する engineer テーブルにマッピングされるためです。 Person から継承する他のサブクラスも、デフォルトでこのスタイルの継承が適用されます(この特定の例では、それぞれが主キー列を指定する必要があります。これについては次のセクションで詳しく説明します)。

対照的に、 PersonManager サブクラスは、 None を返すために __tablename__ クラスメソッドを上書きします。これはDeclarativeに対して、このクラスは生成された Table を持つべきでは**なく**、代わりに Person がマップされたベースの Table だけを利用することを示します。 Manager サブクラスに適用される継承のスタイルは single table inheritance です。

上の例は、それぞれのマップされたクラスがどの Table にマップされるのか、あるいは継承するスーパークラス’ Table に自身をマップするのかを記述する必要があるので、 __tablename__ のような宣言的な指示が、それぞれのサブクラスに 個別に 適用されることを示しています。

代わりに、上記のデフォルトのテーブルスキームを 反転 して、単一のテーブル継承をデフォルトとし、それを上書きするために __tablename__ ディレクティブが指定された場合にのみ結合されたテーブル継承を定義できるようにしたい場合は、最上位の __tablename__() メソッド内で宣言型ヘルパーを使用できます。この場合、ヘルパーは has_inherited_table() と呼ばれます。この関数は、スーパークラスがすでに Table にマップされている場合は True を返します。このヘルパーを最も基本的な __tablename__() クラスメソッド内で使用すると、テーブルがすでに存在している場合に、テーブル名に対して None条件付きで 返すことができ、デフォルトでサブクラスを継承するための単一テーブル継承を示すことができます。:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import has_inherited_table
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class Tablename:
    @declared_attr.directive
    def __tablename__(cls):
        if has_inherited_table(cls):
            return None
        return cls.__name__.lower()

class Person(Tablename, Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    @declared_attr.directive
    def __tablename__(cls):
        """override __tablename__ so that Engineer is joined-inheritance to Person"""

        return cls.__name__.lower()

    id: Mapped[int] = mapped_column(ForeignKey("person.id"), primary_key=True)

    primary_language: Mapped[str]

    __mapper_args__ = {"polymorphic_identity": "engineer"}

class Manager(Person):
    __mapper_args__ = {"polymorphic_identity": "manager"}

Using declared_attr() to generate table-specific inheriting columns

declared_attr と一緒に使用された場合、 `__tablename__` やその他の特殊な名前がどのように処理されるかとは対照的に、列とプロパティ(例えば、関係、列のプロパティなど)が混在する場合、 declared_attr ディレクティブが declared_attr.cascading サブディレクティブと一緒に使用されない限り、関数は階層内の 基底クラスのみ に対して呼び出されます。以下では、 Person クラスのみが id という列を受け取ります。主キーが与えられていない Engineer ではマッピングが失敗します。:

class HasId:
    id: Mapped[int] = mapped_column(primary_key=True)

class Person(HasId, Base):
    __tablename__ = "person"

    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

# this mapping will fail, as there's no primary key
class Engineer(Person):
    __tablename__ = "engineer"

    primary_language: Mapped[str]
    __mapper_args__ = {"polymorphic_identity": "engineer"}

通常、結合テーブルの継承では、各サブクラスに明確な名前の列が必要になります。ただし、この場合は、すべてのテーブルに id 列を作成し、外部キーを介して互いを参照させることができます。これは、 declared_attr.cascading 修飾子を使用してミックスインとして実現できます。これは、関数が **階層内の各クラス**に対して 、 いつも 呼び出されることを示します。

(以下の警告を参照してください) これは __tablename__ の場合と同じです:

class HasIdMixin:
    @declared_attr.cascading
    def id(cls) -> Mapped[int]:
        if has_inherited_table(cls):
            return mapped_column(ForeignKey("person.id"), primary_key=True)
        else:
            return mapped_column(Integer, primary_key=True)

class Person(HasIdMixin, Base):
    __tablename__ = "person"

    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    __tablename__ = "engineer"

    primary_language: Mapped[str]
    __mapper_args__ = {"polymorphic_identity": "engineer"}

Warning

declared_attr.cascading 機能は現在、サブクラスが異なる関数や値で属性をオーバーライドすることを 許可していません 。これは現在のところ、 @declared_attr の解決方法の制限であり、この状態が検出された場合に警告が表示されます。この制限は、ORMマップされた列、関係、およびその他の :class`.MapperProperty` スタイルの属性にのみ適用されます。 declared_attr.cascading とは内部的に異なる方法で解決される __tablename____mapper_args__ などの宣言型ディレクティブには 適用されません

Combining Table/Mapper Arguments from Multiple Mixins

宣言的なmixinで指定された __table_args____mapper_args__ の場合、いくつかのmixinのいくつかのパラメータを、そのクラス自体で定義したいパラメータと組み合わせることができます。 declared_attr デコレータをここで使用して、複数のコレクションからプルするユーザ定義の照合ルーチンを作成できます:

from sqlalchemy.orm import declarative_mixin, declared_attr

class MySQLSettings:
    __table_args__ = {"mysql_engine": "InnoDB"}

class MyOtherMixin:
    __table_args__ = {"info": "foo"}

class MyModel(MySQLSettings, MyOtherMixin, Base):
    __tablename__ = "my_model"

    @declared_attr.directive
    def __table_args__(cls):
        args = dict()
        args.update(MySQLSettings.__table_args__)
        args.update(MyOtherMixin.__table_args__)
        return args

    id = mapped_column(Integer, primary_key=True)

Creating Indexes and Constraints with Naming Conventions on Mixins

IndexUniqueConstraintCheckConstraint などの名前付き制約を使用する場合、各オブジェクトはミックスインから派生した特定のテーブルに対して一意であるため、実際にマップされたクラスごとに各オブジェクトの個々のインスタンスを作成する必要があります。

簡単な例として、ミックスインから派生したすべてのテーブルに適用される、名前付きで、複数列の可能性がある Index を定義するには、 Index の”インライン”形式を使用して、それを __table_args__ の一部として確立します。 declared_attr を使用して、各サブクラスに対して呼び出されるクラスメソッドとして __table_args__() を確立します。:

class MyMixin:
    a = mapped_column(Integer)
    b = mapped_column(Integer)

    @declared_attr.directive
    def __table_args__(cls):
        return (Index(f"test_idx_{cls.__tablename__}", "a", "b"),)

class MyModelA(MyMixin, Base):
    __tablename__ = "table_a"
    id = mapped_column(Integer, primary_key=True)

class MyModelB(MyMixin, Base):
    __tablename__ = "table_b"
    id = mapped_column(Integer, primary_key=True)

上の例では、2つのテーブル table_atable_b が、インデックス test_idx_table_atest_idx_table_b を使って生成されます。

通常、最近のSQLAlchemyでは、 Configuring Constraint Naming Conventions で説明されているような命名規則を使用します。命名規則は、新しい Constraint オブジェクトが作成されるときに MetaData.naming_convention を使用して自動的に行われますが、この規則は特定の Constraint の親 Table に基づいてオブジェクト構築時に適用されるため、独自の Table を持つ継承サブクラスごとに個別の Constraint オブジェクトを作成する必要があります。この場合も、 declared_attr__table_args__()` とともに使用します。以下では、抽象マップベースを使用して説明します。:

from uuid import UUID

from sqlalchemy import CheckConstraint
from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

constraint_naming_conventions = {
    "ix": "ix_%(column_0_label)s",
    "uq": "uq_%(table_name)s_%(column_0_name)s",
    "ck": "ck_%(table_name)s_%(constraint_name)s",
    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s",
}

class Base(DeclarativeBase):
    metadata = MetaData(naming_convention=constraint_naming_conventions)

class MyAbstractBase(Base):
    __abstract__ = True

    @declared_attr.directive
    def __table_args__(cls):
        return (
            UniqueConstraint("uuid"),
            CheckConstraint("x > 0 OR y < 100", name="xy_chk"),
        )

    id: Mapped[int] = mapped_column(primary_key=True)
    uuid: Mapped[UUID]
    x: Mapped[int]
    y: Mapped[int]

class ModelAlpha(MyAbstractBase):
    __tablename__ = "alpha"

class ModelBeta(MyAbstractBase):
    __tablename__ = "beta"

上のマッピングでは、プライマリ・キー、CHECK制約、一意性制約を含むすべての制約のテーブル固有の名前を含むDDLが生成されます。:

CREATE TABLE alpha (
    id INTEGER NOT NULL,
    uuid CHAR(32) NOT NULL,
    x INTEGER NOT NULL,
    y INTEGER NOT NULL,
    CONSTRAINT pk_alpha PRIMARY KEY (id),
    CONSTRAINT uq_alpha_uuid UNIQUE (uuid),
    CONSTRAINT ck_alpha_xy_chk CHECK (x > 0 OR y < 100)
)

CREATE TABLE beta (
    id INTEGER NOT NULL,
    uuid CHAR(32) NOT NULL,
    x INTEGER NOT NULL,
    y INTEGER NOT NULL,
    CONSTRAINT pk_beta PRIMARY KEY (id),
    CONSTRAINT uq_beta_uuid UNIQUE (uuid),
    CONSTRAINT ck_beta_xy_chk CHECK (x > 0 OR y < 100)
)