Python SQLAlchemy:通过关联表的多分支多态性和关系

Python SQLAlchemy:通过关联表的多分支多态性和关系,python,sqlalchemy,polymorphic-associations,Python,Sqlalchemy,Polymorphic Associations,我有一个用例,我想将多态关联特性应用到一个可能不相关的类型集合。使这个用例复杂化的是,其中一些类型已经在应用程序的其他地方使用共享表继承来实现另一个目的。基本上,我有一个类型,它有一个“source”属性,我需要这个“source”属性映射到任意数量的潜在源,其中一些已经因为其他原因使用了多态性,而其中一些在形式或功能上不相似,所以继承没有意义。我希望这种关联更像一种特征,任何标记/混合了这种特征的东西都是我的对象的潜在来源 我试图用一个示例来概括这一点,在这个示例中,我试图使用SA文档中的关联

我有一个用例,我想将多态关联特性应用到一个可能不相关的类型集合。使这个用例复杂化的是,其中一些类型已经在应用程序的其他地方使用共享表继承来实现另一个目的。基本上,我有一个类型,它有一个“source”属性,我需要这个“source”属性映射到任意数量的潜在源,其中一些已经因为其他原因使用了多态性,而其中一些在形式或功能上不相似,所以继承没有意义。我希望这种关联更像一种特征,任何标记/混合了这种特征的东西都是我的对象的潜在来源

我试图用一个示例来概括这一点,在这个示例中,我试图使用SA文档中的关联代理示例来建模类定义和声明的属性。我显然错过了一些东西,我真的需要一些帮助。我希望最后一个print语句能够生成Julius实验室实例

有人看到此设置中的错误吗?非常感谢

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)

因此,这似乎是可行的,但缺点是我无法直接在最通用的“Slobber”类型上查询源代码,源属性仅在由声明的属性生成的派生类
Lab.Slobber
Person.Slobber
上可用。这种方法至少可以强制执行外键关系,这就是为什么我们最终会得到这么多不同的关联表。另一方面,我开始怀疑这是否是一个值得权衡的问题,而不是仅仅使用一个带有描述符列的关联表来处理这个问题,并让应用程序逻辑处理必要的约束

欢迎提供任何意见/其他答案

这一点通过引用以下SA示例得到了帮助:


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