Hey Mike,

thank you for the proof of concept! I tried it out and it seems to work. 
However, as you said, it's a little akward...

I'm not a specialist with DB Design, but I think this pattern is used at 
other places to. What would your recommondation be for this kind of 
relationship between tables? As you said, database-wise this is completly 
reasonable. But in the end I wan't a design which is readable and 
maintainable in the long term.

Btw: Of course, the `is_guide` column should be a boolean, I just forgot to 
change it from the example code. And I think a AssociationSet would be a 
better fit, because a member can either guide a event or participate a 
event, but not both on the same time. 

Am Samstag, 18. März 2017 03:50:22 UTC+1 schrieb Mike Bayer:
>
>
>
> On 03/17/2017 07:43 PM, Jürg Rast wrote: 
> > I followed the examples about association proxies 
> > (
> http://docs.sqlalchemy.org/en/latest/orm/extensions/associationproxy.html) 
>
> > but now I'm stuck. 
> > 
> > I have the following example (using flask + sqlalchemy) of a simple 
> > event management: 
> > 
> > | 
> > from sqlalchemy import Column, String, Integer, ForeignKey, Boolean 
> > from sqlalchemy.orm import relationship, backref 
> > from sqlalchemy.ext.associationproxy import association_proxy 
> > from . import db, BaseModel 
> > 
> > 
> > class Event(BaseModel): 
> >     __tablename__ = 'event' 
> >     title = Column(String(64)) 
> > 
> >     # association proxy of "event_participations" collection 
> >     # to "keyword" attribute 
> >     guides = association_proxy('event_participations', 'member', 
> >                 creator=lambda m: EventParticipation(member=m, 
> > is_guide=True)) 
> > 
> >     participants = association_proxy('event_participations', 'member', 
> >                         creator=lambda m: EventParticipation(member=m, 
> > is_guide=False)) 
> > 
> >     def __init__(self, name): 
> >         self.title = name 
> > 
> >     def __repr__(self): 
> >         return 'Event(%s)' % self.title 
> > 
> > 
> > class EventParticipation(db.Model): 
> >     __tablename__ = 'event_participation' 
> >     event_id = Column(Integer, ForeignKey('event.id'), 
> primary_key=True) 
> >     member_id = Column(Integer, ForeignKey('member.id'), 
> primary_key=True) 
> >     is_guide = Column(String(50)) 
> > 
> >     # bidirectional attribute/collection of 
> "event"/"event_participations" 
> >     event = relationship(Event, 
> >                 backref=backref("event_participations", 
> >                                 cascade="all, delete-orphan") 
> >             ) 
> > 
> >     # reference to the "Keyword" object 
> >     member = relationship("Member") 
> > 
> >     def __init__(self, member=None, event=None, is_guide=None): 
> >         self.event = event 
> >         self.member = member 
> >         self.is_guide = is_guide 
> > 
> >     def __repr__(self): 
> >         return 'EventParticipation(%s, %s, %s)' % (self.event, 
> > self.member, self.is_guide) 
> > 
> > 
> > class Member(BaseModel): 
> >     __tablename__ = 'member' 
> >     name = Column(String(64)) 
> > 
> >     def __repr__(self): 
> >         return 'Member(%s)' % repr(self.name) 
> > 
> > | 
> > 
> > So, basically a member can be a 'guide' for a event or a 'participant' 
> > and each event can have multiple guides and participants. Therefore I 
> > created a secondary table which holds the event and member id and also a 
> > field which indicates if the member has registered as member or as 
> guide. 
>
> OK so database-wise this is fine, you have Event -> EParticipation(Guide 
> or Member) -> Member and that's fine (though seems like "is_guide" 
> should be a Boolean, probably). 
>
> Where it's going wrong is how the association proxies are being used. 
> You have the is_guide=True|False thing inside the "creator", which is 
> fine, but that only affects what happens when you append a record, it 
> doesn't affect what the association proxy does when you read it.  so 
> both of these association proxies are giving you the same list of objects. 
>
> The use case of filtering on read is a little awkward and we have to use 
> custom collection classes.   Making it a little worse is that the 
> association proxy's system for using custom collection classes is even 
> more awkward.    so below is a full proof of concept. 
>
> from sqlalchemy import * 
> from sqlalchemy.orm import * 
> from sqlalchemy.ext.declarative import declarative_base 
> from sqlalchemy.ext import associationproxy 
> from sqlalchemy.ext.associationproxy import association_proxy 
> import functools 
>
> Base = declarative_base() 
>
>
> class FilteredList(associationproxy._AssociationList): 
>      def __init__( 
>              self, is_guide, lazy_collection, creator, value_attr, 
> parent): 
>          # awkward. 
>          getter, setter = parent._default_getset( 
>              associationproxy._AssociationList) 
>          super(FilteredList, self).__init__( 
>              lazy_collection, creator, getter, setter, parent 
>          ) 
>          self.is_guide = is_guide 
>
>      # define all the "read" methods that need to do filtering here.  The 
>      # proxied collection is in self.col 
>      def __iter__(self): 
>          return (m.member for m in self.col if m.is_guide is 
> self.is_guide) 
>
>      def __contains__(self, other): 
>          return other in [ 
>              m.member for m in self.col if m.is_guide is self.is_guide 
>          ] 
>
>
> class Event(Base): 
>      __tablename__ = 'event' 
>      id = Column(Integer, primary_key=True) 
>      title = Column(String(64)) 
>
>      # association proxy of "event_participations" collection 
>      # to "keyword" attribute 
>      guides = association_proxy( 
>          'event_participations', 'member', 
>          proxy_factory=functools.partial(FilteredList, True), 
>          creator=lambda m: EventParticipation(member=m, is_guide=True)) 
>
>      participants = association_proxy( 
>          'event_participations', 'member', 
>          proxy_factory=functools.partial(FilteredList, False), 
>          creator=lambda m: EventParticipation(member=m, is_guide=False)) 
>
>      def __init__(self, name): 
>          self.title = name 
>
>      def __repr__(self): 
>          return 'Event(%s)' % self.title 
>
>
> class EventParticipation(Base): 
>      __tablename__ = 'event_participation' 
>      event_id = Column(Integer, ForeignKey('event.id'), primary_key=True) 
>      member_id = Column(Integer, ForeignKey('member.id'), 
> primary_key=True) 
>      is_guide = Column(Boolean) 
>
>      # bidirectional attribute/collection of 
> "event"/"event_participations" 
>      event = relationship( 
>          Event, 
>          backref=backref("event_participations", 
>                          cascade="all, delete-orphan") 
>      ) 
>
>      # reference to the "Keyword" object 
>      member = relationship("Member") 
>
>      def __init__(self, member=None, event=None, is_guide=None): 
>          self.event = event 
>          self.member = member 
>          self.is_guide = is_guide 
>
>      def __repr__(self): 
>          return 'EventParticipation(%s, %s, %s)' % ( 
>              self.event, self.member, self.is_guide) 
>
>
> class Member(Base): 
>      __tablename__ = 'member' 
>      id = Column(Integer, primary_key=True) 
>      name = Column(String(64)) 
>
>      def __repr__(self): 
>          return 'Member(%s)' % repr(self.name) 
>
> e = create_engine("sqlite://", echo=True) 
> Base.metadata.create_all(e) 
>
> s = Session(e) 
>
> event = Event('test') 
> for g in (Member(name='g1'), Member(name='g2')): 
>      event.guides.append(g) 
>
> s.add(event) 
> s.commit() 
>
> print "-----------------------" 
> assert g in event.guides 
>
> for m in (Member(name='m1'), Member(name='m2')): 
>      event.participants.append(m) 
>
> s.commit() 
>
> for p in s.query(EventParticipation).all(): 
>      print(p) 
>
> assert m in event.participants 
> assert g not in event.participants 
>
>
>
>
> > 
> > Adding guides and participants works as expected, but querying for the 
> > guides or participants only fails. Eg. in the following example the last 
> > test fails 
> > | 
> >     event = Event('test') 
> >     for g in (Member(name='g1'), Member(name='g2')): 
> >         event.guides.append(g) 
> > 
> >     db.session.add(event) 
> >     db.session.commit() 
> > 
> >     assert g in event.guides 
> > 
> >     for m in (Member(name='m1'), Member(name='m2')): 
> >         event.participants.append(m) 
> > 
> >     db.session.commit() 
> > 
> >     for p in EventParticipation.query.all(): 
> >         print(p) 
> > 
> >     assert m in event.participants 
> >     assert g not in event.participants 
> > | 
> > 
> > How can I specify the filter for the association proxy? Is this possible 
> > or is this database design just not good and I should change the data 
> > layout (If yes, what would be a good design?) 
> > 
> > Kind regards 
> > 
> > -- 
> > 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+...@googlegroups.com <javascript:> 
> > <mailto:sqlalchemy+unsubscr...@googlegroups.com <javascript:>>. 
> > To post to this group, send email to sqlal...@googlegroups.com 
> <javascript:> 
> > <mailto:sqlal...@googlegroups.com <javascript:>>. 
> > 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