Mapping von Klassenerbehierarchien

SQLAlchemy unterstützt drei Formen der Vererbung:

  • Single Table Inheritance – mehrere Klassentypen werden durch eine einzige Tabelle repräsentiert;

  • Concrete Table Inheritance – jeder Klassentyp wird durch unabhängige Tabellen repräsentiert;

  • Joined Table Inheritance – die Klassenhierarchie wird auf abhängige Tabellen aufgeteilt. Jede Klasse wird durch ihre eigene Tabelle repräsentiert, die nur die Attribute enthält, die lokal zu dieser Klasse sind.

Die gängigsten Formen der Vererbung sind Single und Joined Table. Concrete Inheritance birgt hingegen größere Konfigurationsherausforderungen.

Wenn Mapper in einer Vererbungshierarchie konfiguriert werden, hat SQLAlchemy die Fähigkeit, Elemente polymorphisch zu laden, was bedeutet, dass eine einzelne Abfrage Objekte verschiedener Typen zurückgeben kann.

Siehe auch

Schreiben von SELECT-Anweisungen für Vererbungsmappings - im ORM Querying Guide

Rezepte für Inheritance Mapping - vollständige Beispiele für Joined, Single und Concrete Inheritance

Joined Table Inheritance

Bei Joined Table Inheritance wird jede Klasse in einer Klassenhierarchie durch eine separate Tabelle repräsentiert. Eine Abfrage nach einer bestimmten Unterklasse in der Hierarchie wird als SQL JOIN über alle Tabellen in ihrem Vererbungspfad gerendert. Wenn die abgefragte Klasse die Basisklasse ist, wird stattdessen die Basistabelle abgefragt, mit Optionen, andere Tabellen gleichzeitig einzubeziehen oder Attribute, die spezifisch für Untertabellen sind, später zu laden.

In allen Fällen wird die endgültige zu instanziierende Klasse für eine gegebene Zeile durch eine Diskriminator-Spalte oder eine SQL-Expression bestimmt, die auf der Basisklasse definiert ist und einen Skalarwert liefert, der einer bestimmten Unterklasse zugeordnet ist.

Die Basisklasse in einer Joined-Inheritance-Hierarchie wird mit zusätzlichen Argumenten konfiguriert, die auf die polymorphe Diskriminatorspalte und optional einen polymorphen Identifikator für die Basisklasse selbst hinweisen.

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

Im obigen Beispiel ist die Diskriminatorspalte die type-Spalte, die mit dem Parameter Mapper.polymorphic_on konfiguriert wird. Dieser Parameter akzeptiert einen spaltenorientierten Ausdruck, der entweder als Zeichenkettenname des zu verwendenden zugeordneten Attributs oder als Spaltenausdrucksobjekt wie Column oder mapped_column()-Konstrukt angegeben wird.

Die Diskriminatorspalte speichert einen Wert, der den Typ des in der Zeile dargestellten Objekts angibt. Die Spalte kann jeden Datentyp haben, wobei Zeichenketten und Ganzzahlen am gebräuchlichsten sind. Der tatsächliche Datenwert, der dieser Spalte für eine bestimmte Zeile in der Datenbank zugewiesen wird, wird mit dem Parameter Mapper.polymorphic_identity angegeben, der unten beschrieben wird.

Obwohl ein polymorpher Diskriminatorausdruck nicht zwingend erforderlich ist, wird er benötigt, wenn polymorphes Laden gewünscht wird. Die Einrichtung einer Spalte in der Basistabelle ist der einfachste Weg, dies zu erreichen. Sehr anspruchsvolle Vererbungsmappings können jedoch SQL-Ausdrücke wie einen CASE-Ausdruck als polymorphen Diskriminator verwenden.

Hinweis

Derzeit darf nur eine Diskriminatorspalte oder ein SQL-Ausdruck für die gesamte Vererbungshierarchie konfiguriert werden, typischerweise auf der basischsten Klasse in der Hierarchie. „Kaskadierende“ polymorphe Diskriminatorausdrücke werden noch nicht unterstützt.

