example of how to integrate Beaker with SQLAlchemy. See
the notes in the "examples" note below.
+ - the "save-update" cascade will now cascade the pending *removed*
+ values from a scalar or collection attribute into the new session
+ during an add() operation. This so that the flush() operation
+ will also delete or modify rows of those disconnected items.
+
- Using a "dynamic" loader with a "secondary" table now produces
a query where the "secondary" table is *not* aliased. This
allows the secondary Table object to be used in the "order_by"
The above mapper specifies two relations, ``items`` and ``customer``. The ``items`` relationship specifies "all, delete-orphan" as its ``cascade`` value, indicating that all ``add``, ``merge``, ``expunge``, ``refresh`` ``delete`` and ``expire`` operations performed on a parent ``Order`` instance should also be performed on the child ``Item`` instances attached to it. The ``delete-orphan`` cascade value additionally indicates that if an ``Item`` instance is no longer associated with an ``Order``, it should also be deleted. The "all, delete-orphan" cascade argument allows a so-called *lifecycle* relationship between an ``Order`` and an ``Item`` object.
-The ``customer`` relationship specifies only the "save-update" cascade value, indicating most operations will not be cascaded from a parent ``Order`` instance to a child ``User`` instance except for the ``add()`` operation. "save-update" cascade indicates that an ``add()`` on the parent will cascade to all child items, and also that items added to a parent which is already present in the session will also be added.
+The ``customer`` relationship specifies only the "save-update" cascade value, indicating most operations will not be cascaded from a parent ``Order`` instance to a child ``User`` instance except for the ``add()`` operation. "save-update" cascade indicates that an ``add()`` on the parent will cascade to all child items, and also that items added to a parent which is already present in the session will also be added. "save-update" cascade also cascades the *pending history* of a relation()-based attribute, meaning that objects which were removed from a scalar or collection attribute whose changes have not yet been flushed are also placed into the new session - this so that foreign key clear operations and deletions will take place (new in 0.6).
Note that the ``delete-orphan`` cascade only functions for relationships where the target object can have a single parent at a time, meaning it is only appropriate for one-to-one or one-to-many relationships. For a :func:`~sqlalchemy.orm.relation` which establishes one-to-one via a local foreign key, i.e. a many-to-one that stores only a single parent, or one-to-one/one-to-many via a "secondary" (association) table, a warning will be issued if ``delete-orphan`` is configured. To disable this warning, also specify the ``single_parent=True`` flag on the relationship, which constrains objects to allow attachment to only one parent at a time.
return self != HISTORY_BLANK
def sum(self):
- return self.added + self.unchanged + self.deleted
+ return (self.added or []) +\
+ (self.unchanged or []) +\
+ (self.deleted or [])
def non_deleted(self):
- return self.added + self.unchanged
+ return (self.added or []) +\
+ (self.unchanged or [])
def non_added(self):
- return self.unchanged + self.deleted
+ return (self.unchanged or []) +\
+ (self.deleted or [])
def has_changes(self):
return bool(self.added or self.deleted)
else:
passive = attributes.PASSIVE_OFF
- mapper = self.mapper.primary_mapper()
- instances = state.value_as_iterable(self.key, passive=passive)
+ if type_ == 'save-update':
+ instances = attributes.get_state_history(state, self.key, passive=passive).sum()
+ else:
+ instances = state.value_as_iterable(self.key, passive=passive)
+
if instances:
for c in instances:
- if c is not None and c not in visited_instances and (halt_on is None or not halt_on(c)):
+ if c is not None and c not in visited_instances and \
+ (halt_on is None or not halt_on(c)):
if not isinstance(c, self.mapper.class_):
raise AssertionError("Attribute '%s' on class '%s' doesn't handle objects "
- "of type '%s'" % (self.key, str(self.parent.class_), str(c.__class__)))
+ "of type '%s'" % (self.key,
+ str(self.parent.class_),
+ str(c.__class__)))
visited_instances.add(c)
- # cascade using the mapper local to this object, so that its individual properties are located
+ # cascade using the mapper local to this
+ # object, so that its individual properties are located
instance_mapper = object_mapper(c)
yield (c, instance_mapper, attributes.instance_state(c))
self._cascade_save_or_update(state)
def _cascade_save_or_update(self, state):
- for state, mapper in _cascade_unknown_state_iterator('save-update', state, halt_on=lambda c:c in self):
+ for state, mapper in _cascade_unknown_state_iterator(
+ 'save-update', state, halt_on=lambda c:c in self):
self._save_or_update_impl(state)
def delete(self, instance):
from sqlalchemy.test.testing import assert_raises, assert_raises_message
from sqlalchemy import Integer, String, ForeignKey, Sequence, exc as sa_exc
from sqlalchemy.test.schema import Table, Column
-from sqlalchemy.orm import mapper, relation, create_session, class_mapper, backref
+from sqlalchemy.orm import mapper, relation, create_session, sessionmaker, class_mapper, backref
from sqlalchemy.orm import attributes, exc as orm_exc
from sqlalchemy.test import testing
from sqlalchemy.test.testing import eq_
sess.add(o5)
assert_raises_message(orm_exc.FlushError, "is an orphan", sess.flush)
-
+ @testing.resolve_artifact_names
+ def test_save_update_sends_pending(self):
+ """test that newly added and deleted collection items are cascaded on save-update"""
+
+ sess = sessionmaker(expire_on_commit=False)()
+ o1, o2, o3 = Order(description='o1'), Order(description='o2'), Order(description='o3')
+ u = User(name='jack', orders=[o1, o2])
+ sess.add(u)
+ sess.commit()
+ sess.close()
+
+ u.orders.append(o3)
+ u.orders.remove(o1)
+
+ sess.add(u)
+ assert o1 in sess
+ assert o2 in sess
+ assert o3 in sess
+ sess.commit()
+
@testing.resolve_artifact_names
def test_delete(self):
sess = create_session()
assert prefs.count().scalar() == 2
assert extra.count().scalar() == 2
+ @testing.resolve_artifact_names
+ def test_save_update_sends_pending(self):
+ """test that newly added and deleted scalar items are cascaded on save-update"""
+
+ sess = sessionmaker(expire_on_commit=False)()
+ p1, p2 = Pref(data='p1'), Pref(data='p2')
+
+
+ u = User(name='jack', pref=p1)
+ sess.add(u)
+ sess.commit()
+ sess.close()
+
+ u.pref = p2
+
+ sess.add(u)
+ assert p1 in sess
+ assert p2 in sess
+ sess.commit()
+
@testing.fails_on('maxdb', 'FIXME: unknown')
@testing.resolve_artifact_names
def test_orphan_on_update(self):