Сопоставление иерархий наследования классов

SQLAlchemy поддерживает три формы наследования: наследование по одной таблице, когда несколько типов классов представлены одной таблицей, наследование по отдельным таблицам, когда каждый тип класса представлен независимыми таблицами, и наследование по объединенным таблицам, когда иерархия классов разбита на зависимые таблицы, каждый класс представлен своей собственной таблицей, которая включает только те атрибуты, которые локальны для этого класса.

Наиболее распространенными формами наследования являются одиночная и объединенная таблица, в то время как конкретное наследование представляет больше конфигурационных проблем.

Когда мапперы настроены в отношениях наследования, SQLAlchemy имеет возможность загружать элементы polymorphically, что означает, что один запрос может возвращать объекты нескольких типов.

См.также

Рецепты отображения наследования - полные примеры объединенного, одиночного и конкретного наследования

Наследование объединенных таблиц

При наследовании по объединенным таблицам каждый класс в иерархии классов представлен отдельной таблицей. Запрос к определенному подклассу в иерархии будет выглядеть как SQL JOIN по всем таблицам в пути наследования. Если запрашиваемый класс является базовым, то по умолчанию в оператор SELECT включается только базовая таблица. Во всех случаях конечный класс, который нужно инстанцировать для данной строки, определяется столбцом дискриминатора или выражением, которое работает с базовой таблицей. Когда подкласс загружается только по базовой таблице, у результирующих объектов сначала будут заполнены базовые атрибуты; атрибуты, локальные для подкласса, будут lazy load при обращении к ним. Кроме того, существуют опции, которые могут изменить поведение по умолчанию, позволяя запросу включать столбцы, соответствующие нескольким таблицам/подклассам.

Базовый класс в объединенной иерархии наследования настраивается с помощью дополнительных аргументов, которые будут ссылаться на колонку полиморфного дискриминатора, а также на идентификатор базового класса:

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

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

Выше установлен дополнительный столбец type, выполняющий роль дискриминатора, который конфигурируется как таковой с помощью параметра mapper.polymorphic_on. Этот столбец будет хранить значение, которое указывает на тип объекта, представленного в строке. Столбец может иметь любой тип данных, хотя наиболее распространенными являются строка и целое число. Фактическое значение данных, которое будет применяться к этому столбцу для конкретной строки в базе данных, задается с помощью параметра mapper.polymorphic_identity, описанного ниже.

Хотя выражение полиморфного дискриминатора не является строго необходимым, оно требуется, если желательна полиморфная загрузка. Создание простого столбца в базовой таблице - самый простой способ достичь этого, однако очень сложные схемы наследования могут даже настроить выражение SQL, такое как оператор CASE, в качестве полиморфного дискриминатора.

Примечание

В настоящее время только один столбец дискриминатора или SQL-выражение может быть настроен для всей иерархии наследования, обычно для самого базового класса в иерархии. «Каскадные» полиморфные выражения дискриминатора пока не поддерживаются.

Далее мы определим Engineer и Manager подклассы Employee. Каждый из них содержит столбцы, которые представляют атрибуты, уникальные для подкласса, который они представляют. Каждая таблица также должна содержать столбец (или столбцы) первичного ключа, а также ссылку внешнего ключа на родительскую таблицу:

class Engineer(Employee):
    __tablename__ = "engineer"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
    engineer_name = Column(String(30))

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


class Manager(Employee):
    __tablename__ = "manager"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
    manager_name = Column(String(30))

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

В приведенном выше примере каждое отображение указывает параметр mapper.polymorphic_identity в аргументах отображения. Это значение заполняет столбец, обозначенный параметром mapper.polymorphic_on, установленным в базовом маппере. Параметр mapper.polymorphic_identity должен быть уникальным для каждого сопоставленного класса во всей иерархии, и для каждого сопоставленного класса должна быть только одна «идентичность»; как отмечалось выше, «каскадные» идентичности, когда некоторые подклассы вводят вторую идентичность, не поддерживаются.

ORM использует значение, заданное mapper.polymorphic_identity, для того, чтобы определить, к какому классу относится строка при полиморфной загрузке строк. В приведенном выше примере каждая строка, представляющая Employee, будет иметь значение 'employee' в своей строке type; аналогично, каждая Engineer получит значение 'engineer', а каждая Manager получит значение 'manager'. Независимо от того, использует ли отображение наследования отдельные объединенные таблицы для подклассов, как при наследовании объединенных таблиц, или все одну таблицу, как при наследовании одной таблицы, ожидается, что это значение будет сохранено и доступно ORM при запросе. Параметр mapper.polymorphic_identity также применяется к наследованию конкретных таблиц, но на самом деле не сохраняется; подробности см. в последующем разделе Наследование конкретной таблицы.

