Mapping Class Inheritance Hierarchies

SQLAlchemyは、次の3つの形式の継承をサポートしています。

  • 単一テーブルの継承 - いくつかのタイプのクラスが単一テーブルで表されます。

  • 具象テーブル継承 - クラスの各タイプは、独立したテーブルによって表されます。

  • 結合テーブルの継承 - クラス階層は依存テーブル間で分割されます。各クラスは、そのクラスに対してローカルな属性のみを含む独自のテーブルによって表されます。

継承の最も一般的な形式は、単一のテーブルと結合されたテーブルですが、具体的な継承では、より多くの設定上の課題があります。

マッパーが継承関係で設定されている場合、SQLAlchemyは polymorphically 要素をロードすることができます。これは、1つのクエリが複数の型のオブジェクトを返すことができることを意味します。

See also

Writing SELECT statements for Inheritance Mappings - ORM Querying Guide を参照してください。

Inheritance Mapping Recipes - 結合継承、単一継承、具象継承の完全な例

Joined Table Inheritance

結合された表の継承では、クラスの階層に沿った各クラスは個別の表によって表されます。階層内の特定のサブクラスの問合せは、その継承パス内のすべての表に沿ってSQL JOINとしてレンダリングされます。問合せ対象のクラスが基本クラスの場合は、他の表を同時に含めるか、サブ表に固有の属性を後でロードできるようにするオプションを使用して、ベース表がかわりに問合せられます。

いずれの場合も、与えられた行をインスタンス化する最終的なクラスは、基底クラスに定義された discriminator 列またはSQL式によって決定されます。これは、特定のサブクラスに関連付けられたスカラー値を生成します。

結合された継承階層の基底クラスは、多様識別子列を示す追加の引数と、オプションで基底クラス自身の多様識別子で設定されます:

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

class Base(DeclarativeBase):
    pass

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }

    def __repr__(self):
        return f"{self.__class__.__name__}({self.name!r})"

上の例では、識別子は Mapper.polymorphic_on パラメータを使って設定された type 列です。このパラメータは、使用するマップされた属性の文字列名として、または Columnmapped_column() 構文のような列式オブジェクトとして指定された、列指向の式を受け付けます。

discriminator列には、行内で表現されるオブジェクトの型を示す値が格納されます。列のデータ型は任意ですが、文字列と整数が最も一般的です。データベース内の特定の行に対してこの列に適用される実際のデータ値は、以下で説明する Mapper.polymorphic_identity パラメータを使用して指定されます。

多相識別子式は厳密には必要ありませんが、多相ロードが必要な場合は必要です。これを実現する最も簡単な方法は、実表に列を設定することです。ただし、非常に高度な継承マッピングでは、CASE式などのSQL式を多相識別子として使用できます。

Note

現在、 継承階層全体に対して1つの識別子列またはSQL式のみを設定できます 。通常は、階層内の最も基本的なクラスに設定されます。”カスケード”多様識別子式はまだサポートされていません。

次に、 EmployeeEngineerManager のサブクラスを定義します。それぞれのサブクラスには、そのサブクラスに固有の属性を表す列が含まれています。各テーブルには、親テーブルへの外部キー参照だけでなく、主キー列も含まれている必要があります。:

class Engineer(Employee):
    __tablename__ = "engineer"
    id: Mapped[int] = mapped_column(ForeignKey("employee.id"), primary_key=True)
    engineer_name: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

class Manager(Employee):
    __tablename__ = "manager"
    id: Mapped[int] = mapped_column(ForeignKey("employee.id"), primary_key=True)
    manager_name: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }

上記の例では、各マッピングはそのマッパー引数内で Mapper.polymorphic_identity パラメータを指定します。この値は、ベースマッパーで確立された Mapper.polymorphic_on パラメータで指定された列に入力されます。 Mapper.polymorphic_identity パラメータは、階層全体にわたってマップされた各クラスに対して一意である必要があり、マップされたクラスごとに1つの”ID”のみである必要があります。前述のように、一部のサブクラスが2番目のIDを導入する”カスケード”IDはサポートされていません。

