On Aug 15, 2013, at 7:32 AM, Paul Balomiri <paulbalom...@gmail.com> wrote:

> Hi,
> 
> Thank you for the elaborated Answer !
> 
> I am trying to implement a general solution for the key->list problem
> using events.
> 
> basically i want to instrument for GroupByKeyCollection any changes
> relevant to the keyfunc.
> 
> say we have
> p= Person()
> p._address_by_role['r1']= [PersonToAddress(address=Address(name='a1'),
> role='r1')  ]
> 
> My problem is that i cannot access the parent object (PersonToAddress)
> from ScalarAttributeImpl supplied by the events framework as
> initiation parameter of the set callback. What i want is to remove an
> object from a key-associated list when it's keying function result
> mutates. For this i have to fetch the PersonToAddress from
> PersonToAddress.role.set event. Can you hint me a way to fetch a
> mapped object from it's attribute set event ?

that's what the "target" argument is, it's the object....

@event.listens_for(PersonToAddress.role, "set")
def set(target, value, oldvalue, initiator):
    person = target.person
    if person:
        # do the manipulation directly...
        collection = person._addresses_by_role[oldvalue]
        collection.remove(target)
        person._addresses_by_role[value].append(target)

        # or let the collection do it:
        # target.person = None
        # set the value early
        # target.role = value
        # target.person = person


what's "person"?  a backref, easy enough...

    _addresses_by_role = relationship("PersonToAddress",
                            collection_class=
                            lambda: GroupByKeyCollection(
                                        keyfunc=lambda item: item.role
                                    ),
                            backref="person"
                        )


test....

some_role = p1._addresses_by_role['r1'][1]
some_role.role = 'r5'

assert p1._addresses_by_role['r5'] == [some_role]



> 

