diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-11-23 10:58:28 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-11-24 11:50:48 -0500 |
| commit | df3bd8d29740e846d7faac842a6e2de16cf483f0 (patch) | |
| tree | 32ebad081b7279839b2e26fde79a4e010ea2f2fc /lib/sqlalchemy/orm | |
| parent | fbec926c4744aa97a48a011939354c8b8f8be566 (diff) | |
| download | sqlalchemy-df3bd8d29740e846d7faac842a6e2de16cf483f0.tar.gz | |
add "merge" to viewonly cascades; propagate NO_RAISE when merging
Fixed bug where :meth:`_orm.Session.merge` would fail to preserve the
current loaded contents of relationship attributes that were indicated with
the :paramref:`_orm.relationship.viewonly` parameter, thus defeating
strategies that use :meth:`_orm.Session.merge` to pull fully loaded objects
from caches and other similar techniques. In a related change, fixed issue
where an object that contains a loaded relationship that was nonetheless
configured as ``lazy='raise'`` on the mapping would fail when passed to
:meth:`_orm.Session.merge`; checks for "raise" are now suspended within
the merge process assuming the :paramref:`_orm.Session.merge.load`
parameter remains at its default of ``True``.
Overall, this is a behavioral adjustment to a change introduced in the 1.4
series as of :ticket:`4994`, which took "merge" out of the set of cascades
applied by default to "viewonly" relationships. As "viewonly" relationships
aren't persisted under any circumstances, allowing their contents to
transfer during "merge" does not impact the persistence behavior of the
target object. This allows :meth:`_orm.Session.merge` to correctly suit one
of its use cases, that of adding objects to a :class:`.Session` that were
loaded elsewhere, often for the purposes of restoring from a cache.
Fixes: #8862
Change-Id: I8731c7810460e6a71f8bf5e8ded59142b9b02956
Diffstat (limited to 'lib/sqlalchemy/orm')
| -rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/base.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 2 |
4 files changed, 26 insertions, 4 deletions
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 89beedc47..5e6852cbf 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -1942,7 +1942,13 @@ class CollectionAttributeImpl(HasCollectionAdapter, AttributeImpl): self.dispatch.bulk_replace(state, new_values, evt, keys=new_keys) - old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT) + # propagate NO_RAISE in passive through to the get() for the + # existing object (ticket #8862) + old = self.get( + state, + dict_, + passive=PASSIVE_ONLY_PERSISTENT ^ (passive & PassiveFlag.NO_RAISE), + ) if old is PASSIVE_NO_RESULT: old = self._default_value(state, dict_) elif old is orig_iterable: diff --git a/lib/sqlalchemy/orm/base.py b/lib/sqlalchemy/orm/base.py index 032364ff4..8f6a81177 100644 --- a/lib/sqlalchemy/orm/base.py +++ b/lib/sqlalchemy/orm/base.py @@ -171,6 +171,13 @@ class PassiveFlag(FastIntFlag): PASSIVE_ONLY_PERSISTENT = PASSIVE_OFF ^ NON_PERSISTENT_OK "PASSIVE_OFF ^ NON_PERSISTENT_OK" + PASSIVE_MERGE = PASSIVE_OFF | NO_RAISE + """PASSIVE_OFF | NO_RAISE + + Symbol used specifically for session.merge() and similar cases + + """ + ( NO_CHANGE, @@ -189,6 +196,7 @@ class PassiveFlag(FastIntFlag): PASSIVE_NO_FETCH, PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT, + PASSIVE_MERGE, ) = PassiveFlag.__members__.values() DEFAULT_MANAGER_ATTR = "_sa_class_manager" diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 020fae600..443801b32 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -1393,7 +1393,9 @@ class RelationshipProperty( # map for those already present. # also assumes CollectionAttributeImpl behavior of loading # "old" list in any case - dest_state.get_impl(self.key).get(dest_state, dest_dict) + dest_state.get_impl(self.key).get( + dest_state, dest_dict, passive=PassiveFlag.PASSIVE_MERGE + ) dest_list = [] for current in instances_iterable: @@ -1419,7 +1421,13 @@ class RelationshipProperty( else: dest_impl = dest_state.get_impl(self.key) assert is_has_collection_adapter(dest_impl) - dest_impl.set(dest_state, dest_dict, dest_list, _adapt=False) + dest_impl.set( + dest_state, + dest_dict, + dest_list, + _adapt=False, + passive=PassiveFlag.PASSIVE_MERGE, + ) else: current = source_dict[self.key] if current is not None: diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 06f2d6d1d..6250cd104 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -136,7 +136,7 @@ class CascadeOptions(FrozenSet[str]): ) _allowed_cascades = all_cascades - _viewonly_cascades = ["expunge", "all", "none", "refresh-expire"] + _viewonly_cascades = ["expunge", "all", "none", "refresh-expire", "merge"] __slots__ = ( "save_update", |