ORMは Mapper.polymorphic_identity で設定された値を使用して、行を多様にロードするときに行がどのクラスに属するかを決定します。上の例では、 Employee を表すすべての行は、その type 列に employee という値を持ちます。同様に、すべての Engineerengineer という値を持ち、それぞれの Managermanager という値を持ちます。継承マッピングが、結合テーブル継承のようにサブクラスに対して個別の結合テーブルを使用するか、単一テーブル継承のようにすべて1つのテーブルを使用するかにかかわらず、この値は永続化され、問い合わせ時にORMが利用できることが期待されます。 Mapper.polymorphic_identity パラメータも具象テーブル継承に適用されますが、実際には永続化されません。詳細については、後のセクションの Concrete Table Inheritance を参照してください。

多様型の設定では、プライマリ・キー自体と同じカラムに外部キー制約を設定することが最も一般的ですが、これは必須ではありません。プライマリ・キーとは別のカラムが、外部キーを介して親を参照するようにすることもできます。ベース・テーブルからサブクラスへのJOINを構築する方法も直接カスタマイズできますが、これが必要になることはほとんどありません。

結合された継承マッピングが完了すると、 Employee に対してクエリを実行すると、 EmployeeEngineerManager の各オブジェクトの組み合わせが返されます。新しく保存された EngineerManagerEmployee の各オブジェクトは、自動的に employee.type 列に正しい discriminator 値を設定します。この場合は、 engineermanageremployee のいずれかになります。

Relationships with Joined Inheritance

結合されたテーブルの継承では、リレーションシップは完全にサポートされています。結合された継承クラスを含むリレーションシップは、外部キー制約にも対応する階層内のクラスをターゲットにする必要があります。以下では、 employee テーブルには company テーブルに戻る外部キー制約があるので、リレーションシップは CompanyEmployee の間に設定されます。:

from __future__ import annotations

from sqlalchemy.orm import relationship

class Company(Base):
    __tablename__ = "company"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    employees: Mapped[List[Employee]] = relationship(back_populates="company")

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]
    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))
    company: Mapped[Company] = relationship(back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }

class Manager(Employee): ...

class Engineer(Employee): ...

外部キー制約がサブクラスに対応するテーブル上にある場合、リレーションシップはそのサブクラスを対象とする必要があります。以下の例では、 manager から company への外部キー制約があるので、 Manager クラスと Company クラスの間にリレーションシップが確立されます。:

class Company(Base):
    __tablename__ = "company"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    managers: Mapped[List[Manager]] = relationship(back_populates="company")

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }

class Manager(Employee):
    __tablename__ = "manager"
    id: Mapped[int] = mapped_column(ForeignKey("employee.id"), primary_key=True)
    manager_name: Mapped[str]

    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))
    company: Mapped[Company] = relationship(back_populates="managers")

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }

class Engineer(Employee): ...

上記では、 Manager クラスは Manager.company 属性を持ち、 Company クラスは employee テーブルと manager テーブルの結合に対して常にロードされる Company.managers 属性を持ちます。

Loading Joined Inheritance Mappings

Writing SELECT statements for Inheritance Mappings を参照して、マッパーの設定時とクエリ時の両方でクエリされるテーブルの設定を含む、継承の読み込みテクニックの背景を調べてください。

Single Table Inheritance

単一表の継承は、単一表内のすべてのサブクラスのすべての属性を表します。そのクラスに一意の属性を持つ特定のサブクラスは、行が別の種類のオブジェクトを参照している場合にNULLとなる表内の列内にその属性を保持します。

階層内の特定のサブクラスに対してクエリを実行すると、ベース・テーブルに対するSELECTとして表示されます。ベース・テーブルには、識別子のカラムまたは式に存在する特定の値を持つローに制限するWHERE句が含まれます。

単一テーブルの継承は、結合テーブルの継承と比較して単純であるという利点があります。クエリは、表現されたすべてのクラスのオブジェクトをロードするために1つのテーブルのみを使用する必要があるため、はるかに効率的です。

単一テーブル継承の設定は、ベースクラスが __tablename__ と指定されていることを除けば、結合テーブル継承とよく似ています。クラスを区別できるように、ベーステーブルには識別子列も必要です。

サブクラスはすべての属性についてベーステーブルを共有しますが、Declarativeを使用する場合、 mapped_column オブジェクトがサブクラスで指定されている可能性があります。これは、列がそのサブクラスにのみマップされることを示します。 mapped_column は同じベース Table オブジェクトに適用されます:

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_on": "type",
        "polymorphic_identity": "employee",
    }