Als Nächstes definieren wir die Engineer und Manager Unterklassen von Employee. Jede enthält Spalten, die die Attribute repräsentieren, die für die jeweilige Unterklasse eindeutig sind. Jede Tabelle muss auch eine Primärschlüsselspalte (oder Spalten) sowie eine Fremdschlüsselreferenz zur übergeordneten Tabelle enthalten.

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",
    }

Im obigen Beispiel gibt jedes Mapping den Parameter Mapper.polymorphic_identity innerhalb seiner Mapper-Argumente an. Dieser Wert füllt die Spalte, die durch den Parameter Mapper.polymorphic_on auf dem Basis-Mapper festgelegt wurde. Der Parameter Mapper.polymorphic_identity sollte für jede zugeordnete Klasse in der gesamten Hierarchie eindeutig sein, und es sollte nur eine „Identität“ pro zugeordneter Klasse geben; wie oben erwähnt, werden „kaskadierende“ Identitäten, bei denen einige Unterklassen eine zweite Identität einführen, nicht unterstützt.

Die ORM verwendet den durch Mapper.polymorphic_identity festgelegten Wert, um zu bestimmen, zu welcher Klasse eine Zeile beim polymorphen Laden von Zeilen gehört. Im obigen Beispiel hat jede Zeile, die einen Employee repräsentiert, den Wert 'employee' in ihrer type-Spalte; ebenso erhält jeder Engineer den Wert 'engineer', und jeder Manager erhält den Wert 'manager'. Unabhängig davon, ob das Vererbungsmpping unterschiedliche joined tables für Unterklassen wie bei Joined Table Inheritance verwendet oder alles in einer Tabelle wie bei Single Table Inheritance, wird erwartet, dass dieser Wert persistiert und der ORM beim Abfragen zur Verfügung steht. Der Parameter Mapper.polymorphic_identity gilt auch für Concrete Table Inheritance, wird aber nicht tatsächlich persistiert; siehe den späteren Abschnitt unter Concrete Table Inheritance für Details.

In einem polymorphen Setup ist es am häufigsten, dass die Fremdschlüsselbeschränkung auf derselben Spalte oder denselben Spalten wie der Primärschlüssel selbst eingerichtet ist. Dies ist jedoch nicht zwingend erforderlich; eine vom Primärschlüssel getrennte Spalte kann auch über einen Fremdschlüssel auf die übergeordnete Tabelle verweisen. Die Art und Weise, wie ein JOIN von der Basistabelle zu Unterklassen konstruiert wird, ist ebenfalls direkt anpassbar, was jedoch selten notwendig ist.

Nach Abschluss des Joined Inheritance-Mappings gibt eine Abfrage gegen Employee eine Kombination aus Employee-, Engineer- und Manager-Objekten zurück. Neu gespeicherte Engineer-, Manager- und Employee-Objekte füllen die employee.type-Spalte automatisch mit dem korrekten „Diskriminator“-Wert, in diesem Fall "engineer", "manager" oder "employee", je nach Anwendbarkeit.

Beziehungen mit Joined Inheritance

Beziehungen werden mit Joined Table Inheritance vollständig unterstützt. Die Beziehung, die eine Joined-Inheritance-Klasse involviert, sollte auf die Klasse in der Hierarchie abzielen, die auch der Fremdschlüsselbeschränkung entspricht; unten, da die employee-Tabelle eine Fremdschlüsselbeschränkung zur company-Tabelle hat, werden die Beziehungen zwischen Company und Employee eingerichtet.

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

Wenn die Fremdschlüsselbeschränkung auf einer Tabelle liegt, die einer Unterklasse entspricht, sollte sich die Beziehung stattdessen auf diese Unterklasse beziehen. Im folgenden Beispiel gibt es eine Fremdschlüsselbeschränkung von manager zu company, daher werden die Beziehungen zwischen den Klassen Manager und Company hergestellt.

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

Oben hat die Klasse Manager ein Attribut Manager.company; Company hat ein Attribut Company.managers, das immer gegen einen Join der Tabellen employee und manager gemeinsam geladen wird.

Laden von Joined Inheritance Mappings