При полиморфной настройке чаще всего ограничение внешнего ключа устанавливается на тот же столбец или столбцы, что и первичный ключ, однако это не обязательно; столбец, отличный от первичного ключа, также может ссылаться на родителя через внешний ключ. Способ построения JOIN от базовой таблицы к подклассам также может быть непосредственно настроен, однако это редко бывает необходимо.

После завершения отображения объединенного наследования запрос к Employee вернет комбинацию объектов Employee, Engineer и Manager. Вновь сохраненные объекты Engineer, Manager и Employee будут автоматически заполнять колонку employee.type правильным значением «дискриминатора», в данном случае "engineer", "manager" или "employee", в зависимости от ситуации.

Отношения с объединенным наследованием

Отношения полностью поддерживаются при объединенном наследовании таблиц. Отношения, включающие класс с объединенным наследованием, должны быть направлены на класс в иерархии, который также соответствует ограничению внешнего ключа; ниже, поскольку таблица employee имеет ограничение внешнего ключа обратно к таблице company, отношения устанавливаются между Company и Employee:

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


class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    type = Column(String(50))
    company_id = Column(ForeignKey("company.id"))
    company = relationship("Company", 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 = Column(Integer, primary_key=True)
    name = Column(String(50))
    managers = relationship("Manager", back_populates="company")


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

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


class Manager(Employee):
    __tablename__ = "manager"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
    manager_name = Column(String(30))

    company_id = Column(ForeignKey("company.id"))
    company = relationship("Company", back_populates="managers")

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


class Engineer(Employee):
    ...

Выше, класс Manager будет иметь атрибут Manager.company; Company будет иметь атрибут Company.managers, который всегда загружается против соединения таблиц employee и manager вместе.

Загрузка сопоставлений объединенного наследования

См. разделы Загрузка иерархий наследования и Загрузка объектов с объединенным наследованием таблиц для получения информации о методах загрузки наследования, включая конфигурацию таблиц, которые будут запрашиваться как во время настройки маппера, так и во время запроса.

Наследование одной таблицы

Наследование по одной таблице представляет все атрибуты всех подклассов в одной таблице. Определенный подкласс, имеющий атрибуты, уникальные для этого класса, будет сохранять их в столбцах таблицы, которые в противном случае являются NULL, если строка относится к объекту другого типа.

Запрос на определенный подкласс в иерархии будет выглядеть как SELECT к базовой таблице, который будет включать в себя предложение WHERE, ограничивающее строки теми, которые имеют определенное значение или значения, присутствующие в столбце дискриминатора или выражении.

Наследование по одной таблице имеет преимущество в простоте по сравнению с наследованием по объединенным таблицам; запросы намного эффективнее, поскольку для загрузки объектов каждого представленного класса необходимо задействовать только одну таблицу.

Конфигурация наследования по одной таблице выглядит так же, как и наследование по объединенной таблице, за исключением того, что в базовом классе указывается __tablename__. В базовой таблице также требуется столбец дискриминатора, чтобы классы можно было отличать друг от друга.

Даже если подклассы разделяют базовую таблицу для всех своих атрибутов, при использовании Declarative, объекты Column все равно могут быть указаны на подклассы, указывая, что колонка должна быть отображена только на этот подкласс; Column будет применен к тому же базовому объекту Table:

class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    type = Column(String(20))

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


class Manager(Employee):
    manager_data = Column(String(50))

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


class Engineer(Employee):
    engineer_info = Column(String(50))

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

Обратите внимание, что отображатели для производных классов Manager и Engineer опускают __tablename__, указывая на то, что у них нет собственной отображаемой таблицы.

Разрешение конфликтов колонок

Обратите внимание в предыдущем разделе, что столбцы manager_name и engineer_info «перемещаются вверх», чтобы быть примененными к Employee.__table__, в результате их объявления в подклассе, который не имеет собственной таблицы. Сложный случай возникает, когда два подкласса хотят указать один и тот же столбец, как показано ниже:

class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    type = Column(String(20))

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


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


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

Выше, столбец start_date, объявленный и в Engineer, и в Manager, приведет к ошибке:

sqlalchemy.exc.ArgumentError: Column 'start_date' on class
<class '__main__.Manager'> conflicts with existing
column 'employee.start_date'

Приведенный выше сценарий представляет двусмысленность для системы декларативного отображения, которую можно разрешить, используя declared_attr для условного определения Column, позаботившись о возврате существующего столбца через родительский __table__, если он уже существует:

from sqlalchemy.orm import declared_attr


class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    type = Column(String(20))

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


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

    @declared_attr
    def start_date(cls):
        "Start date column, if not present already."
        return Employee.__table__.c.get("start_date", Column(DateTime))


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

    @declared_attr
    def start_date(cls):
        "Start date column, if not present already."
        return Employee.__table__.c.get("start_date", Column(DateTime))

Выше, когда отображается Manager, колонка start_date уже присутствует в классе Employee; возвращая существующий объект Column, декларативная система распознает, что это одна и та же колонка, которая должна быть отображена на два разных подкласса по отдельности.

Аналогичная концепция может быть использована с классами-миксинами (см. Составление сопоставленных иерархий с помощью миксинов) для определения определенной серии столбцов и/или других сопоставленных атрибутов из многократно используемого класса-миксина:

class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    type = Column(String(20))

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


class HasStartDate:
    @declared_attr
    def start_date(cls):
        return cls.__table__.c.get("start_date", Column(DateTime))


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


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

Отношения с наследованием одной таблицы

Отношения полностью поддерживаются при наследовании одной таблицы. Конфигурация выполняется так же, как и при объединенном наследовании; атрибут внешнего ключа должен находиться в том же классе, который является «иностранной» стороной отношения:

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


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

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


class Manager(Employee):
    manager_data = Column(String(50))

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


class Engineer(Employee):
    engineer_info = Column(String(50))

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

Также, как и в случае с объединенным наследованием, мы можем создавать отношения, включающие определенный подкласс. При запросе оператор SELECT будет включать предложение WHERE, которое ограничивает выбор класса этим подклассом или подклассами:

class Company(Base):
    __tablename__ = "company"
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    managers = relationship("Manager", back_populates="company")


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

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


class Manager(Employee):
    manager_name = Column(String(30))

    company_id = Column(ForeignKey("company.id"))
    company = relationship("Company", back_populates="managers")

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


class Engineer(Employee):
    engineer_info = Column(String(50))

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

Выше, класс Manager будет иметь атрибут Manager.company; Company будет иметь атрибут Company.managers, который всегда загружается против employee с дополнительным предложением WHERE, которое ограничивает строки теми, которые имеют type = 'manager'.

Загрузка отображений одиночного наследования

Техники загрузки для наследования по одной таблице в основном идентичны тем, которые используются для наследования по объединенной таблице, и между этими двумя типами отображения обеспечивается высокая степень абстракции, что позволяет легко переключаться между ними, а также смешивать их в одной иерархии (просто опустите __tablename__ в тех подклассах, которые будут наследоваться по одной таблице). См. разделы Загрузка иерархий наследования и Загрузка объектов с наследованием одной таблицы для документации по технике загрузки наследования, включая конфигурацию классов, которые будут запрашиваться как во время настройки маппера, так и во время запроса.

Наследование конкретной таблицы

Конкретное наследование отображает каждый подкласс в отдельную таблицу, каждая из которых содержит все столбцы, необходимые для создания экземпляра данного класса. Конфигурация конкретного наследования по умолчанию выполняет запросы неполиморфно; запрос для конкретного класса будет запрашивать только таблицу этого класса и возвращать только экземпляры этого класса. Полиморфная загрузка конкретных классов включается путем настройки в маппере специального SELECT, который обычно производится как объединение всех таблиц.

Предупреждение

Наследование конкретных таблиц значительно сложнее, чем наследование объединенных или одиночных таблиц, и значительно более ограничено в функциональности, особенно в отношении использования с отношениями, нетерпеливой загрузкой и полиморфной загрузкой. При полиморфном использовании это приводит к очень большим запросам с UNIONS, которые не будут работать так же хорошо, как простые объединения. Настоятельно рекомендуется, если требуется гибкость в загрузке отношений и полиморфной загрузке, использовать объединенное или однотабличное наследование, если это вообще возможно. Если полиморфная загрузка не требуется, то можно использовать обычные ненаследуемые отображения, если каждый класс полностью ссылается на свою собственную таблицу.

В то время как объединенное наследование и наследование одной таблицы свободно работают с «полиморфной» загрузкой, это более неудобное дело в конкретном наследовании. По этой причине конкретное наследование более уместно, когда полиморфная загрузка не требуется. Установление отношений, включающих классы конкретного наследования, также более неудобно.

Чтобы определить класс как использующий конкретное наследование, добавьте параметр mapper.concrete внутри __mapper_args__. Это указывает Declarative и отображению, что таблица суперклассов не должна рассматриваться как часть отображения:

class Employee(Base):
    __tablename__ = "employee"

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


class Manager(Employee):
    __tablename__ = "manager"

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

    __mapper_args__ = {
        "concrete": True,
    }


class Engineer(Employee):
    __tablename__ = "engineer"

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

    __mapper_args__ = {
        "concrete": True,
    }

Следует отметить два важных момента:

  • Мы должны определить все столбцы явно в каждом подклассе, даже одноименные. Такой столбец, как Employee.name здесь, не копируется в таблицы, отображаемые Manager или Engineer для нас.

  • Хотя классы Engineer и Manager отображены в отношения наследования с Employee, они все еще не включают полиморфную загрузку. То есть, если мы запрашиваем объекты Employee, таблицы manager и engineer вообще не запрашиваются.

Конфигурация бетонной полиморфной нагрузки

Полиморфная загрузка с конкретным наследованием требует, чтобы специализированный SELECT был настроен на каждый базовый класс, который должен иметь полиморфную загрузку. Этот SELECT должен быть способен обращаться ко всем сопоставленным таблицам по отдельности и обычно представляет собой оператор UNION, построенный с помощью помощника SQLAlchemy polymorphic_union().

Как обсуждалось в Загрузка иерархий наследования, конфигурации наследования mapper любого типа могут быть настроены на загрузку из специального selectable по умолчанию с помощью аргумента mapper.with_polymorphic. Текущий публичный API требует, чтобы этот аргумент был установлен в Mapper при его первом создании.

Однако в случае с Declarative и отображаемый Table создается сразу, в момент определения отображаемого класса. Это означает, что аргумент mapper.with_polymorphic еще не может быть предоставлен, поскольку объекты Table, соответствующие подклассам, еще не определены.

Существует несколько стратегий для решения этого цикла, однако Declarative предоставляет вспомогательные классы ConcreteBase и AbstractConcreteBase, которые решают эту проблему за сценой.

Используя ConcreteBase, мы можем установить наше конкретное отображение почти так же, как и другие формы отображения наследования:

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

Base = declarative_base()


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

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


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

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


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

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

Выше, Declarative устанавливает полиморфный selectable для класса Employee во время «инициализации» маппера; это поздний шаг конфигурации для мапперов, который разрешает другие зависимые мапперы. Помощник ConcreteBase использует функцию polymorphic_union() для создания UNION всех concrete-mapped таблиц после настройки всех других классов, а затем конфигурирует это утверждение с уже существующим маппером базового класса.

После выбора полиморфное объединение выдает запрос, подобный этому:

session.query(Employee).all()
SELECT pjoin.id AS pjoin_id, pjoin.name AS pjoin_name, pjoin.type AS pjoin_type, pjoin.manager_data AS pjoin_manager_data, pjoin.engineer_info AS pjoin_engineer_info FROM ( SELECT employee.id AS id, employee.name AS name, CAST(NULL AS VARCHAR(50)) AS manager_data, CAST(NULL AS VARCHAR(50)) 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(50)) AS engineer_info, 'manager' AS type FROM manager UNION ALL SELECT engineer.id AS id, engineer.name AS name, CAST(NULL AS VARCHAR(50)) AS manager_data, engineer.engineer_info AS engineer_info, 'engineer' AS type FROM engineer ) AS pjoin