class Manager(Employee):
    manager_data: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }

class Engineer(Employee):
    engineer_info: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

派生クラスManagerとEngineerのマッパーでは、独自のマップされたテーブルがないことを示すために、 __tablename__ が省略されていることに注意してください。さらに、 nullable=True を指定した mapped_column() ディレクティブが含まれています。これらのクラスに対して宣言されたPython型には Optional[] が含まれていないため、列は通常 NOT NULL としてマップされますが、この列はその特定のサブクラスに対応する行に対してのみ入力されることを期待しているため、これは適切ではありません。

Resolving Column Conflicts with use_existing_column

前のセクションで、 manager_name 列と engineer_info 列が、それ自身のテーブルを持たないサブクラスで宣言された結果、 Employee.__table__ に適用されるために 上に移動 されることに注意してください。以下のように、2つのサブクラスが*同じ*列を指定しようとすると、厄介なケースが発生します。:

from datetime import datetime

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_on": "type",
        "polymorphic_identity": "employee",
    }

class Engineer(Employee):
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }
    start_date: Mapped[datetime] = mapped_column(nullable=True)

class Manager(Employee):
    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }
    start_date: Mapped[datetime] = mapped_column(nullable=True)

上記では、 EngineerManager の両方で宣言された start_date 列はエラーになります。

sqlalchemy.exc.ArgumentError: Column 'start_date' on class Manager conflicts
with existing column 'employee.start_date'.  If using Declarative,
consider using the use_existing_column parameter of mapped_column() to
resolve conflicts.

上記のシナリオは宣言型マッピングシステムに曖昧さを与えますが、これは mapped_column()mapped_column.use_existing_column パラメータを使用することで解決できます。これは mapped_column() に、継承するスーパークラスが存在するかどうかを調べ、すでにマップされている列が存在する場合はそれを使用し、存在しない場合は新しい列をマップするように指示します。:

from sqlalchemy import DateTime

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_on": "type",
        "polymorphic_identity": "employee",
    }

class Engineer(Employee):
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

    start_date: Mapped[datetime] = mapped_column(
        nullable=True, use_existing_column=True
    )

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

    start_date: Mapped[datetime] = mapped_column(
        nullable=True, use_existing_column=True
    )

上の例では、 Manager がマップされている場合、 start_date 列は Employee クラスにすでに存在しており、 Engineer マッピングによってすでに提供されています。 mapped_column.use_existing_column パラメータは、 mapped_column() に対して、要求された Column がマップされた Table 上で Employee のために最初に検索され、存在する場合はその既存のマッピングが維持されることを示します。存在しない場合、 mapped_column() は通常通りその列をマップし、 Employee スーパークラスが参照する Table 内の列の1つとして追加します。

New in version 2.0.0b4: - mapped_column.use_existing_column が追加されました。これは、継承するサブクラスの列を条件付きでマッピングする2.0互換の手段を提供します。 declared_attr と親の .__table__ の検索を組み合わせた以前のアプローチも同様に機能しますが、 :pep: 484 の型付けサポートがありません。

同様の概念をmixinクラス( Composing Mapped Hierarchies with Mixins を参照)で使用して、再利用可能なmixinクラスから特定の列やその他のマップされた属性を定義することができます:

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_on": type,
        "polymorphic_identity": "employee",
    }

class HasStartDate:
    start_date: Mapped[datetime] = mapped_column(
        nullable=True, use_existing_column=True
    )

class Engineer(HasStartDate, Employee):
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

class Manager(HasStartDate, Employee):
    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }

Relationships with Single Table Inheritance

リレーションシップは、単一テーブルの継承で完全にサポートされています。設定は、結合された継承と同じ方法で行われます。外部キー属性は、リレーションシップの「外部」側である同じクラスにある必要があります。:

class Company(Base):
    __tablename__ = "company"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    employees: Mapped[List[Employee]] = relationship(back_populates="company")

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]
    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))
    company: Mapped[Company] = relationship(back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }

class Manager(Employee):
    manager_data: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }

class Engineer(Employee):
    engineer_info: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

また、結合継承の場合と同様に、特定のサブクラスを含む関係を作成することもできます。照会すると、SELECT文には、クラス選択をそのサブクラスに制限するWHERE句が含まれます。:

