:ref:`change_3891`
+ .. change:: 3913
+ :tags: bug, orm
+ :tickets: 3913
+
+ When assigning a collection to an attribute mapped by a relationship,
+ the previous collection is no longer mutated. Previously, the old
+ collection would be emptied out in conjunction with the "item remove"
+ events that fire off; the events now fire off without affecting
+ the old collection.
+
+ .. seealso::
+
+ :ref:`change_3913`
+
.. change:: 3932
:tags: bug, oracle
:tickets: 3932
:ticket:`3891`
+.. _change_3913:
+
+Previous collection is no longer mutated upon replacement
+---------------------------------------------------------
+
+The ORM emits events whenever the members of a mapped collection change.
+In the case of assigning a collection to an attribute that would replace
+the previous collection, a side effect of this was that the collection
+being replaced would also be mutated, which is misleading and unnecessary::
+
+ >>> a1, a2, a3 = Address('a1'), Address('a2'), Address('a3')
+ >>> user.addresses = [a1, a2]
+
+ >>> previous_collection = user.addresses
+
+ # replace the collection with a new one
+ >>> user.addresses = [a2, a3]
+
+ >>> previous_collection
+ [Address('a1'), Address('a2')]
+
+Above, prior to the change, the ``previous_collection`` would have had the
+"a1" member removed, corresponding to the member that's no longer in the
+new collection.
+
+:ticket:`3913`
+
Key Behavioral Changes - Core
=============================
appender(member, _sa_initiator=False)
if existing_adapter:
- remover = existing_adapter.bulk_remover()
for member in removals:
- remover(member)
+ existing_adapter.fire_remove_event(member)
def prepare_instrumentation(factory):
u1.addresses = [a2, a3] # <- old collection is disposed
- The mechanics of the event will typically include that the given
- collection is empty, even if it stored objects while being replaced.
+ The old collection received will contain its previous contents.
+
+ .. versionchanged:: 1.2 The collection passed to
+ :meth:`.AttributeEvents.dispose_collection` will now have its
+ contents before the dispose intact; previously, the collection
+ would be empty.
.. versionadded:: 1.0.0 the :meth:`.AttributeEvents.init_collection`
and :meth:`.AttributeEvents.dispose_collection` events supersede
f1.barlist = [b2]
adapter_two = f1.barlist._sa_adapter
eq_(canary.init.mock_calls, [
- call(f1, [], adapter_one),
+ call(f1, [b1], adapter_one), # note the f1.barlist that
+ # we saved earlier has been mutated
+ # in place, new as of [ticket:3913]
call(f1, [b2], adapter_two),
])
eq_(
canary.dispose.mock_calls,
[
- call(f1, [], adapter_one)
+ call(f1, [b1], adapter_one)
]
)
coll = a1.bs
a1.bs.append(B())
a1.bs = []
- # a bulk replace empties the old collection
- assert len(coll) == 0
- coll.append(B())
+ # a bulk replace no longer empties the old collection
+ # as of [ticket:3913]
assert len(coll) == 1
+ coll.append(B())
+ assert len(coll) == 2
def test_pop_existing(self):
A, B = self.A, self.B
bulk1 = [e2]
# empty & sever col1 from obj
obj.attr = bulk1
- self.assert_(len(col1) == 0)
+
+ # as of [ticket:3913] the old collection
+ # remains unchanged
+ self.assert_(len(col1) == 1)
+
self.assert_(len(canary.data) == 1)
self.assert_(obj.attr is not col1)
self.assert_(obj.attr is not bulk1)