On 03/21/2017 12:39 AM, Isaac Martin wrote:
down votefavorite
<http://stackoverflow.com/questions/42918152/sqlalchemy-polymorphism-without-discriminators#>
I am trying to use an external library which defines a class model in my
own program. I want the classes I define to be the same in all respects
to their parents from the library, except that I want to append some
helper methods to my local extensions. For example:
*External Library:*
|Base=declarative_base()classBaseUser(Base):__tablename__ ='user'email
=Column(String(100),nullable=False,unique=True)password
=Column(String(128),nullable=False)address_uid
=Column(Integer,ForeignKey('address.uid'))address
=relationship('BaseAddress',back_populates="users")def__init__(self,**kwargs):super().__init__(**kwargs)self.address
=BaseAddress()classBaseAddress(Base):__tablename__ ='address'street
=Column(String(100))unit =Column(String(32))city
=Column(String(64))state =Column(String(32))postal
=Column(String(32))country =Column(String(32))users
=relationship('user',back_populates="address")|
*Local model:*
|classUser(BaseUser):defin_country(county):returnself.address.country
==country classAddress(BaseAddress):pass|
The goal here is to create subclasses which sqlalchemy need not
distinguish from their parents. If I insert
an |Address| into |User.address|, for example, sqlalchemy should not
complain about a type mismatch (|Address| instead of the
expected |BaseAddress|).
The only way of doing this that I can discern would involve
using |polymorphic_on| in the parent classes. I don't want to do this,
because it doesn't accurately model what is happening. It would require
a discriminator, and it might behave strangely in the event I used a
migration script locally. Is there a way with sqlalchemy to achieve
polymorphism (I think it's called "ad-hoc polymorphism") /without/ using
discriminators, or some other way of achieving my goal?
So when I've seen this before, I've asked folks about the nature of this
"external library" - e.g. can this "external library" be changed?
Because as it is designed, it is not correctly built for subclassing.
As you've observed, the BaseUser.address relationship is going to load
BaseAddress objects when queried.
To figure this out we need to go to basic object oriented principles
(even though OO is "dead" and all that).
Suppose this is our external library:
class BaseUser:
def get_addresses(self):
data = load_address_data(self)
return [BaseAddress(rec) for rec in data]
class BaseAddress:
pass
We then want to subclass it as follows:
class User(BaseUser):
# ...
class Address(BaseAddress):
# ...
We can see that even in plain OO, this design isn't easy to subclass.
User would need to override get_addresses() entirely. In fact in our
ORM example, this is an option; you can specify a new relationship for
User.addresses that specifies Address, instead of BaseAddress, as the
target.
In plain OO, to make this easier we'd do something like this:
class BaseUser:
def get_addresses(self):
data = load_address_data(self)
return [self.address_factory(rec) for rec in data]
def address_factory(self, rec):
return BaseAddress(rec)
class User(BaseUser):
def address_factory(self, rec):
return Address(rec)
that is, we use a pattern called "template method" to factor out part of
the operation to subclasses.
The question is how to rig this into the ORM. Declarative includes
just enough hooks to make the above pattern possible however since we
aren't working with a traditional method for "relationship" or
"back_populates", we need to use declarative indicators; specifically
that we build classmethods in conjunction with @declared_attr, rather
than using relationship() at the class level.
Another thing that makes this way more straightforward is if the
BaseUser/BaseAddress themselves are not mapped to tables; since these
are called "BaseXYZ" this seems to imply that they are only meant for
subclassing. For that we use the __abstract__ indicator on the bases.
This makes this kind of subclass case a lot easier since the whole
system knows that anything to do with a mapping is on the subclasses,
not the BaseUser/BaseAddress. If BaseUser/BaseAddress need to
actually be usable as mapped classes independently (which is uncommon),
declarative doesn't yet quite have a seamless way to make that happen in
this kind of setup; a simple fix is to just make more subclasses of
BaseUser/BaseAddress in the "external" library to serve as the "mapped"
version of them.
Anyway, with @declared_attr and __abstract__ here's a full proof of concept:
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.declarative import declared_attr
Base = declarative_base()
class BaseUser(Base):
__abstract__ = True
__tablename__ = 'user'
uid = Column(Integer, primary_key=True)
email = Column(String(100), nullable=False, unique=True)
password = Column(String(128))
@declared_attr
def address_uid(cls):
return Column(Integer, ForeignKey('address.uid'))
@declared_attr
def address(cls):
return relationship(cls.address_factory, back_populates="users")
@classmethod
def address_factory(cls):
return BaseAddress
raise NotImplementedError()
def __init__(self, **kwargs):
super(BaseUser, self).__init__(**kwargs)
self.address = self.address_factory()()
class BaseAddress(Base):
__abstract__ = True
__tablename__ = 'address'
uid = Column(Integer, primary_key=True)
street = Column(String(100))
unit = Column(String(32))
city = Column(String(64))
state = Column(String(32))
postal = Column(String(32))
country = Column(String(32))
@classmethod
def user_factory(cls):
return BaseUser
raise NotImplementedError()
@declared_attr
def users(cls):
return relationship(cls.user_factory, back_populates="address")
class User(BaseUser):
def in_country(county):
return self.address.country == country
@classmethod
def address_factory(cls):
return Address
class Address(BaseAddress):
@classmethod
def user_factory(cls):
return User
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
s.add(User(email='@', address=Address(street='foo bar')))
s.commit()
s.close()
u1 = s.query(User).first()
print(u1.address)
*Oblig:*
Special thanks to everyone who participants in this group. I have
followed it, though haven't contributed to it. I have created a SO post
for this as well here in case anyone wants to rack up a few
points:
http://stackoverflow.com/questions/42918152/sqlalchemy-polymorphism-without-discriminators
--
SQLAlchemy -
The Python SQL Toolkit and Object Relational Mapper
http://www.sqlalchemy.org/
To post example code, please provide an MCVE: Minimal, Complete, and
Verifiable Example. See http://stackoverflow.com/help/mcve for a full
description.
---
You received this message because you are subscribed to the Google
Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send
an email to sqlalchemy+unsubscr...@googlegroups.com
<mailto:sqlalchemy+unsubscr...@googlegroups.com>.
To post to this group, send email to sqlalchemy@googlegroups.com
<mailto:sqlalchemy@googlegroups.com>.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.
--
SQLAlchemy -
The Python SQL Toolkit and Object Relational Mapper
http://www.sqlalchemy.org/
To post example code, please provide an MCVE: Minimal, Complete, and Verifiable
Example. See http://stackoverflow.com/help/mcve for a full description.
---
You received this message because you are subscribed to the Google Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.