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.

Reply via email to