class Company(Base):
    __tablename__ = "company"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    managers: Mapped[List[Manager]] = relationship(back_populates="company")

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }

class Manager(Employee):
    manager_name: Mapped[str] = mapped_column(nullable=True)

    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))
    company: Mapped[Company] = relationship(back_populates="managers")

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }

class Engineer(Employee):
    engineer_info: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

上の例では、 Manager クラスには Manager.company 属性があり、 Company クラスには Company.managers 属性があります。この属性は employee に対して常にロードされ、 type='manager' の行に制限するWHERE句が追加されます。

Building Deeper Hierarchies with polymorphic_abstract

New in version 2.0.

任意の種類の継承階層を構築する場合、マップされたクラスは、 Mapper.polymorphic_abstract パラメータを「True」に設定することができます。これは、クラスが通常通りにマップされるべきであることを示しますが、直接インスタンス化されることを期待せず、 Mapper.polymorphic_identity を含みません。サブクラスは、このマップされたクラスのサブクラスとして宣言することができます。サブクラスは、それ自体が Mapper.polymorphic_identity を含むことができるので、通常通り使用されます。これにより、クエリと relationship() 宣言の両方で、階層内で「抽象」であると見なされる共通の基本クラスによって、一連のサブクラスを一度に参照することができます。この使用は、ターゲットクラスを完全にマップされないままにして、それ自体でマップされたクラスとして使用できないDeclarativeでの __abstract__ 属性の使用とは異なります。 Mapper.polymorphic_abstract は、一度に複数のレベルを含む、階層内の任意のレベルの任意のクラスに適用できます。

例えば、 ManagerPrincipal の両方がスーパークラス Executive に対して分類され、 EngineerSysadmin がスーパークラス Technologist に対して分類されたとします。 ExecutiveTechnologist もインスタンス化されていないので、 Mapper.polymorphic_identity はありません。これらのクラスは、次のように Mapper.polymorphic_abstract を使って設定できます。:

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }

class Executive(Employee):
    """An executive of the company"""

    executive_background: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {"polymorphic_abstract": True}

class Technologist(Employee):
    """An employee who works with technology"""

    competencies: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {"polymorphic_abstract": True}

class Manager(Executive):
    """a manager"""

    __mapper_args__ = {"polymorphic_identity": "manager"}

class Principal(Executive):
    """a principal of the company"""

    __mapper_args__ = {"polymorphic_identity": "principal"}

class Engineer(Technologist):
    """an engineer"""

    __mapper_args__ = {"polymorphic_identity": "engineer"}

class SysAdmin(Technologist):
    """a systems administrator"""

    __mapper_args__ = {"polymorphic_identity": "sysadmin"}

上の例では、新しいクラス TechnologistExecutive は通常のマップされたクラスで、 executive_backgroundcompetencies という名前のスーパークラスに追加される新しい列を示しています。しかし、どちらにも Mapper.polymorphic_identity の設定がありません。これは、 TechnologistExecutive が直接インスタンス化されることが想定されていないためです。つまり、 ManagerPrincipalEngineerSysAdmin のいずれかが常に存在します。ただし、 PrincipalTechnologist のロールを問い合わせたり、 relationship() のターゲットにすることもできます。以下の例では、 Technologist オブジェクトのSELECT文を示しています。

session.scalars(select(Technologist)).all()
SELECT employee.id, employee.name, employee.type, employee.competencies FROM employee WHERE employee.type IN (?, ?) [...] ('engineer', 'sysadmin')

上記のマッピングを使用すると、 Company.technologistsCompany.executives にまたがる結合と関係の読み込みテクニックを個別に使用できます。

session.scalars(
    select(Company)
    .join(Company.technologists)
    .where(Technologist.competency.ilike("%java%"))
    .options(selectinload(Company.executives))
).all()
SELECT company.id FROM company JOIN employee ON company.id = employee.company_id AND employee.type IN (?, ?) WHERE lower(employee.competencies) LIKE lower(?) [...] ('engineer', 'sysadmin', '%java%') SELECT employee.company_id AS employee_company_id, employee.id AS employee_id, employee.name AS employee_name, employee.type AS employee_type, employee.executive_background AS employee_executive_background FROM employee WHERE employee.company_id IN (?) AND employee.type IN (?, ?) [...] (1, 'manager', 'principal')