Приведенный выше запрос UNION должен производить столбцы «NULL» для каждой подтаблицы, чтобы учесть те столбцы, которые не являются членами данного конкретного подкласса.

См.также

ConcreteBase

Абстрактные конкретные классы

Конкретные отображения, проиллюстрированные до сих пор, показывают как подклассы, так и базовый класс, отображенные на отдельные таблицы. В конкретном случае использования наследования обычно базовый класс не представлен в базе данных, только подклассы. Другими словами, базовый класс является «абстрактным».

Обычно, когда нужно отобразить два разных подкласса на отдельные таблицы, а базовый класс оставить без отображения, это можно сделать очень просто. При использовании Declarative достаточно объявить базовый класс с помощью индикатора __abstract__:

from sqlalchemy.orm import declarative_base

Base = declarative_base()


class Employee(Base):
    __abstract__ = True


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


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

Выше мы фактически не используем возможности SQLAlchemy по отображению наследования; мы можем загружать и сохранять экземпляры Manager и Engineer обычным образом. Однако ситуация меняется, когда нам нужно запросить полиморфно, то есть мы хотим выдать session.query(Employee) и получить обратно коллекцию экземпляров Manager и Engineer. Это возвращает нас в область конкретного наследования, и мы должны построить специальный отображатель для Employee, чтобы достичь этого.

