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.
signature.asc
Description: Message signed with OpenPGP using GPGMail