See also

__abstract__ - 宣言型クラスが階層内で完全にマップされないようにする宣言型パラメータで、マップされたスーパークラスから拡張されます。

Loading Single Inheritance Mappings

単一テーブル継承のロードテクニックは、結合テーブル継承で使用されるものとほとんど同じです。また、これら2つのマッピング型の間には高度な抽象化が提供されているので、それらを簡単に切り替えたり、単一の階層に混在させたりすることができます(単一継承となるサブクラスからは、単に __tablename__ を省略してください)。マッパーの設定時とクエリ時の両方でクエリされるクラスの設定を含む、継承のロードテクニックに関するドキュメントについては、 Writing SELECT statements for Inheritance MappingsSELECT Statements for Single Inheritance Mappings のセクションを参照してください。

Concrete Table Inheritance

具象継承は、各サブクラスを独自のテーブルにマップします。各テーブルには、そのクラスのインスタンスを生成するために必要なすべての列が含まれています。具象継承の設定では、デフォルトで非多態的にクエリが実行されます。特定のクラスのクエリでは、そのクラスのテーブルのみがクエリされ、そのクラスのインスタンスのみが返されます。具象クラスの多様ロードを有効にするには、マッパー内で、通常はすべてのテーブルのUNIONとして生成される特撰を設定します。

Warning

具象テーブルの継承は、結合または単一テーブルの継承よりも はるかに複雑 であり、特に関係、eager loading、およびpolymorphic loadingでの使用に関して 機能がはるかに制限されています 。polymorphicに使用すると、UNION SEを持つ 非常に大きなクエリ が生成されますが、単純な結合ほどのパフォーマンスは得られません。関係の読み込みとポリモーフィックな読み込みに柔軟性が必要な場合は、可能であれば結合または単一テーブルの継承を使用することを強くお勧めします。ポリモーフィックな読み込みが必要でない場合は、各クラスが独自のテーブルを完全に参照する場合に、単純な非継承マッピングを使用できます。

結合された単一テーブルの継承は”多相”ロードに適していますが、具象継承では厄介な問題になります。このため、 多相ロードが必要ない 場合は具象継承の方が適しています。具象継承クラスを含む関係を確立するのも厄介です。

具象継承を使用してクラスを確立するには、 Mapper.concrete パラメータを __mapper_args__ 内に追加します。これは、Declarativeおよびマッピングに対して、スーパークラステーブルをマッピングの一部と見なすべきではないことを示します。:

class Employee(Base):
    __tablename__ = "employee"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))

class Manager(Employee):
    __tablename__ = "manager"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(50))

    __mapper_args__ = {
        "concrete": True,
    }

class Engineer(Employee):
    __tablename__ = "engineer"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(50))

    __mapper_args__ = {
        "concrete": True,
    }

2つの重要な点に注意する必要があります。:

  • 同じ名前であっても、サブクラスごとにすべての列を**明示的に定義**する必要があります。ここにある Employee.name のような列は、 ManagerEngineer によってマップされたテーブルには**コピーされません**。

  • 一方、 Engineer クラスと Manager クラスは Employee と継承関係でマップされますが、それらはまだ**ポリモーフィックローディングを含んでいません**。つまり、 Employee オブジェクトを問い合わせても、 manager テーブルと engineer テーブルはまったく問い合わせされません。

Concrete Polymorphic Loading Configuration

具象継承を使用した多様型読み込みでは、多様型読み込みを行う必要がある各基底クラスに対して、特殊なSELECTを設定する必要があります。このSELECTは、マップされたすべてのテーブルに個別にアクセスできる必要があり、通常はSQLAlchemyヘルパー polymorphic_union() を使用して構築されたUNION文です。

Writing SELECT statements for Inheritance Mappings で説明されているように、 Mapper.with_polymorphic 引数を使用して、デフォルトで特別な選択可能なものからロードするように、あらゆるタイプのマッパー継承設定を設定することができます。現在の公開APIでは、最初に構築されるときにこの引数が Mapper に設定される必要があります。

しかし、Declarativeの場合、マップされたクラスが定義された瞬間に、マッパーとマップされた Table の両方が同時に作成されます。これは、サブクラスに対応する Table オブジェクトがまだ定義されていないため、 Mapper.with_polymorphic 引数をまだ提供できないことを意味します。

