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+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