diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2021-05-12 09:26:03 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2021-05-12 09:26:03 -0400 |
| commit | 71a858817cb8c11451ae577c61329f4239fab46b (patch) | |
| tree | 14fcd7fa0facf6f772280e0aea42d7124aeca509 /lib/sqlalchemy | |
| parent | f6329060f1b2f5631deac3ca877c2ae19e3e6f36 (diff) | |
| download | sqlalchemy-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.py | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/collections.py | 52 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/events.py | 30 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/unitofwork.py | 1 |
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) |
