The Versioned mixin described in http://docs.sqlalchemy.org/en/rel_0_8/orm/examples.html#versioned-objects (which I renamed VersionedMixin, but is otherwise the same) has what I would consider an unintuitive and undesirable interaction with backref: if C references A with a backref, adding a new C object referencing a particular A object will cause the version number of the target A object to increment, even though there are no changes to the A table. If the relation has no backref (as in the relationship from C to B below), then the target object version number is not incremented, as I would expect. It seems that the code is effectively using session.is_modified(a) to determine whether to increment the version number, whereas I would have thought session.is_modified(a, include_collections=False) would be more appropriate. Is there some use case I'm not considering that favors the current behavior?
Thanks, Seth from sqlalchemy import Column, Integer, String, ForeignKey, create_engine from sqlalchemy.ext.declarative.api import declarative_base from sqlalchemy.orm import sessionmaker, scoped_session, relationship, backref from history_meta import VersionedMixin, versioned_session Base = declarative_base(object) metadata = Base.metadata class A(VersionedMixin, Base): __tablename__ = 'a' __table_args__ = {} id = Column(Integer, primary_key=True) name = Column(String(3)) def __repr__(self): return "A(id=%d,name='%s',version=%d,cs=%s)" % (self.id, self.name, self.version, [c.name for c in self.cs]) class B(VersionedMixin, Base): __tablename__ = 'b' __table_args__ = {} id = Column(Integer, primary_key=True) name = Column(String(3)) def __repr__(self): return "B(id=%d,name='%s',version=%d)" % (self.id, self.name, self.version) class C(VersionedMixin, Base): __tablename__ = 'c' __table_args__ = {} id = Column(Integer, primary_key=True) name = Column(String(3)) a_id = Column(Integer, ForeignKey('a.id')) a_re = relationship(A, backref='cs') b_id = Column(Integer, ForeignKey('b.id')) b_re = relationship(B) if __name__ == '__main__': engine = create_engine('sqlite:///:memory:', echo=False) metadata.create_all(bind=engine) Session = scoped_session(sessionmaker(bind=engine)) versioned_session(Session) session = Session() # populate tables with a single entry in each table a = A(name='a') b = B(name='b') c1 = C(name='c1', a_re=a, b_re=b) session.add_all([a, b, c1]) session.commit() print '\nAfter initial commit' print 'a=%s; is_modified(a)=%s; is_modified(a, include_collections=False)=%s' % (a, session.is_modified(a), session.is_modified(a, include_collections=False)) print 'b=%s; is_modified(b)=%s; is_modified(b, include_collections=False)=%s' % (b, session.is_modified(b), session.is_modified(b, include_collections=False)) # add another entry in b that points to a c2 = C(name='c2', a_re=a, b_re=b) session.add(c2) print "\nAfter adding C(name='c2', a_re=a, b_re=b), but before committing:" print 'a=%s; is_modified(a)=%s; is_modified(a, include_collections=False)=%s' % (a, session.is_modified(a), session.is_modified(a, include_collections=False)) print 'b=%s; is_modified(b)=%s; is_modified(b, include_collections=False)=%s' % (b, session.is_modified(b), session.is_modified(b, include_collections=False)) session.commit() print '\nAfter final commit:' print 'a=%s; is_modified(a)=%s; is_modified(a, include_collections=False)=%s' % (a, session.is_modified(a), session.is_modified(a, include_collections=False)) print 'b=%s; is_modified(b)=%s; is_modified(b, include_collections=False)=%s' % (b, session.is_modified(b), session.is_modified(b, include_collections=False)) -- 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.