Siehe den Abschnitt Schreiben von SELECT-Anweisungen für Vererbungsmappings für Hintergrundinformationen zu Vererbungsladetechniken, einschließlich der Konfiguration von Tabellen, die sowohl zur Mapper-Konfigurationszeit als auch zur Abfragezeit abgefragt werden.

Single Table Inheritance

Single Table Inheritance repräsentiert alle Attribute aller Unterklassen innerhalb einer einzigen Tabelle. Eine bestimmte Unterklasse, die Attribute besitzt, die für diese Klasse einzigartig sind, speichert diese in Spalten der Tabelle, die andernfalls NULL sind, wenn die Zeile einen anderen Objekttyp repräsentiert.

Eine Abfrage nach einer bestimmten Unterklasse in der Hierarchie wird als SELECT gegen die Basistabelle gerendert, die eine WHERE-Klausel enthält, die die Zeilen auf diejenigen mit einem bestimmten Wert oder bestimmten Werten in der Diskriminatorspalte oder im Ausdruck beschränkt.

Single Table Inheritance hat den Vorteil der Einfachheit im Vergleich zu Joined Table Inheritance; Abfragen sind wesentlich effizienter, da nur eine Tabelle beteiligt sein muss, um Objekte jeder dargestellten Klasse zu laden.

Die Konfiguration von Single-Table-Inheritance sieht Single-Table-Inheritance ähnlich wie Joined-Table-Inheritance aus, außer dass nur die Basisklasse __tablename__ angibt. Eine Diskriminatorspalte ist ebenfalls auf der Basistabelle erforderlich, damit Klassen voneinander unterschieden werden können.

Auch wenn Unterklassen die Basistabelle für alle ihre Attribute gemeinsam nutzen, können bei Verwendung von Declarative mapped_column-Objekte immer noch auf Unterklassen spezifiziert werden, was angibt, dass die Spalte nur dieser Unterklasse zugeordnet wird; die mapped_column wird auf dasselbe Basis- Table-Objekt angewendet.

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",
    }

Beachten Sie, dass die Mapper für die abgeleiteten Klassen Manager und Engineer __tablename__ weglassen, was darauf hindeutet, dass sie keine eigene zugeordnete Tabelle haben. Zusätzlich ist eine mapped_column()-Direktive mit nullable=True enthalten; da die Python-Typen, die für diese Klassen deklariert sind, nicht Optional[] enthalten, würde die Spalte normalerweise als NOT NULL zugeordnet werden, was nicht angemessen wäre, da diese Spalte nur dann gefüllt werden soll, wenn die Zeile dieser bestimmten Unterklasse entspricht.

Auflösen von Spaltenkonflikten mit use_existing_column

Beachten Sie im vorherigen Abschnitt, dass die Spalten manager_name und engineer_info „hochgeschoben“ werden, um auf Employee.__table__ angewendet zu werden, als Ergebnis ihrer Deklaration auf einer Unterklasse, die keine eigene Tabelle hat. Ein kniffliger Fall tritt auf, wenn zwei Unterklassen *dieselbe* Spalte definieren möchten, wie unten.

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)

Oben führt die Spalte start_date, die sowohl auf Engineer als auch auf Manager deklariert ist, zu einem Fehler.

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.

Das obige Szenario stellt eine Mehrdeutigkeit für das Declarative-Mapping-System dar, die durch die Verwendung des Parameters mapped_column.use_existing_column auf mapped_column() gelöst werden kann. Dies weist mapped_column() an, auf der vererbenden Oberklasse nachzusehen und die bereits zugeordnete Spalte zu verwenden, falls sie bereits vorhanden ist, andernfalls eine neue Spalte zuzuordnen.

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
    )

Oben, wenn Manager zugeordnet wird, ist die Spalte start_date bereits in der Klasse Employee vorhanden, nachdem sie bereits durch das Mapping Engineer bereitgestellt wurde. Der Parameter mapped_column.use_existing_column weist mapped_column() an, zuerst nach der angeforderten Column in der zugeordneten Table für Employee zu suchen und, falls vorhanden, diese bestehende Zuordnung beizubehalten. Wenn sie nicht vorhanden ist, ordnet mapped_column() die Spalte normal zu und fügt sie als eine der Spalten in der Table hinzu, auf die von der Employee-Oberklasse verwiesen wird.

