Search code examples
pythonsqlalchemy

SQLAlchemy relationship on many-to-many association table


I am trying to build a relationship to another many-to-many relationship, the code looks like this:

from sqlalchemy import Column, Integer, ForeignKey, Table, ForeignKeyConstraint, create_engine
from sqlalchemy.orm import relationship, backref, scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

supervision_association_table = Table('supervision', Base.metadata,
    Column('supervisor_id', Integer, ForeignKey('supervisor.id'), primary_key=True),
    Column('client_id', Integer, ForeignKey('client.id'), primary_key=True)
)

class User(Base):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True)

class Supervisor(User):
    __tablename__ = 'supervisor'
    __mapper_args__ = {'polymorphic_identity': 'supervisor'}

    id = Column(Integer, ForeignKey('user.id'), primary_key = True)

    schedules = relationship("Schedule", backref='supervisor')

class Client(User):
    __tablename__ = 'client'
    __mapper_args__ = {'polymorphic_identity': 'client'}

    id = Column(Integer, ForeignKey('user.id'), primary_key = True)

    supervisor = relationship("Supervisor", secondary=supervision_association_table,
                                backref='clients')
    schedules = relationship("Schedule", backref="client")

class Schedule(Base):
    __tablename__ = 'schedule'
    __table_args__ = (
        ForeignKeyConstraint(['client_id', 'supervisor_id'], ['supervision.client_id', 'supervision.supervisor_id']),
    )

    id = Column(Integer, primary_key=True)
    client_id = Column(Integer, nullable=False)
    supervisor_id = Column(Integer, nullable=False)

engine = create_engine('sqlite:///temp.db')
db_session = scoped_session(sessionmaker(bind=engine))
Base.metadata.create_all(bind=engine)

What I want to do is to relate a schedule to a specific Client-Supervisor-relationship, though I have not found out how to do it. Going through the SQLAlchemy documentation I found a few hints, resulting in the ForeignKeyConstraint on the Schedule-Table.

How can I specify the relationship to have this association work?


Solution

  • You need to map supervision_association_table so that you can create relationships to/from it.

    I may be glossing over something here, but it seems like since you have many-to-many here you really can't have Client.schedules - if I say Client.schedules.append(some_schedule), which row in "supervision" is it pointing to?

    The example below provides a read-only "rollup" accessor for those which joins the Schedule collections of each SupervisorAssociation. The association_proxy extension is used to conceal, when convenient, the details of the SupervisionAssociation object.

    from sqlalchemy import Column, Integer, ForeignKey, Table, ForeignKeyConstraint, create_engine
    from sqlalchemy.orm import relationship, backref, scoped_session, sessionmaker
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.ext.associationproxy import association_proxy
    from itertools import chain
    
    Base = declarative_base()
    
    class SupervisionAssociation(Base):
        __tablename__ = 'supervision'
    
        supervisor_id = Column(Integer, ForeignKey('supervisor.id'), primary_key=True)
        client_id = Column(Integer, ForeignKey('client.id'), primary_key=True)
    
        supervisor = relationship("Supervisor", backref="client_associations")
        client = relationship("Client", backref="supervisor_associations")
        schedules = relationship("Schedule")
    
    class User(Base):
        __tablename__ = 'user'
    
        id = Column(Integer, primary_key=True)
    
    class Supervisor(User):
        __tablename__ = 'supervisor'
        __mapper_args__ = {'polymorphic_identity': 'supervisor'}
    
        id = Column(Integer, ForeignKey('user.id'), primary_key = True)
    
        clients = association_proxy("client_associations", "client", 
                            creator=lambda c: SupervisionAssociation(client=c))
    
        @property
        def schedules(self):
            return list(chain(*[c.schedules for c in self.client_associations]))
    
    class Client(User):
        __tablename__ = 'client'
        __mapper_args__ = {'polymorphic_identity': 'client'}
    
        id = Column(Integer, ForeignKey('user.id'), primary_key = True)
    
        supervisors = association_proxy("supervisor_associations", "supervisor", 
                            creator=lambda s: SupervisionAssociation(supervisor=s))
        @property
        def schedules(self):
            return list(chain(*[s.schedules for s in self.supervisor_associations]))
    
    class Schedule(Base):
        __tablename__ = 'schedule'
        __table_args__ = (
            ForeignKeyConstraint(['client_id', 'supervisor_id'], 
            ['supervision.client_id', 'supervision.supervisor_id']),
        )
    
        id = Column(Integer, primary_key=True)
        client_id = Column(Integer, nullable=False)
        supervisor_id = Column(Integer, nullable=False)
        client = association_proxy("supervisor_association", "client")
    
    engine = create_engine('sqlite:///temp.db', echo=True)
    db_session = scoped_session(sessionmaker(bind=engine))
    Base.metadata.create_all(bind=engine)
    
    c1, c2 = Client(), Client()
    sp1, sp2 = Supervisor(), Supervisor()
    sch1, sch2, sch3 = Schedule(), Schedule(), Schedule()
    
    sp1.clients = [c1]
    c2.supervisors = [sp2]
    c2.supervisor_associations[0].schedules = [sch1, sch2]
    c1.supervisor_associations[0].schedules = [sch3]
    
    db_session.add_all([c1, c2, sp1, sp2, ])
    db_session.commit()
    
    
    print c1.schedules
    print sp2.schedules