Чтобы изменить наш конкретный пример наследования для иллюстрации «абстрактной» базы, способной к полиморфной загрузке, у нас будет только таблица engineer и manager и никакой таблицы employee, однако отображатель Employee будет отображен непосредственно на «полиморфное объединение», а не указан локально в параметре mapper.with_polymorphic.

Чтобы помочь в этом, Declarative предлагает вариант класса ConcreteBase под названием AbstractConcreteBase, который достигает этого автоматически:

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

Base = declarative_base()


class Employee(AbstractConcreteBase, Base):
    pass


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

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


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

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


Base.registry.configure()

Выше вызывается метод registry.configure(), который вызывает отображение класса Employee; до этапа настройки класс не имеет отображения, поскольку подтаблицы, к которым он будет обращаться, еще не определены. Этот процесс сложнее, чем процесс ConcreteBase, поскольку полное отображение базового класса должно быть отложено до тех пор, пока не будут объявлены все подклассы. При таком отображении, как описано выше, могут сохраняться только экземпляры Manager и Engineer; запрос к классу Employee всегда будет выдавать объекты Manager и Engineer.

См.также

AbstractConcreteBase

Классическая и полуклассическая полиморфная конфигурация бетона

Декларативные конфигурации, проиллюстрированные ConcreteBase и AbstractConcreteBase, эквивалентны двум другим формам конфигурации, которые используют polymorphic_union() явно. Эти конфигурационные формы используют объект Table явно, так что «полиморфный союз» может быть сначала создан, а затем применен к отображениям. Они проиллюстрированы здесь, чтобы прояснить роль функции polymorphic_union() в плане отображения.