Neu in Version 2.0.0b4: - Hinzugefügt mapped_column.use_existing_column, welches eine 2.0-kompatible Methode zur bedingten Zuordnung einer Spalte auf einer vererbenden Unterklasse bereitstellt. Der vorherige Ansatz, der declared_attr mit einer Suche auf der übergeordneten .__table__ kombiniert, funktioniert weiterhin, verfügt aber nicht über PEP 484-Typunterstützung.

Ein ähnliches Konzept kann mit Mixin-Klassen (siehe Zusammensetzen von Mapped Hierarchien mit Mixins) verwendet werden, um eine bestimmte Reihe von Spalten und/oder andere zugeordnete Attribute aus einer wiederverwendbaren Mixin-Klasse zu definieren.

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",
    }

Beziehungen mit Single Table Inheritance

Beziehungen werden mit Single Table Inheritance vollständig unterstützt. Die Konfiguration erfolgt auf die gleiche Weise wie bei Joined Inheritance; ein Fremdschlüsselattribut sollte sich auf derselben Klasse befinden, die die „fremde“ Seite der Beziehung ist.

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",
    }

Ebenso können wir, wie im Fall von Joined Inheritance, Beziehungen erstellen, die eine bestimmte Unterklasse involvieren. Beim Abfragen enthält die SELECT-Anweisung eine WHERE-Klausel, die die Klassenauswahl auf diese Unterklasse oder Unterklassen beschränkt.

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",
    }

Oben hat die Klasse Manager ein Attribut Manager.company; Company hat ein Attribut Company.managers, das immer gegen die employee mit einer zusätzlichen WHERE-Klausel geladen wird, die die Zeilen auf diejenigen mit type = 'manager' beschränkt.

Aufbau tieferer Hierarchien mit polymorphic_abstract

Neu in Version 2.0.

Beim Aufbau jeder Art von Vererbungshierarchie kann eine zugeordnete Klasse den Parameter Mapper.polymorphic_abstract auf True gesetzt haben, was angibt, dass die Klasse normal zugeordnet werden soll, aber nicht direkt instanziiert werden soll und keine Mapper.polymorphic_identity enthalten würde. Unterklassen können dann als Unterklassen dieser zugeordneten Klasse deklariert werden, die selbst eine Mapper.polymorphic_identity enthalten und daher normal verwendet werden können. Dies ermöglicht, dass eine Reihe von Unterklassen gleichzeitig durch eine gemeinsame Basisklasse referenziert werden, die als „abstrakt“ innerhalb der Hierarchie betrachtet wird, sowohl in Abfragen als auch in relationship()-Deklarationen. Diese Verwendung unterscheidet sich von der Verwendung des Attributs __abstract__ mit Declarative, das die Zielklasse vollständig unmapped lässt und somit nicht als zugeordnete Klasse für sich allein verwendet werden kann. Mapper.polymorphic_abstract kann auf jede Klasse oder Klassen auf jeder Ebene der Hierarchie angewendet werden, auch auf mehreren Ebenen gleichzeitig.

Als Beispiel nehmen wir an, Manager und Principal würden beide gegen eine Oberklasse Executive klassifiziert, und Engineer und Sysadmin würden gegen eine Oberklasse Technologist klassifiziert. Weder Executive noch Technologist werden jemals instanziiert, daher haben sie keine Mapper.polymorphic_identity. Diese Klassen können wie folgt mit Mapper.polymorphic_abstract konfiguriert werden:

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"}

In dem obigen Beispiel sind die neuen Klassen Technologist und Executive normale zugeordnete Klassen und geben auch neue Spalten an, die zur Oberklasse executive_background und competencies hinzugefügt werden sollen. Sie haben jedoch beide keine Einstellung für Mapper.polymorphic_identity; dies liegt daran, dass nicht erwartet wird, dass Technologist oder Executive direkt instanziiert werden; wir hätten immer einen von Manager, Principal, Engineer oder SysAdmin. Wir können jedoch nach Principal und Technologist Rollen abfragen und sie als Ziele von relationship() verwenden. Das folgende Beispiel zeigt eine SELECT-Anweisung für Technologist-Objekte:

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

