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 ? The following describes how i see path to the solution. Do you think i am on the right track? The keying function shall be reapplied whenever keying Attributes are mutated on PersonToAddress. Upon detecting a changed value i want to reorganize the _address_by_role structure. The second step would be to implement callbacks on the instrumented lists which form the values of the GroupByKeyCollection. The behavior i target is such that: p._address_by_role.append(PersonToAddress(address=Address(name='a1', role='r1')) #OK p._address_by_role['r2'].append(PersonToAddress(address=Address(name='a1'), role='r1')) # OK, but PersonToAddress.role is changed to 'r2' p._address_by_role['r2'].append(PersonToAddress(address=Address(name='a1'))) #OK, PersonToAddress.role is set to 'r2' del p._address_by_role['r2'][0] #O.K, the first element is removed, and it's role value is set to the default value p._address_by_role['r2'][1]= p._address_by_role['r1'][0] # OK, but may steps should happen here: # -p._address_by_role['r1'][0] is put into p._address_by_role['r2'] # -this changes the attr. value p._address_by_role['r1'][0].role to "r2" # this triggers the removal from p._address_by_role['r1'] Thank you Paul 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.