On Aug 15, 2013, at 7:32 AM, Paul Balomiri <[email protected]> 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 <[email protected]>:
>>
>> On Aug 13, 2013, at 11:44 AM, Paul Balomiri <[email protected]> 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)
>>
>>
>
>
>
> --
> [email protected]
>
> --
> 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 [email protected].
> To post to this group, send email to [email protected].
> 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