Die abstrakten zugeordneten Klassen Technologist und Executive können, wie jede andere zugeordnete Klasse, auch Ziele von relationship()-Mappings sein. Wir können das obige Beispiel erweitern, um Company mit separaten Sammlungen Company.technologists und Company.principals einzuschließen:

class Company(Base):
    __tablename__ = "company"
    id = Column(Integer, primary_key=True)

    executives: Mapped[List[Executive]] = relationship()
    technologists: Mapped[List[Technologist]] = relationship()


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

    # foreign key to "company.id" is added
    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))

    # rest of mapping is the same
    name: Mapped[str]
    type: Mapped[str]

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


# Executive, Technologist, Manager, Principal, Engineer, SysAdmin
# classes from previous example would follow here unchanged

Mit dem obigen Mapping können wir Joins und Relationship-Ladeverfahren individuell über Company.technologists und Company.executives verwenden.

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

Siehe auch

__abstract__ - Declarative-Parameter, der es einer Declarative-Klasse ermöglicht, innerhalb einer Hierarchie vollständig unmapped zu sein, während sie dennoch von einer zugeordneten Oberklasse erbt.

Laden von Single Inheritance Mappings

Die Ladetechniken für Single-Table-Inheritance sind weitgehend identisch mit denen für Joined-Table-Inheritance, und es wird ein hoher Abstraktionsgrad zwischen diesen beiden Mapping-Typen geboten, sodass es einfach ist, zwischen ihnen zu wechseln und sie in einer einzigen Hierarchie zu vermischen (lassen Sie einfach __tablename__ von den Unterklassen weg, die Single-Inheriting sein sollen). Siehe die Abschnitte Schreiben von SELECT-Anweisungen für Vererbungsmappings und SELECT-Anweisungen für Single Inheritance Mappings für Dokumentationen zu Vererbungsladetechniken, einschließlich der Konfiguration von Klassen, die sowohl zur Mapper-Konfigurationszeit als auch zur Abfragezeit abgefragt werden.

Concrete Table Inheritance

Concrete Inheritance ordnet jede Unterklasse ihrer eigenen, separaten Tabelle zu, wobei jede alle Spalten enthält, die zur Erzeugung einer Instanz dieser Klasse erforderlich sind. Eine Concrete-Inheritance-Konfiguration fragt standardmäßig nicht-polymorph ab; eine Abfrage nach einer bestimmten Klasse fragt nur die Tabelle dieser Klasse ab und gibt nur Instanzen dieser Klasse zurück. Polymorphes Laden von Concrete-Klassen wird durch die Konfiguration einer speziellen SELECT-Anweisung innerhalb des Mappers ermöglicht, die typischerweise als UNION aller Tabellen erzeugt wird.

Warnung

Concrete Table Inheritance ist wesentlich komplizierter als Joined oder Single Table Inheritance und ist funktional stark eingeschränkt, insbesondere bei der Verwendung mit Beziehungen, Eager Loading und polymorphem Laden. Bei polymorpher Verwendung erzeugt es sehr große Abfragen mit UNIONs, die nicht so gut performen wie einfache Joins. Es wird dringend empfohlen, Joined oder Single Table Inheritance zu verwenden, wenn dies möglich ist, wenn Flexibilität beim Laden von Beziehungen und beim polymorphen Laden erforderlich ist. Wenn kein polymorphes Laden erforderlich ist, können einfache nicht-erbende Mappings verwendet werden, wenn jede Klasse vollständig auf ihre eigene Tabelle verweist.

Während Joined und Single Table Inheritance im „polymorphen“ Laden flüssig sind, ist es bei Concrete Inheritance eine umständlichere Angelegenheit. Aus diesem Grund ist Concrete Inheritance besser geeignet, wenn polymorphes Laden nicht erforderlich ist. Das Einrichten von Beziehungen, die Concrete-Inheritance-Klassen involvieren, ist ebenfalls umständlicher.

