Как реализовать наследование таблиц в SQLAlchemy (single-table, joined-table, concrete-table)?
SQLAlchemy поддерживает три стратегии: STI (одна таблица, колонка-дискриминатор), JTI (JOIN между таблицами иерархии) и CTI (независимые таблицы). JTI — наиболее нормализованная, STI — быстрейшая для чтения.
Наследование таблиц в SQLAlchemy
SQLAlchemy поддерживает три стратегии ORM-наследования, каждая из которых отражает разный дизайн базы данных.
Single Table Inheritance (STI)
Все подклассы хранятся в одной таблице. Колонка-дискриминатор указывает тип строки. Колонки, специфичные для подклассов, должны быть nullable.
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String
class Base(DeclarativeBase):
pass
class Animal(Base):
__tablename__ = "animals"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
type: Mapped[str] = mapped_column(String(50)) # дискриминатор
__mapper_args__ = {
"polymorphic_on": "type",
"polymorphic_identity": "animal",
}
class Dog(Animal):
breed: Mapped[str | None] = mapped_column(String(100))
__mapper_args__ = {"polymorphic_identity": "dog"}
class Cat(Animal):
indoor: Mapped[bool | None] = mapped_column()
__mapper_args__ = {"polymorphic_identity": "cat"}
Плюсы: один JOIN-less запрос, простые миграции. Минусы: много NULL-колонок, сложные CHECK-ограничения.
Joined Table Inheritance (JTI)
Каждый класс — отдельная таблица. При запросе ORM делает JOIN. Рекомендуется для глубоких иерархий с разными наборами колонок.
class Employee(Base):
__tablename__ = "employees"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
type: Mapped[str] = mapped_column(String(50))
__mapper_args__ = {
"polymorphic_on": "type",
"polymorphic_identity": "employee",
}
class Manager(Employee):
__tablename__ = "managers"
id: Mapped[int] = mapped_column(ForeignKey("employees.id"), primary_key=True)
department: Mapped[str] = mapped_column(String(100))
__mapper_args__ = {"polymorphic_identity": "manager"}
class Engineer(Employee):
__tablename__ = "engineers"
id: Mapped[int] = mapped_column(ForeignKey("employees.id"), primary_key=True)
stack: Mapped[str] = mapped_column(String(200))
__mapper_args__ = {"polymorphic_identity": "engineer"}
Плюсы: нормализованная схема, нет NULL. Минусы: JOIN на каждый запрос, проблемы с производительностью на глубоких иерархиях.
Concrete Table Inheritance (CTI)
Каждый подкласс — полностью независимая таблица со всеми колонками. Нет JOIN, нет общей таблицы.
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy import String
class Vehicle(Base):
__abstract__ = True
id: Mapped[int] = mapped_column(primary_key=True)
make: Mapped[str] = mapped_column(String(100))
class Car(Vehicle):
__tablename__ = "cars"
doors: Mapped[int] = mapped_column()
class Truck(Vehicle):
__tablename__ = "trucks"
payload_tons: Mapped[float] = mapped_column()
Для полиморфных запросов через базовый класс нужен ConcreteBase или AbstractConcreteBase:
from sqlalchemy.orm import ConcreteBase
class Vehicle(ConcreteBase, Base):
__abstract__ = True
...
Как выбрать стратегию
- STI — иерархия неглубокая, различия минимальны, важна скорость SELECT.
- JTI — разные наборы полей, нормализация важна, иерархия не более 2–3 уровней.
- CTI — подклассы используются независимо, полиморфные запросы редки.
Подводные камни
- STI + NOT NULL: колонки подклассов должны быть nullable, иначе INSERT для других подклассов упадёт.
- JTI и polymorphic_load: по умолчанию SQLAlchemy загружает все подтаблицы одним JOIN — для больших иерархий используйте
polymorphic_load="selectin". - CTI без ConcreteBase:
select(Vehicle)не найдёт ни Car, ни Truck — нужен AbstractConcreteBase или ручной UNION. - Alembic и CTI: миграции сложнее, каждая таблица управляется отдельно.
- with_polymorphic: без него запрос к базовому классу JTI не включает колонки подклассов — нужно явно указать
with_polymorphic("*")илиselectin_polymorphic. - Дискриминатор как строка vs Enum: строковый дискриминатор гибче, но без DB-ограничения можно случайно записать неизвестный тип.
Common mistakes
- Описывать orm inheritance только как термин и не показывать механизм на минимальном примере.
- Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
- Не связывать поведение с официальным контрактом SQLAlchemy и реальной эксплуатацией.
What the interviewer is testing
- Объясняет orm inheritance через последовательность действий, а не через набор ключевых слов.
- Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
- Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.