このサイクルを解決するために利用できる戦略はいくつかありますが、Declarativeはこの問題を舞台裏で処理するヘルパークラス ConcreteBaseAbstractConcreteBase を提供しています。

ConcreteBase を使うと、他の形式の継承マッピングとほとんど同じ方法で具象マッピングを設定できます:

from sqlalchemy.ext.declarative import ConcreteBase
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class Employee(ConcreteBase, Base):
    __tablename__ = "employee"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "concrete": True,
    }

class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }

上の例では、Declarativeはマッパーの”初期化”時に Employee クラスの多様選択可能型を設定します。これは、他の依存するマッパーを解決するマッパーの後期設定ステップです。 ConcreteBase ヘルパーは、 polymorphic_union() 関数を使用して、他のすべてのクラスが設定された後に、具象マップされたすべてのテーブルのUNIONを作成し、既存の基本クラスマッパーでこの文を設定します。

選択すると、多様ユニオンは次のようなクエリを生成します。:

session.scalars(select(Employee)).all()
SELECT pjoin.id, pjoin.name, pjoin.type, pjoin.manager_data, pjoin.engineer_info FROM ( SELECT employee.id AS id, employee.name AS name, CAST(NULL AS VARCHAR(40)) AS manager_data, CAST(NULL AS VARCHAR(40)) AS engineer_info, 'employee' AS type FROM employee UNION ALL SELECT manager.id AS id, manager.name AS name, manager.manager_data AS manager_data, CAST(NULL AS VARCHAR(40)) AS engineer_info, 'manager' AS type FROM manager UNION ALL SELECT engineer.id AS id, engineer.name AS name, CAST(NULL AS VARCHAR(40)) AS manager_data, engineer.engineer_info AS engineer_info, 'engineer' AS type FROM engineer ) AS pjoin

上記のUNIONクエリは、その特定のサブクラスのメンバではない列に対応するために、各サブテーブルに対して”NULL”列を作成する必要があります。

See also

ConcreteBase

Abstract Concrete Classes

これまでに説明した具象マッピングでは、サブクラスと基本クラスの両方が個々のテーブルにマップされています。具象継承のユースケースでは、基本クラスがデータベース内で表現されず、サブクラスのみが表現されるのが一般的です。つまり、基本クラスは「抽象」です。

通常、2つの異なるサブクラスを個々のテーブルにマップし、ベースクラスをマップされないままにしておきたい場合、これは非常に簡単に実現できます。Declarativeを使用する場合は、単にベースクラスを __abstract__ インジケータで宣言します。:

from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class Employee(Base):
    __abstract__ = True

class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))

class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))

上記では、実際にはSQLAlchemyの継承マッピング機能を使用していません。通常は、 ManagerEngineer のインスタンスをロードして永続化することができます。しかし、 多態的に クエリする必要がある場合、つまり、 select(Employee) を出力して ManagerEngineer のインスタンスのコレクションを取得したい場合は、状況が変わります。これにより、具体的な継承の領域に戻り、これを実現するために Employee に対する特別なマッパーを構築する必要があります。

具象継承の例を変更して、ポリモーフィックなロードが可能な 抽象 ベースを示すために、 engineer テーブルと manager テーブルだけがあり、 employee テーブルはありません。しかし、 Employee マッパーは、 Mapper.with_polymorphic パラメータにローカルに指定するのではなく、 polymorphic union に直接マップされます。

これを支援するために、Declarativeは AbstractConcreteBase と呼ばれる ConcreteBase クラスの変種を提供しており、これを自動的に実現します:

from sqlalchemy.ext.declarative import AbstractConcreteBase
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class Employee(AbstractConcreteBase, Base):
    strict_attrs = True

    name = mapped_column(String(50))

class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }

Base.registry.configure()

上の例では、 registry.configure() メソッドが呼び出されて、実際にマップされるべき Employee クラスを起動します。設定ステップの前には、問い合わせ元のサブテーブルがまだ定義されていないので、このクラスにはマッピングがありません。このプロセスは、すべてのサブクラスが宣言されるまで基本クラスのマッピング全体を遅らせなければならないという点で、 ConcreteBase のプロセスよりも複雑です。上の例のようなマッピングでは、 ManagerEngineer のインスタンスだけが永続化されます。 Employee クラスに対する問い合わせは、常に ManagerEngineer オブジェクトを生成します。