Um eine Klasse als Concrete-Inheritance zu etablieren, fügen Sie den Parameter Mapper.concrete innerhalb von __mapper_args__ hinzu. Dies zeigt Declarative sowie dem Mapping an, dass die Oberklassen-Tabelle nicht als Teil des Mappings betrachtet werden soll.

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,
    }

Zwei kritische Punkte sollten beachtet werden:

  • Wir müssen alle Spalten explizit auf jeder Unterklasse definieren, auch solche mit demselben Namen. Eine Spalte wie Employee.name wird hier nicht für uns auf die von Manager oder Engineer zugeordneten Tabellen kopiert.

  • Während die Klassen Engineer und Manager in einer Vererbungsbeziehung mit Employee abgebildet werden, beinhalten sie immer noch **kein polymorphes Laden**. Das bedeutet, wenn wir nach Employee-Objekten abfragen, werden die Tabellen manager und engineer überhaupt nicht abgefragt.

Konfiguration des konkreten polymorphen Ladens

Polymorphes Laden mit konkreter Vererbung erfordert, dass für jede Basisklasse, die polymorphes Laden unterstützen soll, ein spezialisiertes SELECT konfiguriert wird. Dieses SELECT muss in der Lage sein, alle abgebildeten Tabellen einzeln abzurufen, und ist typischerweise eine UNION-Anweisung, die mithilfe eines SQLAlchemy-Helferprogramms polymorphic_union() erstellt wird.

Wie in Schreiben von SELECT-Anweisungen für Vererbungsmappings besprochen, können Mapper-Vererbungskonfigurationen jeglicher Art so konfiguriert werden, dass sie standardmäßig aus einem speziellen wählbaren Element geladen werden, indem das Argument Mapper.with_polymorphic verwendet wird. Die aktuelle öffentliche API verlangt, dass dieses Argument einem Mapper beim ersten Erstellen zugewiesen wird.

Im Fall von Declarative werden jedoch der Mapper und die abgebildete Table gleichzeitig erstellt, sobald die abgebildete Klasse definiert ist. Das bedeutet, dass das Argument Mapper.with_polymorphic noch nicht übergeben werden kann, da die Table-Objekte, die den Unterklassen entsprechen, noch nicht definiert wurden.

Es gibt einige Strategien, um diesen Zyklus aufzulösen. Declarative bietet jedoch Hilfsklassen ConcreteBase und AbstractConcreteBase, die dieses Problem im Hintergrund behandeln.

Mit ConcreteBase können wir unser konkretes Mapping fast genauso einrichten wie andere Formen von Vererbungsmappings.

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,
    }

Oben richtet Declarative das polymorphe wählbare Element für die Employee-Klasse zur Mapper-"Initialisierungszeit" ein; dies ist der späte Konfigurationsschritt für Mapper, der andere abhängige Mapper auflöst. Die Hilfsklasse ConcreteBase verwendet die Funktion polymorphic_union(), um eine UNION aller konkret abgebildeten Tabellen zu erstellen, nachdem alle anderen Klassen eingerichtet wurden, und konfiguriert dann diese Anweisung mit dem bereits vorhandenen Basisklassen-Mapper.

Beim Auswählen erzeugt die polymorphe Union eine Abfrage wie diese

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

Die obige UNION-Abfrage muss "NULL"-Spalten für jede Untertabelle erstellen, um Spalten aufzunehmen, die nicht zu dieser bestimmten Unterklasse gehören.

Siehe auch

ConcreteBase

Abstrakte konkrete Klassen

Die bisher gezeigten konkreten Mappings bilden sowohl die Unterklassen als auch die Basisklasse auf einzelne Tabellen ab. Im konkreten Vererbungsfall ist es üblich, dass die Basisklasse nicht in der Datenbank repräsentiert ist, sondern nur die Unterklassen. Mit anderen Worten, die Basisklasse ist "abstrakt".

