SQLAlchemy ORM: Наследование полиморфных одиночных таблиц с возвратом к родительскому классу, если "polymorphic_identity" не найден

Использование Python 3.5 и SQLAlchemy 1.0.14 (ORM).

У меня есть таблица элементов, объявленных как таковая:

from sqlalchemy.ext.declarative.api import declarative_base

Base = declarative_base()

class Item(Base):
    __tablename__ = 'items'

    id = Column(Integer, primary_key=True)
    type = Column(String)
    # other non relevant attributes

Мои элементы могут быть разных типов, идентификатор типа хранится в type. Для некоторых из этих типов объектов мне нужно иметь определенные методы или атрибуты.

Чтобы добиться этого, я попытался использовать однонаправленное наследование с несколькими SpecialisedItem как подкласс Item:

class Item(Base):
    __tablename__ = 'items'

    id = Column(Integer, primary_key=True)
    type = Column(String, index=True)
    # other non relevant attributes

    __mapper_args__ = {
        'polymorphic_on': type,
    }

class SpecialisedItem(Base):
    __mapper_args__ = {
        'polymorphic_identity': 'specialitem',
    }

    def specialised_method(self):
        return "I am special"

Теперь, когда я загружаю свои элементы, я хотел бы, чтобы все специализированные элементы (имеющие type=='specialitem') загружались как таковые, в то время как любое другое значение типа приводило к загрузке родительского класса Item. Это не работает, я получаю AssertionError: No such polymorphic_identity 'normal' is defined при загрузке элементов.

Я бы хотел избежать создания унаследованных классов, которые ничего не делают для покрытия всех возможных значений type, вместо этого "unmapped" type возвращается к родительскому классу Item.

Есть ли способ достичь этого эффекта?

Минимальный тестовый пример для справки:

from sqlalchemy.engine import create_engine
from sqlalchemy.ext.declarative.api import declarative_base
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import Integer, String


Base = declarative_base()

class Item(Base):
    __tablename__ = 'items'

    id = Column(Integer, primary_key=True)
    type = Column(String, index=True)
    # other non relevant attributes

    __mapper_args__ = {
        'polymorphic_on': type,
    }

class SpecialisedItem(Item):
    __mapper_args__ = {
        'polymorphic_identity': 'special',
    }

    specialAttribute = Column(String)

    def specialised_method(self):
        return "I am special"


engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

session.add(Item(type='normal'))
session.add(Item(type='special'))
session.commit()
# loading only specialized items works
for item in session.query(Item).filter_by(type="special"):
    print(item.specialised_method())

# loading other items fails
for item in session.query(Item):
    print(item.type)

Спасибо,

Гийом

Ответ 1

Отображение идентификаторов "полиморфного идентификатора" в экземплярах Mapper хранится в polymorphic_map dict. Вы можете создать пользовательский polymorphic_map, который вернет родительский класс mapper для undefined полиморфных тождеств.

from sqlalchemy.engine import create_engine
from sqlalchemy.ext.declarative.api import declarative_base
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import Integer, String
from sqlalchemy import event

Base = declarative_base()

class Item(Base):
    __tablename__ = 'items'

    id = Column(Integer, primary_key=True)
    type = Column(String, index=True)
    # other non relevant attributes

    __mapper_args__ = {
        'polymorphic_on': type,
    }

class SpecialisedItem(Item):
    __mapper_args__ = {
        'polymorphic_identity': 'special',
    }

    specialAttribute = Column(String)

    def specialised_method(self):
        return "I am special"

#http://docs.sqlalchemy.org/en/rel_1_1/orm/events.html#sqlalchemy.orm.events.MapperEvents.mapper_configured
@event.listens_for(Item, 'mapper_configured')
def receive_mapper_configured(mapper, class_):
    class FallbackToParentPolymorphicMap(dict):
        def __missing__(self, key):
            # return parent Item mapper for undefined polymorphic_identity
            return mapper

    new_polymorphic_map = FallbackToParentPolymorphicMap()
    new_polymorphic_map.update(mapper.polymorphic_map)
    mapper.polymorphic_map = new_polymorphic_map

    # for prevent 'incompatible polymorphic identity' warning, not necessarily
    mapper._validate_polymorphic_identity = None

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

session.add(Item(type='normal'))
session.add(Item(type='special'))
session.commit()
# loading only specialized items works
for item in session.query(Item).filter_by(type="special"):
    print(item.specialised_method())

# loading other items fails
for item in session.query(Item):
    print(item.type)