I see now, thanks! It's fairly difficult to plug in the events as you suggested in my code, but I subclassed MappedCollection to discard setitem calls with a None key, and used this as the collection class. This seems to work, and if there's no other events I need to worry about during this append process, should be a complete solution.
On Monday, July 8, 2013 9:14:02 PM UTC-5, Michael Bayer wrote: > > here's how you debug that: > > @event.listens_for(A.ab, "append") > def append(target, value, initiator): > import pdb > pdb.set_trace() > > @event.listens_for(B.ab, "append") > def append(target, value, initiator): > import pdb > pdb.set_trace() > > and if you're concerned about attribute-access side effects, in your pdb > you look at an object like: > > >> ab.__dict__ > > no side effects that way (though there aren't any getter side effects in > this test). > > the sequence is: > > 1. create AB(a=a, b=b) > > 2. the AB has "a" set first, which then fires off the backref A.ab > > 3. AB is assigned to the A.ab dictionary, with key of None because AB.b is > None > > 4. AB.b is then assigned to "b" > > 5. AB.b fires off backref B.ab > > 6. the association proxy now gets involved, and appends your AB to the > A.ab collection again, this time with the correct key of "b" > > So if you just did the assignment without the association proxy (which is > a good idea when understanding this), you just get the key of None in aa.ab > and nothing else. If you were to assign .b or .a to the AB first, you get > the same problem here in one direction or the other, because both AB.a and > AB.b will both try to assign it to a dictionary that requires the other > reference be present, it's a mutual referencing issue. > > It's an awkward mapping, one way to make it work is to just not use > backrefs and make your own event, though to make it work in both directions > without going into an endless loop would require a more intricate approach > (using internal appenders that pass along the "initiator" so you can stop > an endless setter loop). Below is just the one direction: > > class A(Base): > __tablename__ = 'table_a' > id = Column(Integer, primary_key=True) > ab = relationship('AB', > collection_class=attribute_mapped_collection('b')) > abnum = correlated_proxy('ab', 'num', correlator=corr) > > class AB(Base): > __tablename__ = 'table_ab' > num = Column(Integer) > a_id = Column(Integer, ForeignKey('table_a.id'), primary_key=True) > b_id = Column(Integer, ForeignKey('table_b.id'), primary_key=True) > a = relationship("A") > b = relationship("B") > > class B(Base): > __tablename__ = 'table_b' > id = Column(Integer, primary_key=True) > ab = relationship('AB', > collection_class=attribute_mapped_collection('a') > ) > > @event.listens_for(A.ab, "append") > def append(target, value, initiator): > value.b.ab[value.a] = value > > > > > > > > > On Jul 8, 2013, at 9:07 PM, Greg Yang <sorcer...@gmail.com <javascript:>> > wrote: > > I created a class CorrelatedProxy inheriting from AssociationProxy that > allows the creator function to depend on the owner instance of the > association proxy. Essentially it gets a attribute 'correlator' of the > something like lambda x: lambda y, z: Constructor(x, y, z), and then > intercepts the __get__ of AssociationProxy to create self.creator on the > fly by applying the owner instance to the correlator. Now consider the code > below. > > from sqlalchemy.engine import create_engine > from sqlalchemy.ext.declarative.api import declarative_base > from sqlalchemy.orm import relationship > from sqlalchemy.orm.collections import attribute_mapped_collection > from sqlalchemy.orm.session import sessionmaker > from sqlalchemy.schema import Column, ForeignKey > from sqlalchemy.types import Integer > from sqlalchemy.ext.associationproxy import AssociationProxy > > class CorrelatedProxy(AssociationProxy): > def __init__(self, *args, **kw): > self.correlator = kw.pop('correlator', None) > AssociationProxy.__init__(self, *args, **kw) > def __get__(self, obj, class_): > if obj: > self.creator = self.correlator(obj) > return AssociationProxy.__get__(self, obj, class_) > > def correlated_proxy(*args, **kw): > return CorrelatedProxy(*args, **kw) > > > Base = declarative_base() > > class A(Base): > __tablename__ = 'table_a' > id = Column(Integer, primary_key=True) > ab = relationship('AB', backref = 'a', > collection_class=attribute_mapped_collection('b')) > abnum = correlated_proxy('ab', 'num', correlator=\ > lambda a: lambda b, n: AB(a=a, b=b, num=n)) > class AB(Base): > __tablename__ = 'table_ab' > num = Column(Integer) > a_id = Column(Integer, ForeignKey('table_a.id'), primary_key=True) > b_id = Column(Integer, ForeignKey('table_b.id'), primary_key=True) > > class B(Base): > __tablename__ = 'table_b' > id = Column(Integer, primary_key=True) > ab = relationship('AB', backref = 'b', > collection_class=attribute_mapped_collection('a')) > > > if __name__ == '__main__': > engine = create_engine('sqlite:///:memory:') > Session = sessionmaker(engine) > session = Session() > Base.metadata.create_all(engine) > > aa = A() > bb = B() > aa.abnum[bb] = 1 > assert aa.abnum[bb] == aa.abnum[None] == 1 > > Basically, no matter, what I do, any time I assign something to the > CorrelatedProxy, everything goes normally except that 'None' always becomes > a key, assigned to the last value I assigned to the proxy. I tried > debugging and tracing, but there's some quantum effect going on where if I > inspect some value, some other value changes. I for the life of me can't > figure out why it's doing this. I'm guessing it's some Instrumentation > effect of SA, but I don't understand the in and outs of that very much. I > currently can work around this by filtering out the None, but it'd be nice > to know why this occurs and whether it will affect any other elements of my > program with whatever is going on underneath. > > > -- > 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+...@googlegroups.com <javascript:>. > To post to this group, send email to sqlal...@googlegroups.com<javascript:> > . > Visit this group at http://groups.google.com/group/sqlalchemy. > For more options, visit https://groups.google.com/groups/opt_out. > > > > > -- 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.