> 2013/8/13 Michael Bayer <mike...@zzzcomputing.com>:
>> 
>> On Aug 13, 2013, at 11:44 AM, Paul Balomiri <paulbalom...@gmail.com> wrote:
>> 
>>> I would like to get a list as value for the dict, such that i can
>>> assign more than one entity to any one key. The output should look
>>> like this:
>>> {u'home': [<Address object at 0x29568d0>,<Address object at ...>] ,
>>> u'work': [<Address object at 0x2a3eb90>]}
>>> 
>>> Now in the database whenever i set a new value for a key(=role), the
>>> entry in PersonToAddress' table is replaced (not added). This is
>>> consistent with having a 1-key to 1-value mapping. Can I however
>>> change the behaviour in such a way that more than one Addresses are
>>> allowed for one Person using the same key(=role in this example)?
>>> 
>> 
>> OK, an attribute_mapped_collection is just an adapter for what is basically 
>> a sequence.  Instead of a sequence of objects, it's a sequence of (key, 
>> object).   So by itself, attribute_mapped_collection can only store mapped 
>> objects, not collections as values.
>> 
>> When using the association proxy, there is a way to get a dictionary of 
>> values, but the association proxy only knows how to close two "hops" into 
>> one.  So to achieve that directly, you'd need one relationship that is a 
>> key/value mapping to a middle object, then that middle object has a 
>> collection of things.    So here PersonToAddress would be more like 
>> PersonAddressCollection, and then each Address object would have a 
>> person_address_collection_id.   That's obviously not the traditional 
>> association object pattern - instead of a collection of associations to 
>> scalars, it's a collection of collections, since that's really the structure 
>> you're looking for here.
>> 
>> To approximate the "collection of collections" on top of a traditional 
>> association pattern is tricky.  The simplest way is probably to make a 
>> read-only @property that just fabricates a dictionary of collections on the 
>> fly, reading from the pure collection of PersonToAddress objects.  If you 
>> want just a quick read-only system, I'd go with that.
>> 
>> Otherwise, we need to crack open the collection mechanics completely, and 
>> since you want association proxying, we need to crack that open as well.  
>> I've worked up a proof of concept for this idea which is below, and it was 
>> not at all trivial to come up with.  In particular I stopped at getting 
>> Person.addresses_by_role['role'].append(Address()) to work, since that means 
>> we'd need two distinctly instrumented collections, it's doable but is more 
>> complex.    Below I adapted collections.defaultdict() to provide us with a 
>> "collection of collections" over a single collection and also the 
>> association proxy's base collection adapter in order to reduce the hops:
>> 
>> from sqlalchemy import *
>> from sqlalchemy.orm import *
>> from sqlalchemy.ext.declarative import declarative_base
>> import collections
>> from sqlalchemy.orm.collections import collection, collection_adapter
>> from sqlalchemy.ext.associationproxy import association_proxy, 
>> _AssociationCollection
>> Base = declarative_base()
>> 
>> class GroupByKeyCollection(collections.defaultdict):
>>    def __init__(self, keyfunc):
>>        super(GroupByKeyCollection, self).__init__(list)
>>        self.keyfunc = keyfunc
>> 
>>    @collection.appender
>>    def add(self, value, _sa_initiator=None):
>>        key = self.keyfunc(value)
>>        self[key].append(value)
>> 
>>    @collection.remover
>>    def remove(self, value, _sa_initiator=None):
>>        key = self.keyfunc(value)
>>        self[key].remove(value)
>> 
>>    @collection.internally_instrumented
>>    def __setitem__(self, key, value):
>>        adapter = collection_adapter(self)
>>        # the collection API usually provides these events transparently, but 
>> due to
>>        # the unusual structure, we pretty much have to fire them ourselves
>>        # for each item.
>>        for item in value:
>>            item = adapter.fire_append_event(item, None)
>>        collections.defaultdict.__setitem__(self, key, value)
>> 
>>    @collection.internally_instrumented
>>    def __delitem__(self, key, value):
>>        adapter = collection_adapter(self)
>>        for item in value:
>>            item = adapter.fire_remove_event(item, None)
>>        collections.defaultdict.__delitem__(self, key, value)
>> 
>>    @collection.iterator
>>    def iterate(self):
>>        for collection in self.values():
>>            for item in collection:
>>                yield item
>> 
>>    @collection.converter
>>    def _convert(self, target):
>>        for collection in target.values():
>>            for item in collection:
>>                yield item
>> 
>>    def update(self, k):
>>        raise NotImplementedError()
>> 
>> 
>> class AssociationGBK(_AssociationCollection):
>>    def __init__(self, lazy_collection, creator, value_attr, parent):
>>        getter, setter = parent._default_getset(parent.collection_class)
>>        super(AssociationGBK, self).__init__(
>>                lazy_collection, creator, getter, setter, parent)
>> 
>>    def _create(self, key, value):
>>        return self.creator(key, value)
>> 
>>    def _get(self, object):
>>        return self.getter(object)
>> 
>>    def _set(self, object, key, value):
>>        return self.setter(object, key, value)
>> 
>>    def __getitem__(self, key):
>>        return [self._get(item) for item in self.col[key]]
>> 
>>    def __setitem__(self, key, value):
>>        self.col[key] = [self._create(key, item) for item in value]
>> 
>>    def add(self, key, item):
>>        self.col.add(self._create(key, item))
>> 
>>    def items(self):
>>        return ((key, [self._get(item) for item in self.col[key]])
>>                for key in self.col)
>> 
>>    def update(self, kw):
>>        for key, value in kw.items():
>>            self[key] = value
>> 
>>    def clear(self):
>>        self.col.clear()
>> 
>>    def copy(self):
>>        return dict(self.items())
>> 
>>    def __repr__(self):
>>        return repr(dict(self.items()))
>> 
>> 
>> class Person(Base):
>>    __tablename__ = 'person'
>> 
>>    id = Column(Integer, primary_key=True)
>> 
>>    _addresses_by_role = relationship("PersonToAddress",
>>                            collection_class=
>>                            lambda: GroupByKeyCollection(
>>                                        keyfunc=lambda item: item.role
>>                                    )
>>                        )
>>    addresses_by_role = association_proxy(
>>                            "_addresses_by_role",
>>                            "address",
>>                            proxy_factory=AssociationGBK,
>>                            creator=lambda k, v: PersonToAddress(role=k, 
>> address=v))
>> 
>> class PersonToAddress(Base):
>>    __tablename__ = 'person_to_address'
>> 
>>    id = Column(Integer, primary_key=True)
>>    person_id = Column(Integer, ForeignKey('person.id'))
>>    address_id = Column(Integer, ForeignKey('address.id'))
>>    role = Column(String)
>>    address = relationship("Address")
>> 
>> class Address(Base):
>>    __tablename__ = 'address'
>> 
>>    id = Column(Integer, primary_key=True)
>>    name = Column(String)
>> 
>> e = create_engine("sqlite://", echo=True)
>> Base.metadata.create_all(e)
>> 
>> sess = Session(e)
>> 
>> p1 = Person(addresses_by_role={
>>    "r1": [
>>        Address(name='a1'),
>>        Address(name='a2')
>>    ],
>>    "r2": [
>>        Address(name='a3')
>>    ]
>> })
>> 
>> sess.add(p1)
>> 
>> sess.commit()
>> 
>> print(p1.addresses_by_role)
>> 
>> # to get p1.addresses_by_role['r3'].append(Address()) to work,
>> # we'd need to also instrument the lists inside of the mapping....
>> p1.addresses_by_role.add('r3', Address(name='r3'))
>> 
>> print(p1.addresses_by_role)
>> 
>> sess.commit()
>> 
>> print(p1.addresses_by_role)
>> 
>> 
> 
> 
> 
> -- 
> paulbalom...@gmail.com
> 
> -- 
> 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 http://groups.google.com/group/sqlalchemy.
> For more options, visit https://groups.google.com/groups/opt_out.

Attachment: signature.asc
Description: Message signed with OpenPGP using GPGMail

Reply via email to