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)


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

Reply via email to