Например, semi-classical mapping использует 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)),
)

Далее производится объединение с помощью 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__; наш полиморфный союз выше передается через __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(object):
    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",
)

Абстрактный» пример также может быть отображен с использованием «полуклассического» или «классического» стиля. Разница в том, что вместо применения «полиморфного союза» к параметру mapper.with_polymorphic, мы применяем его непосредственно как отображаемый selectable на нашем базовом отображателе. Полуклассическое отображение показано ниже:

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.

См.также

Императивное картирование - справочная информация об императивных, или «классических» отображениях

Отношения с конкретным наследованием

В конкретном сценарии наследования отображение отношений является сложной задачей, поскольку разные классы не имеют общей таблицы. Если отношения затрагивают только определенные классы, как, например, отношения между Company в наших предыдущих примерах и Manager, специальные шаги не нужны, поскольку это просто две связанные таблицы.

Однако, если Company будет иметь отношение «один-ко-многим» к Employee, указывая, что коллекция может включать как Engineer, так и Manager объекты, это подразумевает, что Employee должен иметь полиморфные возможности загрузки, а также что каждая таблица, с которой будет осуществляться связь, должна иметь внешний ключ к таблице company. Пример такой конфигурации выглядит следующим образом:

from sqlalchemy.ext.declarative import ConcreteBase


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


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

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


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

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


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

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

Следующая сложность с конкретным наследованием и отношениями возникает, когда мы хотим, чтобы один или все из Employee, Manager и Engineer сами ссылались обратно на Company. Для этого случая SQLAlchemy имеет особое поведение, которое заключается в том, что relationship(), размещенный на Employee, который ссылается на Company не работает против классов Manager и Engineer, когда выполняется на уровне экземпляра. Вместо этого к каждому классу должен быть применен отдельный relationship(). Для достижения двунаправленного поведения в терминах трех отдельных отношений, которые служат противоположностью Company.employees, между каждым из отношений используется параметр relationship.back_populates:

from sqlalchemy.ext.declarative import ConcreteBase


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


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

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


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

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


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

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

Вышеуказанное ограничение связано с текущей реализацией, в том числе с тем, что конкретные наследуемые классы не разделяют ни один из атрибутов суперкласса и поэтому нуждаются в установке отдельных отношений.

Загрузка конкретных отображений наследования

Возможности загрузки с наследованием в concrete ограничены; как правило, если полиморфная загрузка настроена на маппере с помощью одного из декларативных миксинов concrete, ее нельзя изменить во время запроса в текущих версиях SQLAlchemy. Обычно функция with_polymorphic() могла бы переопределить стиль загрузки, используемый concrete, однако из-за текущих ограничений это пока не поддерживается.

Вернуться на верх