SQLAlchemy: многоотраслевой полиморфизм и отношения через таблицы ассоциаций - PullRequest
0 голосов
/ 06 февраля 2020

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

Я пытался свести это к примеру, в котором я пытаюсь использовать пример прокси-сервера ассоциации в документации SA для моделирования определений классов и объявленных атрибутов. Я явно что-то упустил и действительно мог бы помочь. Я ожидаю, что последний оператор print создаст экземпляр Julius Lab.

Кто-нибудь видит ошибку в этой настройке? Большое спасибо заранее.

from sqlalchemy import Column, String, Integer, ForeignKey, create_engine
from sqlalchemy.schema import MetaData
from sqlalchemy.orm import relationship, backref, Session
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base, declared_attr


class CustomBase:

    id = Column(Integer, primary_key=True)

    @declared_attr
    def __tablename__(cls):
        return cls.__name__.lower()


convention = {
    "ix": "ix_%(column_0_label)s",
    "uq": "uq_%(table_name)s_%(column_0_name)s",
    "ck": "ck_%(table_name)s_%(constraint_name)s",
    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s",
}

metadata = MetaData(naming_convention=convention)
Base = declarative_base(cls=CustomBase, metadata=metadata)


class Animal(Base):
    __tablename__ = "animal"
    name = Column(String)
    discriminator = Column("type", String(50))

    __mapper_args__ = {
        "polymorphic_identity": "animal",
        "polymorphic_on": discriminator
    }


class Slobber(Base):

    consistency = Column(Integer)
    association = relationship("SlobberAssociation", backref="slobber")
    association_id = Column(Integer, ForeignKey("slobberassociation.id"))
    source = association_proxy("association", "source")


class SlobberAssociation(Base):
    """Polymorphic trait that determines the source of Slobber, 
       be it a Lab or Spaniel or Person"""

    discriminator = Column(String(50), nullable=False)
    __mapper_args__ = {"polymorphic_on": discriminator}

    def __init__(self, instance):
        self.discriminator = instance.__class__.__name__.lower()


class IsSlobberSource:
    @declared_attr
    def slobberassociation_id(cls):
        return Column(Integer, ForeignKey("slobberassociation.id"))

    @declared_attr
    def slobberassociation(cls):
        name = cls.__name__
        discriminator = name.lower()

        assoc_cls = type(
            f"{name}Association",
            (SlobberAssociation,),
            dict(
                __tablename__=None,
                __mapper_args__={"polymorphic_identity": discriminator},
            ),
        )

        cls.slobbers = association_proxy(
            "slobberassociation",
            "slobbers",
            creator=lambda slobbers: assoc_cls(slobbers=slobbers),
        )
        return relationship(assoc_cls, backref=backref("source", uselist=False))


class Person(IsSlobberSource, Base):
    name = Column(String())


class Lab(IsSlobberSource, Animal):
    __table_args__ = {"extend_existing": True}
    __mapper_args__ = {"polymorphic_identity": "lab"}



class Spaniel(IsSlobberSource, Animal):
    __table_args__ = {"extend_existing": True}
    __mapper_args__ = {"polymorphic_identity": "spaniel"}


class Cat(Animal):
    __table_args__ = {"extend_existing": True}
    __mapper_args__ = {"polymorphic_identity": "cat"}


engine = create_engine('sqlite://', echo=True)
Base.metadata.create_all(engine)

session = Session(engine)

julius = Lab(name='Julius')
poppy = Lab(name='Poppy')
lucas = Lab(name='Lucas')
wyatt = Spaniel(name='Wyatt')
holmes = Cat(name="Holmes")

session.add_all([julius, poppy, lucas, wyatt, holmes])
session.commit()

slobber_ball = Slobber(consistency=8)
slobber_ball.source = julius

session.add(slobber_ball)
session.commit()

s = session.query(Slobber).one()
print(s.source)

1 Ответ

0 голосов
/ 10 февраля 2020

Так что, похоже, это работает, но недостатком является то, что я не могу напрямую запрашивать источник для самого общего c типа "Слоббер", атрибут источника доступен только для этих производных классов Lab.Slobber, Person.Slobber и т. Д. c. генерируется объявленным атрибутом slobbers. Этот подход, по крайней мере, навязывает отношения по внешнему ключу, поэтому у нас так много разных таблиц связей. С другой стороны, я начинаю задаваться вопросом, является ли это выгодным компромиссом по сравнению с простой обработкой этого с помощью одной таблицы ассоциации со столбцом дескриптора, и приложение logi c обрабатывает необходимые ограничения.

Любые комментарии / дополнительные ответы более чем приветствуются!

Этому помогла ссылка на этот пример SA: https://docs.sqlalchemy.org/en/13/_modules/examples/generic_associations/table_per_related.html


class Animal(Base):
    __tablename__ = "animal"
    name = Column(String)
    discriminator = Column("type", String(50))

    __mapper_args__ = {
        "polymorphic_identity": "animal",
        "polymorphic_on": discriminator
    }


class Slobber(Base):
    consistency = Column(Integer)


class IsSlobberSource:
    slobber_assoc_tables = {}

    @declared_attr
    def slobbers(cls):
        tablename = cls.__tablename__
        table_key = f"{tablename}_slobbers"
        cls.Slobber = IsSlobberSource.slobber_assoc_tables.get(table_key, None)
        if cls.Slobber is None:
            cls.Slobber = type(
                f"{cls.__name__}Slobber",
                (Slobber, Base, ),
                dict(__tablename__=f"{tablename}_slobber",
                    source_id=Column(Integer, ForeignKey(f"{tablename}.id")),
                    source=relationship(cls),
                    slobber_id=Column(Integer, ForeignKey("slobber.id"), primary_key=True),
                ),
            )
            IsSlobberSource.slobber_assoc_tables[table_key] = cls.Slobber
        return relationship(cls.Slobber)

class Person(IsSlobberSource, Base):
    name = Column(String())


class Lab(IsSlobberSource, Animal):
    __table_args__ = {"extend_existing": True}
    __mapper_args__ = {"polymorphic_identity": "lab"}



class Spaniel(IsSlobberSource, Animal):
    __table_args__ = {"extend_existing": True}
    __mapper_args__ = {"polymorphic_identity": "spaniel"}


class Cat(Animal):
    __table_args__ = {"extend_existing": True}
    __mapper_args__ = {"polymorphic_identity": "cat"}

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...