Issues

Issue #2555 new

cascading polymorphic ons

Mike Bayer
repo owner created an issue

have the polymorphic_on check during loading continue to check on the type returned so that polymorphic loads can cascade. Other complexities here regard getting the multiple discriminators assigned during init, as well as considering if polymorphic_map can be local to each sub-hierarchy.

"workaround" example below.

This is only tentatively on the 0.8 milestone. can move to 0.9.

"""
mixed single and joined table inheritance.
"""

from sqlalchemy import *
from sqlalchemy import types
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base,  declared_attr
from sqlalchemy import event

Base = declarative_base()

class Product(Base):
    __tablename__ = 'products'
    id = Column(types.Integer, primary_key=True)
    discriminator = Column('product_type', types.String(50), nullable=False)

    _discriminator = "discriminator"

    def price_history(self):
        return [PhysicalProduct(Product):
    p_discr = Column(types.String(50))

    _discriminator = "p_discr"

    @declared_attr
    def __mapper_args__(cls):
        return {'polymorphic_identity': 'physical_product'}

    def inventory(self):
        return "computed inventory"

class NonPhysicalProduct(Product):
    np_discr = Column(types.String(50))

    _discriminator = "np_discr"

    @declared_attr
    def __mapper_args__(cls):
        return {'polymorphic_identity': 'nonphysical_product'}

    def somefunc(self):
        return "someval"

# set polymorphic on as a coalesce of those three
# columns.  It's after the fact beacuse p_discr and np_discr
# are defined after Product, but if you move them up then
# this can be inline inside of Product.__mapper_args__.
# this would improve loads too as it appears the p_discr/np_discr columns
# aren't loaded directly when you query for Product
for mp in Product.__mapper__.self_and_descendants:
    mp._set_polymorphic_on(
            func.coalesce(
                    Product.__table__.c.p_discr,
                    Product.__table__.c.np_discr,
                    Product.__table__.c.product_type
            ))

# build our own system of assigning polymorphic identities
# to instances; use the 'init' event.
# Add a "print" for the "identity" dict to see what it's doing.
@event.listens_for(Product, "init", propagate=True)
def init(target, args, kwargs):
    identity = {}
    for cls, supercls in zip(type(target).__mro__, type(target).__mro__[1:](]

class)):
        if not hasattr(supercls, '_discriminator'):
            break
        discriminator_attr = supercls._discriminator
        poly_identity = cls.__mapper__.polymorphic_identity
        identity.setdefault(discriminator_attr, poly_identity)
    for key in identity:
        setattr(target, key, identity[key](key))


class Newspaper(PhysicalProduct):
    __tablename__ = 'newspapers'
    __mapper_args__ = {'polymorphic_identity': 'newspaper'}

    id = Column(types.Integer,
                ForeignKey('products.id'),
                primary_key=True
                )
    title = Column(types.String(50))

    def __init__(self, title):
        self.title = title


class NewspaperDelivery(NonPhysicalProduct):
    __tablename__ = 'deliveries'
    __mapper_args__ = {'polymorphic_identity': 'delivery'}

    id = Column(types.Integer,
                ForeignKey('products.id'),
                primary_key=True
                )
    destination = Column(types.String(50))

    def __init__(self, destination):
        self.destination = destination


# note here how the polymorphic map works out:
print Product.__mapper__.polymorphic_map
# {'newspaper': <Mapper at 0x1014d8890; Newspaper>,
# 'delivery': <Mapper at 0x1014dec90; NewspaperDelivery>,
# 'nonphysical_product': <Mapper at 0x1014d2350; NonPhysicalProduct>,
# 'physical_product': <Mapper at 0x1014d00d0; PhysicalProduct>}


e = create_engine('sqlite:///:memory:', echo='debug')
Base.metadata.drop_all(e)
Base.metadata.create_all(e)

session = Session(e, autoflush=True, autocommit=False)

session.add_all([   Newspaper(title="Financial Times"),
    NewspaperDelivery(destination="__somewhere__"),
    PhysicalProduct(),
    NonPhysicalProduct()
](
))

session.commit()

# the important part - that a row only known as Product can
# interpret as a specific subclass
assert [   type(c) for c in session.query(Product).order_by(Product.id)
](
) == [NewspaperDelivery, PhysicalProduct, NonPhysicalProduct](Newspaper,)

# test sub-table load.  The load for "title" apparently emits a JOIN still because
# in order to refresh the subclass of "Product" it also wants to get
# at p_discr.
np = session.query(Product).filter_by(id=1).first()
assert np.title == "Financial Times"

session.close()

# in this version, it emits two separate, single table SELECT statements,
# since the first query loads the full set of columns for PhysicalProduct.
np = session.query(PhysicalProduct).filter_by(id=1).first()
assert np.title == "Financial Times"

Comments (4)

  1. Log in to comment