On Fri, Oct 20, 2017 at 4:52 PM, Leslie Luyt <leslieluyt.j...@gmail.com> wrote: > Here is my example: > > class Role(Base): > __tablename__ = 'role' > role_id = Column(BigInteger, primary_key=True) > role_name = Column(String, nullable=False) > role_description = Column(String) > > > class Group(Base): > __tablename__ = 'group' > group_id = Column(BigInteger, primary_key=True) > group_name = Column(String, nullable=False) > group_description = Column(String) > > profiles = association_proxy('_profile_group_roles', 'profile') > > > class Profile(Base): > __tablename__ = 'profile' > profile_id = Column(BigInteger, primary_key=True) > profile_username = Column(String, nullable=False, unique=True) > > groups = association_proxy('_profile_group_roles', 'group') > roles = association_proxy('_profile_group_roles', 'role') > > > class ProfileGroupRole(Base): > __tablename__ = 'profile_group_role_assoc' > profile_id = Column(BigInteger, ForeignKey('profile.profile_id'), > primary_key=True) > group_id = Column(BigInteger, ForeignKey('group.group_id'), > primary_key=True) > role_id = Column(BigInteger, ForeignKey('role.role_id'), > primary_key=True) > > group = relationship(Group, backref=backref('_profile_group_roles', > cascade='all, delete-orphan')) > role = relationship(Role) > profile = relationship(Profile, backref=backref('_profile_group_roles', > cascade='all, delete-orphan')) > > > Example data: > s = db.connect(engine_config) > profile1 = db.Profile(profile_username='test_user') > group1 = db.Group(group_name='group1', group_description='') > role1 = db.Role(role_name='role1') > prg1 = db.ProfileGroupRole(profile=profile1, group=group1, > role=role1) > group2 = db.Group(group_name='group2', group_description='') > role2 = db.Role(role_name='role2') > prg2 = db.ProfileGroupRole(profile=profile1, group=group2, > role=role1) > prg3 = db.ProfileGroupRole(profile=profile1, group=group2, > role=role2) > s.add_all([prg1, prg2, prg3]) > s.commit() > > Querying: > db.Group.query.filter(db.Group.group_name == > 'group1').one().profiles[0].roles > > produces: > [Role{'role_id': 4, 'role_name': 'role1', 'role_description': None}, > Role{'role_id': 4, 'role_name': 'role1', 'role_description': None}, > Role{'role_id': 5, 'role_name': 'role2', 'role_description': None}] > > and now what I expected to get back was just one the one role associated > with 'group1': > [Role{'role_id': 4, 'role_name': 'role1', 'role_description': None}]
OK so this is a little awkward if you truly want "group.profiles[0].roles" to be local to the group. It means you don't really want Group.profiles to be a list of Profile objects, which is an object that can be associated with multiple groups. You want a "view" object that is a GroupProfile, a Profile that's associated with a specific group, which then has one or more Roles set up for that Group/Profile association. So implied here is that the join across the ProfileGroupRole association is between GroupProfile and Role - as far as how to link Group and Profile together we will need to produce distinct pairs by joining against a DISTINCT of ProfileGroupRole. This is a more complicated technique. I'm going off your original email that indicates you can't change the table structure at all here. So to work all these requirements together we can create a GroupProfile "view" object as: class GroupProfile(Base): __table__ = ( select([Group.group_id, Profile]). select_from(join(Group, ProfileGroupRole).join(Profile)). distinct().alias() ) group = relationship( Group, backref=backref("profiles", viewonly=True, order_by=__table__.c.profile_id), primaryjoin=foreign(__table__.c.group_id) == Group.group_id, viewonly=True) roles = relationship( Role, secondary=ProfileGroupRole.__table__, primaryjoin=and_( __table__.c.group_id == ProfileGroupRole.group_id, __table__.c.profile_id == ProfileGroupRole.profile_id), viewonly=True, collection_class=set, lazy="joined", innerjoin=True) Just so you know, that mapping is using several advanced techniques to work the three-way association table into a structure that behaves more like individual two-way associations (however note this is strictly read-only stuff, it won't persist data as designed). The map to a SELECT sets up that we can get unique Group/Profile pairs regardless of how many ProfileGroupRole rows link them together. The GroupProfile.role relationship brings ProfileGroupRole in as an association table a second time to produce individual rows per Role, and it also uses joined eager loading with innerjoin=True to emit fewer queries; we can use innerjoin because there are definitely Role rows present. Below is full demo for SQLite that returns what you expect as well as what I think you expect for some more complex cases: from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class Role(Base): __tablename__ = 'role' role_id = Column(Integer, primary_key=True) role_name = Column(String, nullable=False) role_description = Column(String) def __repr__(self): return "%s%r" % ( self.__class__.__name__, {k: v for k, v in self.__dict__.items() if not k.startswith('_')}) class Group(Base): __tablename__ = 'group' group_id = Column(Integer, primary_key=True) group_name = Column(String, nullable=False) group_description = Column(String) class Profile(Base): __tablename__ = 'profile' profile_id = Column(Integer, primary_key=True) profile_username = Column(String, nullable=False, unique=True) class ProfileGroupRole(Base): __tablename__ = 'profile_group_role_assoc' profile_id = Column( Integer, ForeignKey('profile.profile_id'), primary_key=True) group_id = Column( Integer, ForeignKey('group.group_id'), primary_key=True) role_id = Column( Integer, ForeignKey('role.role_id'), primary_key=True) group = relationship( Group, backref=backref('_profile_group_roles', cascade='all, delete-orphan')) role = relationship(Role) profile = relationship( Profile, backref=backref('_profile_group_roles', cascade='all, delete-orphan')) # using generic DISTINCT here, so create a subquery to minimize the columns # against which DISTINCT is being applied. On Postgresql, we can # use DISTINCT ON which would allow us to drop the extra subquery. distinct_profile_groups = select( [ProfileGroupRole.group_id, ProfileGroupRole.profile_id]).\ distinct().alias() class GroupProfile(Base): # the GroupProfile itself is then based on Group/Profile rows # joined to our distinct group_id/profile_id pairs __table__ = ( select([Group.group_id, Profile]). select_from(join(Group, distinct_profile_groups).join(Profile)). alias() ) group = relationship( Group, backref=backref("profiles", viewonly=True, order_by=__table__.c.profile_id), # "foreign" - means our local "group_id" column can refer # to the same "Group.group_id" across multiple rows, e.g. as though # it's a foreign key to Group.group_id primaryjoin=foreign(__table__.c.group_id) == Group.group_id, viewonly=True) roles = relationship( Role, secondary=ProfileGroupRole.__table__, primaryjoin=and_( __table__.c.group_id == ProfileGroupRole.group_id, __table__.c.profile_id == ProfileGroupRole.profile_id), viewonly=True, collection_class=set, lazy="joined", innerjoin=True) e = create_engine("sqlite://", echo=True) Base.metadata.create_all(e) s = Session(e) profile1 = Profile(profile_username='test_user') group1 = Group(group_name='group1', group_description='') role1 = Role(role_name='role1') prg1 = ProfileGroupRole(profile=profile1, group=group1, role=role1) group2 = Group(group_name='group2', group_description='') role2 = Role(role_name='role2') prg2 = ProfileGroupRole(profile=profile1, group=group2, role=role1) prg3 = ProfileGroupRole(profile=profile1, group=group2, role=role2) group3 = Group(group_name='group3', group_description='') profile2 = Profile(profile_username='user2') role3 = Role(role_name='role3') prg4 = ProfileGroupRole(profile=profile1, group=group3, role=role1) prg5 = ProfileGroupRole(profile=profile2, group=group3, role=role2) prg6 = ProfileGroupRole(profile=profile2, group=group3, role=role3) s.add_all([prg1, prg2, prg3, prg4, prg5]) s.commit() # test one. A group with one GroupProfile and one Role. group1 = s.query(Group).filter(Group.group_name == 'group1').one() assert group1.profiles[0].roles == {role1} print group1.profiles[0].roles # test two. A group with one GroupProfile and two Roles. group2 = s.query(Group).filter(Group.group_name == 'group2').one() assert group2.profiles[0].roles == {role1, role2} print group2.profiles[0].roles assert len(group2.profiles) == 1 # test three. A group with *two* GroupProfile objects (e.g. two Profiles), # each with roles. group3 = s.query(Group).filter(Group.group_name == 'group3').one() assert group3.profiles[0].roles == {role1} assert group3.profiles[1].roles == {role2, role3} print group3.profiles[0].roles print group3.profiles[1].roles assert len(group3.profiles) == 2 > > -- > 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. -- 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.