Normalerweise, wenn man zwei verschiedene Unterklassen auf einzelne Tabellen abbilden und die Basisklasse nicht abbilden möchte, kann dies sehr einfach erreicht werden. Bei der Verwendung von Declarative deklarieren Sie die Basisklasse einfach mit dem Indikator __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))

Oben nutzen wir die Vererbungsmöglichkeitsfunktionen von SQLAlchemy nicht tatsächlich; wir können Instanzen von Manager und Engineer normal laden und speichern. Die Situation ändert sich jedoch, wenn wir **polymorph abfragen** müssen, d.h. wir möchten select(Employee) ausgeben und eine Sammlung von Manager- und Engineer-Instanzen erhalten. Dies bringt uns zurück in den Bereich der konkreten Vererbung, und wir müssen einen speziellen Mapper gegen Employee erstellen, um dies zu erreichen.

Um unser konkretes Vererbungsbeispiel so zu modifizieren, dass es eine "abstrakte" Basis darstellt, die polymorphes Laden ermöglicht, werden wir nur eine engineer- und eine manager-Tabelle haben und keine employee-Tabelle. Der Employee-Mapper wird jedoch direkt auf die "polymorphe Union" abgebildet, anstatt ihn lokal dem Parameter Mapper.with_polymorphic zuzuweisen.

Um dabei zu helfen, bietet Declarative eine Variante der Klasse ConcreteBase namens AbstractConcreteBase an, die dies automatisch erreicht.

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

Oben wird die Methode registry.configure() aufgerufen, die die tatsächliche Abbildung der Employee-Klasse auslöst; vor dem Konfigurationsschritt hat die Klasse keine Abbildung, da die Untertabellen, aus denen sie abrufen wird, noch nicht definiert sind. Dieser Prozess ist komplexer als der von ConcreteBase, da die gesamte Abbildung der Basisklasse verzögert werden muss, bis alle Unterklassen deklariert sind. Bei einem Mapping wie dem obigen können nur Instanzen von Manager und Engineer gespeichert werden; Abfragen gegen die Employee-Klasse ergeben immer Manager- und Engineer-Objekte.

Mit dem obigen Mapping können Abfragen in Bezug auf die Employee-Klasse und alle Attribute, die lokal auf ihr deklariert sind, wie z.B. Employee.name, erstellt werden.

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

Der Parameter AbstractConcreteBase.strict_attrs gibt an, dass die Employee-Klasse nur die Attribute direkt abbilden soll, die lokal zur Employee-Klasse sind, in diesem Fall das Attribut Employee.name. Andere Attribute wie Manager.manager_data und Engineer.engineer_info sind nur auf ihrer entsprechenden Unterklasse vorhanden. Wenn AbstractConcreteBase.strict_attrs nicht gesetzt ist, werden alle Unterklassenattribute wie Manager.manager_data und Engineer.engineer_info auf die Basisklasse Employee abgebildet. Dies ist ein Legacy-Nutzungsmodus, der für Abfragen praktischer sein kann, aber dazu führt, dass alle Unterklassen den vollständigen Satz von Attributen für die gesamte Hierarchie teilen; im obigen Beispiel würde die Nichtverwendung von AbstractConcreteBase.strict_attrs dazu führen, dass nicht nützliche Attribute wie Engineer.manager_name und Manager.engineer_info generiert werden.

Neu in Version 2.0: Parameter AbstractConcreteBase.strict_attrs zu AbstractConcreteBase hinzugefügt, der ein saubereres Mapping erzeugt; der Standardwert ist False, um Legacy-Mappings wie in den Versionen 1.x beizubehalten.

Klassische und semi-klassische konkrete polymorphe Konfiguration

Die mit ConcreteBase und AbstractConcreteBase dargestellten Declarative-Konfigurationen sind äquivalent zu zwei anderen Konfigurationsformen, die polymorphic_union() explizit verwenden. Diese Konfigurationsformen verwenden das Objekt Table explizit, damit die "polymorphe Union" zuerst erstellt und dann auf die Mappings angewendet werden kann. Diese werden hier dargestellt, um die Rolle der Funktion polymorphic_union() in Bezug auf das Mapping zu verdeutlichen.

