:ticket:`2789`
+.. _migration_2833:
+
+``viewonly=True`` on ``relationship()`` prevents history from taking effect
+---------------------------------------------------------------------------
+
+The ``viewonly`` flag on :func:`.relationship` is applied to prevent changes
+to the target attribute from having any effect within the flush process.
+This is achieved by eliminating the attribute from being considered during
+the flush. However, up until now, changes to the attribute would still
+register the parent object as "dirty" and trigger a potential flush. The change
+is that the ``viewonly`` flag now prevents history from being set for the
+target attribute as well. Attribute events like backrefs and user-defined events
+still continue to function normally.
+
+The change is illustrated as follows::
+
+ from sqlalchemy import Column, Integer, ForeignKey, create_engine
+ from sqlalchemy.orm import backref, relationship, Session
+ from sqlalchemy.ext.declarative import declarative_base
+ from sqlalchemy import inspect
+
+ Base = declarative_base()
+
+ class A(Base):
+ __tablename__ = 'a'
+ id = Column(Integer, primary_key=True)
+
+ class B(Base):
+ __tablename__ = 'b'
+
+ id = Column(Integer, primary_key=True)
+ a_id = Column(Integer, ForeignKey('a.id'))
+ a = relationship("A", backref=backref("bs", viewonly=True))
+
+ e = create_engine("sqlite://")
+ Base.metadata.create_all(e)
+
+ a = A()
+ b = B()
+
+ sess = Session(e)
+ sess.add_all([a, b])
+ sess.commit()
+
+ b.a = a
+
+ assert b in sess.dirty
+
+ # before 0.9.0b2
+ # assert a in sess.dirty
+ # assert inspect(a).attrs.bs.history.has_changes()
+
+ # after 0.9.0b2
+ assert a not in sess.dirty
+ assert not inspect(a).attrs.bs.history.has_changes()
+
+:ticket:`2833`
.. _migration_2751:
callable_, dispatch, trackparent=False, extension=None,
compare_function=None, active_history=False,
parent_token=None, expire_missing=True,
+ send_modified_events=True,
**kwargs):
"""Construct an AttributeImpl.
during state.expire_attributes(None), if no value is present
for this key.
+ send_modified_events
+ if False, the InstanceState._modified_event method will have no effect;
+ this means the attribute will never show up as changed in a
+ history entry.
"""
self.class_ = class_
self.key = key
self.dispatch = dispatch
self.trackparent = trackparent
self.parent_token = parent_token or self
+ self.send_modified_events = send_modified_events
if compare_function is None:
self.is_equal = operator.eq
else:
from sqlalchemy.testing import fixtures
from test.orm import _fixtures
from sqlalchemy import exc
+from sqlalchemy import inspect
class _RelationshipErrors(object):
def _assert_raises_no_relevant_fks(self, fn, expr, relname,
assert t3.count().scalar() == 1
+class ViewOnlyHistoryTest(fixtures.MappedTest):
+ @classmethod
+ def define_tables(cls, metadata):
+ Table("t1", metadata,
+ Column('id', Integer, primary_key=True,
+ test_needs_autoincrement=True),
+ Column('data', String(40)))
+ Table("t2", metadata,
+ Column('id', Integer, primary_key=True,
+ test_needs_autoincrement=True),
+ Column('data', String(40)),
+ Column('t1id', Integer, ForeignKey('t1.id')))
+
+ def _assert_fk(self, a1, b1, is_set):
+ s = Session(testing.db)
+ s.add_all([a1, b1])
+ s.flush()
+
+ if is_set:
+ eq_(b1.t1id, a1.id)
+ else:
+ eq_(b1.t1id, None)
+
+ return s
+
+ def test_o2m_viewonly_oneside(self):
+ class A(fixtures.ComparableEntity):
+ pass
+ class B(fixtures.ComparableEntity):
+ pass
+
+ mapper(A, self.tables.t1, properties={
+ "bs": relationship(B, viewonly=True,
+ backref=backref("a", viewonly=False))
+ })
+ mapper(B, self.tables.t2)
+
+ a1 = A()
+ b1 = B()
+ a1.bs.append(b1)
+ assert b1.a is a1
+ assert not inspect(a1).attrs.bs.history.has_changes()
+ assert inspect(b1).attrs.a.history.has_changes()
+
+ sess = self._assert_fk(a1, b1, True)
+
+ a1.bs.remove(b1)
+ assert a1 not in sess.dirty
+ assert b1 in sess.dirty
+
+ def test_m2o_viewonly_oneside(self):
+ class A(fixtures.ComparableEntity):
+ pass
+ class B(fixtures.ComparableEntity):
+ pass
+
+ mapper(A, self.tables.t1, properties={
+ "bs": relationship(B, viewonly=False,
+ backref=backref("a", viewonly=True))
+ })
+ mapper(B, self.tables.t2)
+
+ a1 = A()
+ b1 = B()
+ b1.a = a1
+ assert b1 in a1.bs
+ assert inspect(a1).attrs.bs.history.has_changes()
+ assert not inspect(b1).attrs.a.history.has_changes()
+
+ sess = self._assert_fk(a1, b1, True)
+
+ a1.bs.remove(b1)
+ assert a1 in sess.dirty
+ assert b1 not in sess.dirty
+
+ def test_o2m_viewonly_only(self):
+ class A(fixtures.ComparableEntity):
+ pass
+ class B(fixtures.ComparableEntity):
+ pass
+
+ mapper(A, self.tables.t1, properties={
+ "bs": relationship(B, viewonly=True)
+ })
+ mapper(B, self.tables.t2)
+
+ a1 = A()
+ b1 = B()
+ a1.bs.append(b1)
+ assert not inspect(a1).attrs.bs.history.has_changes()
+
+ self._assert_fk(a1, b1, False)
+
+ def test_m2o_viewonly_only(self):
+ class A(fixtures.ComparableEntity):
+ pass
+ class B(fixtures.ComparableEntity):
+ pass
+
+ mapper(A, self.tables.t1)
+ mapper(B, self.tables.t2, properties={
+ 'a': relationship(A, viewonly=True)
+ })
+
+ a1 = A()
+ b1 = B()
+ b1.a = a1
+ assert not inspect(b1).attrs.a.history.has_changes()
+
+ self._assert_fk(a1, b1, False)
+
class ViewOnlyM2MBackrefTest(fixtures.MappedTest):
@classmethod
def define_tables(cls, metadata):
a1 = A()
b1 = B(as_=[a1])
+ assert not inspect(b1).attrs.as_.history.has_changes()
+
sess.add(a1)
sess.flush()
eq_(