上記のマッピングを使用すると、 Employee クラスと、そのクラスでローカルに宣言されている Employee.name などの属性に基づいてクエリを生成できます。

>>> stmt = select(Employee).where(Employee.name == "n1")
>>> print(stmt)
SELECT pjoin.id, pjoin.name, pjoin.type, pjoin.manager_data, pjoin.engineer_info FROM ( SELECT engineer.id AS id, engineer.name AS name, engineer.engineer_info AS engineer_info, CAST(NULL AS VARCHAR(40)) AS manager_data, 'engineer' AS type FROM engineer UNION ALL SELECT manager.id AS id, manager.name AS name, CAST(NULL AS VARCHAR(40)) AS engineer_info, manager.manager_data AS manager_data, 'manager' AS type FROM manager ) AS pjoin WHERE pjoin.name = :name_1

AbstractConcreteBase.strict_attrs パラメータは、 Employee クラスが Employee クラスに対してローカルな属性(この場合は Employee.name 属性)のみを直接マップすべきであることを示します。 Manager.manager_dataEngineer.engineer_info のような他の属性は、対応するサブクラスにのみ存在します。 AbstractConcreteBase.strict_attrs が設定されていない場合、 Manager.manager_dataEngineer.engineer_info のようなすべてのサブクラス属性は、ベースの Employee クラスにマップされます。これはレガシーモードの使用方法で、クエリには便利ですが、すべてのサブクラスが階層全体の属性の完全なセットを共有するという効果があります。上の例では、 AbstractConcreteBase.strict_attrs を使用しないと、役に立たない Engineer.manager_name および Manager.engineer_info 属性が生成されます。

New in version 2.0: AbstractConcreteBase.strict_attrs`パ ラメータが :class:.AbstractConcreteBase` に追加されました。これにより、よりクリーンなマッピングが生成されます。デフォルトはFalseで、レガシーマッピングが1.xバージョンで行われたように動作し続けることができます。

Classical and Semi-Classical Concrete Polymorphic Configuration

ConcreteBaseAbstractConcreteBase で示された宣言的な設定は、 polymorphic_union() を明示的に使用する他の2つの形式の設定と等価です。これらの設定形式は Table オブジェクトを明示的に使用するので、”polymorphic union”を最初に作成してからマッピングに適用することができます。ここでは、マッピングの観点から polymorphic_union() 関数の役割を明確にするために、これらを示します。

例えば、 半古典的なマッピング はDeclarativeを利用しますが、 Table オブジェクトを別々に確立します:

metadata_obj = Base.metadata

employees_table = Table(
    "employee",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
)

managers_table = Table(
    "manager",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("manager_data", String(50)),
)

engineers_table = Table(
    "engineer",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("engineer_info", String(50)),
)

次に、UNIONは polymorphic_union() を使って生成されます:

from sqlalchemy.orm import polymorphic_union

pjoin = polymorphic_union(
    {
        "employee": employees_table,
        "manager": managers_table,
        "engineer": engineers_table,
    },
    "type",
    "pjoin",
)

上記の Table オブジェクトでは、「半古典的」なスタイルを使ってマッピングを生成することができます。ここでは、Declarativeを __table__ 引数と組み合わせて使用します。上記のpolymorphic unionは、 __mapper_args__ を経由して Mapper.with_polymorphic パラメータに渡されます。:

class Employee(Base):
    __table__ = employee_table
    __mapper_args__ = {
        "polymorphic_on": pjoin.c.type,
        "with_polymorphic": ("*", pjoin),
        "polymorphic_identity": "employee",
    }

class Engineer(Employee):
    __table__ = engineer_table
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }

class Manager(Employee):
    __table__ = manager_table
    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }

あるいは、同じ Table オブジェクトを、Declarativeをまったく使用せずに、完全に「古典的」なスタイルで使用することもできます。Declarativeによって提供されるものと同様のコンストラクタを次に示します:

class Employee:
    def __init__(self, **kw):
        for k in kw:
            setattr(self, k, kw[k])

class Manager(Employee):
    pass

class Engineer(Employee):
    pass