Ein **semi-klassisches Mapping** verwendet zum Beispiel Declarative, etabliert aber die Table-Objekte separat.

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

Als Nächstes wird die UNION mithilfe von polymorphic_union() erstellt.

from sqlalchemy.orm import polymorphic_union

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

Mit den obigen Table-Objekten können die Mappings im "semi-klassischen" Stil erstellt werden, wobei wir Declarative in Verbindung mit dem Argument __table__ verwenden; unsere polymorphe Union wird oben über __mapper_args__ an den Parameter Mapper.with_polymorphic übergeben.

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,
    }

Alternativ können dieselben Table-Objekte im vollständig "klassischen" Stil verwendet werden, ohne Declarative überhaupt zu verwenden. Ein von Declarative bereitgestellter ähnlicher Konstruktor wird gezeigt.

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

Das "abstrakte" Beispiel kann auch im "semi-klassischen" oder "klassischen" Stil abgebildet werden. Der Unterschied besteht darin, dass anstatt die "polymorphe Union" dem Parameter Mapper.with_polymorphic zuzuweisen, wir sie direkt als abgebildetes wählbares Element auf unserem untersten Mapper anwenden. Das semi-klassische Mapping wird unten gezeigt.

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,
    }

Oben verwenden wir polymorphic_union() auf die gleiche Weise wie zuvor, außer dass wir die employee-Tabelle weglassen.

Siehe auch

Imperatives Mapping - Hintergrundinformationen zu imperativen oder "klassischen" Mappings

Beziehungen mit konkreter Vererbung

In einem konkreten Vererbungsszenario ist das Abbilden von Beziehungen schwierig, da die einzelnen Klassen keine Tabelle teilen. Wenn die Beziehungen nur bestimmte Klassen betreffen, z. B. eine Beziehung zwischen Company aus unseren früheren Beispielen und Manager, sind keine besonderen Schritte erforderlich, da dies nur zwei verwandte Tabellen sind.

Wenn Company jedoch eine Eins-zu-Viele-Beziehung zu Employee haben soll, die angibt, dass die Sammlung sowohl Engineer- als auch Manager-Objekte enthalten kann, bedeutet dies, dass Employee polymorphe Ladefähigkeiten haben muss und auch, dass jede zu beziehende Tabelle einen Fremdschlüssel zurück zur company-Tabelle haben muss. Ein Beispiel für eine solche Konfiguration ist wie folgt:

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,
    }

Die nächste Komplexität bei konkreter Vererbung und Beziehungen tritt auf, wenn wir möchten, dass Employee, Manager und Engineer (oder alle von ihnen) sich selbst auf Company beziehen. In diesem Fall hat SQLAlchemy ein besonderes Verhalten, nämlich dass eine relationship(), die auf Employee platziert ist und sich auf Company bezieht, **nicht** gegen die Klassen Manager und Engineer funktioniert, wenn sie auf Instanzebene aufgerufen wird. Stattdessen muss eine separate relationship() auf jede Klasse angewendet werden. Um ein bidirektionales Verhalten in Bezug auf drei separate Beziehungen zu erreichen, die das Gegenstück zu Company.employees darstellen, wird der Parameter relationship.back_populates zwischen jeder der Beziehungen verwendet.

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,
    }

Die oben genannte Einschränkung hängt mit der aktuellen Implementierung zusammen, insbesondere damit, dass konkret vererbende Klassen keine Attribute der Oberklasse gemeinsam haben und daher separate Beziehungen eingerichtet werden müssen.

Laden von konkreten Vererbungsmappings

Die Optionen für das Laden mit konkreter Vererbung sind begrenzt; im Allgemeinen, wenn polymorphes Laden auf dem Mapper mithilfe einer der deklarativen konkreten Mixins konfiguriert ist, kann es in aktuellen SQLAlchemy-Versionen nicht zur Abfragezeit geändert werden. Normalerweise könnte die Funktion with_polymorphic() den für die konkrete Vererbung verwendeten Ladestil überschreiben, aber aufgrund aktueller Einschränkungen wird dies noch nicht unterstützt.