- changed milestone to 0.9.0
cascading polymorphic ons
Issue #2555
new
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 (6)
-
reporter -
reporter - changed milestone to 0.9.xx
-
reporter - changed milestone to 1.0.xx
-
reporter Issue
#3214was marked as a duplicate of this issue. -
reporter - changed milestone to 1.2
- edited description
I'm not very interested in this feature but still want to consider its addition.
-
reporter - changed milestone to 1.x.xx
- Log in to comment