employee_mapper = mapper_registry.map_imperatively(
    Employee,
    pjoin,
    with_polymorphic=("*", pjoin),
    polymorphic_on=pjoin.c.type,
)
manager_mapper = mapper_registry.map_imperatively(
    Manager,
    managers_table,
    inherits=employee_mapper,
    concrete=True,
    polymorphic_identity="manager",
)
engineer_mapper = mapper_registry.map_imperatively(
    Engineer,
    engineers_table,
    inherits=employee_mapper,
    concrete=True,
    polymorphic_identity="engineer",
)

“abstract”の例は、”semi-classical”または”classical”スタイルを使用してマップすることもできます。違いは、”polymorphic union”を Mapper.with_polymorphic パラメータに適用するのではなく、最も基本的なマッパーで選択可能なマップとして直接適用することです。半古典的なマッピングを以下に示します。:

from sqlalchemy.orm import polymorphic_union

pjoin = polymorphic_union(
    {
        "manager": managers_table,
        "engineer": engineers_table,
    },
    "type",
    "pjoin",
)

class Employee(Base):
    __table__ = pjoin
    __mapper_args__ = {
        "polymorphic_on": pjoin.c.type,
        "with_polymorphic": "*",
        "polymorphic_identity": "employee",
    }

class Engineer(Employee):
    __table__ = engineer_table
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }

class Manager(Employee):
    __table__ = manager_table
    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }

上記では、以前と同じ方法で polymorphic_union() を使用していますが、 employee テーブルを省略しています。

See also

Imperative Mapping - 命令型マッピング、つまり”古典的な”マッピングに関する背景情報

Relationships with Concrete Inheritance

具体的な継承シナリオでは、別個のクラスがテーブルを共有しないため、関係のマッピングは困難です。前の例の CompanyManager の間の関係のように、関係が特定のクラスのみを含む場合、これらは2つの関連するテーブルであるため、特別な手順は必要ありません。

しかし、もし CompanyEmployee に対して一対多の関係を持ち、コレクションが Engineer オブジェクトと Manager オブジェクトの両方を含む可能性がある場合、 Employee が多様な読み込み機能を持ち、関連する各テーブルが company テーブルへの外部キーを持つ必要があることを意味します。このような設定の例を次に示します。:

from sqlalchemy.ext.declarative import ConcreteBase

class Company(Base):
    __tablename__ = "company"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    employees = relationship("Employee")

class Employee(ConcreteBase, Base):
    __tablename__ = "employee"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    company_id = mapped_column(ForeignKey("company.id"))

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "concrete": True,
    }

class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))
    company_id = mapped_column(ForeignKey("company.id"))

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))
    company_id = mapped_column(ForeignKey("company.id"))

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }

具体的な継承と関係に関する次の複雑さは、 EmployeeManagerEngineer の1つまたはすべてを Company に参照させたい場合です。この場合、SQLAlchemyには特別な動作があります。つまり、インスタンスレベルで実行された場合、 Employee に配置された Company にリンクする relationship() は、 Manager クラスと Engineer クラスに対して**機能しません**。代わりに、各クラスに個別の relationship() を適用する必要があります。 Company.employees とは反対の役割を果たす3つの独立した関係に関して双方向の動作を実現するために、 relationship.back_populates パラメータが各関係の間で使用されます:

from sqlalchemy.ext.declarative import ConcreteBase

class Company(Base):
    __tablename__ = "company"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    employees = relationship("Employee", back_populates="company")

class Employee(ConcreteBase, Base):
    __tablename__ = "employee"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    company_id = mapped_column(ForeignKey("company.id"))
    company = relationship("Company", back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "concrete": True,
    }

class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))
    company_id = mapped_column(ForeignKey("company.id"))
    company = relationship("Company", back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))
    company_id = mapped_column(ForeignKey("company.id"))
    company = relationship("Company", back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }

上記の制限は現在の実装に関連しており、具体的な継承クラスはスーパークラスの属性を共有しないため、明確な関係を設定する必要があることが含まれます。

Loading Concrete Inheritance Mappings

具象継承を使用したロードのオプションには制限があります。一般に、宣言型具象ミックスインの1つを使用してマッパーで多様ロードが設定されている場合、現在のSQLAlchemyバージョンではクエリ時に変更できません。通常、 with_polymorphic() 関数は具象で使用されているロードのスタイルを上書きできますが、現在の制限により、これはまだサポートされていません。