summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2021-05-12 09:26:03 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2021-05-12 09:26:03 -0400
commit71a858817cb8c11451ae577c61329f4239fab46b (patch)
tree14fcd7fa0facf6f772280e0aea42d7124aeca509 /lib/sqlalchemy
parentf6329060f1b2f5631deac3ca877c2ae19e3e6f36 (diff)
downloadsqlalchemy-71a858817cb8c11451ae577c61329f4239fab46b.tar.gz
Create new event for collection add w/o mutation
Fixed issue when using :paramref:`_orm.relationship.cascade_backrefs` parameter set to ``False``, which per :ref:`change_5150` is set to become the standard behavior in SQLAlchemy 2.0, where adding the item to a collection that uniquifies, such as ``set`` or ``dict`` would fail to fire a cascade event if the object were already associated in that collection via the backref. This fix represents a fundamental change in the collection mechanics by introducing a new event state which can fire off for a collection mutation even if there is no net change on the collection; the action is now suited using a new event hook :meth:`_orm.AttributeEvents.append_wo_mutation`. Fixes: #6471 Change-Id: Ic50413f7e62440dad33ab84838098ea62ff4e815
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/orm/attributes.py6
-rw-r--r--lib/sqlalchemy/orm/collections.py52
-rw-r--r--lib/sqlalchemy/orm/events.py30
-rw-r--r--lib/sqlalchemy/orm/unitofwork.py1
4 files changed, 88 insertions, 1 deletions
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index 05b12dda2..105a9cfd2 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -1389,6 +1389,12 @@ class CollectionAttributeImpl(AttributeImpl):
return value
+ def fire_append_wo_mutation_event(self, state, dict_, value, initiator):
+ for fn in self.dispatch.append_wo_mutation:
+ value = fn(state, value, initiator or self._append_token)
+
+ return value
+
def fire_pre_remove_event(self, state, dict_, initiator):
"""A special event used for pop() operations.
diff --git a/lib/sqlalchemy/orm/collections.py b/lib/sqlalchemy/orm/collections.py
index 63278fb7e..3874cd5f9 100644
--- a/lib/sqlalchemy/orm/collections.py
+++ b/lib/sqlalchemy/orm/collections.py
@@ -708,6 +708,32 @@ class CollectionAdapter(object):
__nonzero__ = __bool__
+ def fire_append_wo_mutation_event(self, item, initiator=None):
+ """Notify that a entity is entering the collection but is already
+ present.
+
+
+ Initiator is a token owned by the InstrumentedAttribute that
+ initiated the membership mutation, and should be left as None
+ unless you are passing along an initiator value from a chained
+ operation.
+
+ .. versionadded:: 1.4.15
+
+ """
+ if initiator is not False:
+ if self.invalidated:
+ self._warn_invalidated()
+
+ if self.empty:
+ self._reset_empty()
+
+ return self.attr.fire_append_wo_mutation_event(
+ self.owner_state, self.owner_state.dict, item, initiator
+ )
+ else:
+ return item
+
def fire_append_event(self, item, initiator=None):
"""Notify that a entity has entered the collection.
@@ -1083,6 +1109,18 @@ def _instrument_membership_mutator(method, before, argument, after):
return wrapper
+def __set_wo_mutation(collection, item, _sa_initiator=None):
+ """Run set wo mutation events.
+
+ The collection is not mutated.
+
+ """
+ if _sa_initiator is not False:
+ executor = collection._sa_adapter
+ if executor:
+ executor.fire_append_wo_mutation_event(item, _sa_initiator)
+
+
def __set(collection, item, _sa_initiator=None):
"""Run set events.
@@ -1351,7 +1389,11 @@ def _dict_decorators():
self.__setitem__(key, default)
return default
else:
- return self.__getitem__(key)
+ value = self.__getitem__(key)
+ if value is default:
+ __set_wo_mutation(self, value, None)
+
+ return value
_tidy(setdefault)
return setdefault
@@ -1363,13 +1405,19 @@ def _dict_decorators():
for key in list(__other):
if key not in self or self[key] is not __other[key]:
self[key] = __other[key]
+ else:
+ __set_wo_mutation(self, __other[key], None)
else:
for key, value in __other:
if key not in self or self[key] is not value:
self[key] = value
+ else:
+ __set_wo_mutation(self, value, None)
for key in kw:
if key not in self or self[key] is not kw[key]:
self[key] = kw[key]
+ else:
+ __set_wo_mutation(self, kw[key], None)
_tidy(update)
return update
@@ -1410,6 +1458,8 @@ def _set_decorators():
def add(self, value, _sa_initiator=None):
if value not in self:
value = __set(self, value, _sa_initiator)
+ else:
+ __set_wo_mutation(self, value, _sa_initiator)
# testlib.pragma exempt:__hash__
fn(self, value)
diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py
index 0824ae7de..926c2dea7 100644
--- a/lib/sqlalchemy/orm/events.py
+++ b/lib/sqlalchemy/orm/events.py
@@ -2295,6 +2295,36 @@ class AttributeEvents(event.Events):
"""
+ def append_wo_mutation(self, target, value, initiator):
+ """Receive a collection append event where the collection was not
+ actually mutated.
+
+ This event differs from :meth:`_orm.AttributeEvents.append` in that
+ it is fired off for de-duplicating collections such as sets and
+ dictionaries, when the object already exists in the target collection.
+ The event does not have a return value and the identity of the
+ given object cannot be changed.
+
+ The event is used for cascading objects into a :class:`_orm.Session`
+ when the collection has already been mutated via a backref event.
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param value: the value that would be appended if the object did not
+ already exist in the collection.
+ :param initiator: An instance of :class:`.attributes.Event`
+ representing the initiation of the event. May be modified
+ from its original value by backref handlers in order to control
+ chained event propagation, as well as be inspected for information
+ about the source of the event.
+
+ :return: No return value is defined for this event.
+
+ .. versionadded:: 1.4.15
+
+ """
+
def bulk_replace(self, target, values, initiator):
"""Receive a collection 'bulk replace' event.
diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py
index 77bbb4751..ae99da059 100644
--- a/lib/sqlalchemy/orm/unitofwork.py
+++ b/lib/sqlalchemy/orm/unitofwork.py
@@ -144,6 +144,7 @@ def track_cascade_events(descriptor, prop):
sess.expunge(oldvalue)
return newvalue
+ event.listen(descriptor, "append_wo_mutation", append, raw=True)
event.listen(descriptor, "append", append, raw=True, retval=True)
event.listen(descriptor, "remove", remove, raw=True, retval=True)
event.listen(descriptor, "set", set_, raw=True, retval=True)