New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Possible bug when using Mutable and detached instances #8446
Comments
here's a few things:
assert (
sa.inspect(cached_persistent_account) in cached_detached_account.settings._parents
), "instance not being tracked"
some_other_session = Session()
# dont use pickle. just have an Account that we can merge from outside
# that is independent of this Session.
account = some_other_session.query(Account).get(1)
# establish the Account as already present in the session.
# combined with load=False below this causes a problem
# comment this out, test case works
account_already_loaded = session.query(Account).get(1)
cached_detached_account = account
cached_persistent_account = session.merge(
cached_detached_account,
# or, comment this out, test case works
load=False
) we can make a test case that shows the problem more directly as: import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import sessionmaker
from sqlalchemy.types import JSON
Base = declarative_base()
Session = sessionmaker()
class Account(Base):
__tablename__ = "account"
id = sa.Column(sa.Integer, primary_key=True)
settings = sa.Column(MutableDict.as_mutable(JSON))
def main():
engine = sa.create_engine("sqlite:///:memory:", echo=True)
Base.metadata.create_all(engine)
Session.configure(bind=engine)
session = Session()
session.add(Account(id=1, settings={"email_notifications": False}))
session.commit()
some_other_session = Session()
# dont use pickle. just have an Account that we can merge from outside
# that is independent of this Session.
account = some_other_session.query(Account).get(1)
# establish the Account as already present in the session.
# combined with load=False below this causes a problem
# comment this out, test case works
account_already_loaded = session.query(Account).get(1)
cached_detached_account = account
cached_persistent_account = session.merge(
cached_detached_account,
load=False
)
assert cached_persistent_account is account_already_loaded
cached_persistent_account.settings["email_notifications"] = True
assert sa.inspect(
cached_persistent_account
).modified, "instance not modified"
main() the fix needs to be something like the following, as there's no event hook for "merged existing object with load=False", yet the merge between the persistent and "detached" object still happens, overwriting the dictionary diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py
index cbec06a31f..0ef4b69e39 100644
--- a/lib/sqlalchemy/ext/mutable.py
+++ b/lib/sqlalchemy/ext/mutable.py
@@ -511,6 +511,8 @@ class MutableBase(object):
for val in state_dict["ext.mutable.values"][key]:
val._parents[state] = key
+ event.listen(parent_cls, "_sa_event_merge_wo_load", load, raw=True, propagate=True)
+
event.listen(parent_cls, "load", load, raw=True, propagate=True)
event.listen(
parent_cls, "refresh", load_attrs, raw=True, propagate=True
diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py
index 39659c7232..27bc0bf470 100644
--- a/lib/sqlalchemy/orm/events.py
+++ b/lib/sqlalchemy/orm/events.py
@@ -325,6 +325,9 @@ class InstanceEvents(event.Events):
"""
+ def _sa_event_merge_wo_load(self, target, context):
+ """internal use only"""
+
def load(self, target, context):
"""Receive an object instance after it has been created via
``__new__``, and after initial attribute population has
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index d32af17464..a025a774db 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -311,6 +311,7 @@ class ColumnProperty(StrategizedProperty):
if not load:
dest_dict[self.key] = value
+
else:
impl = dest_state.get_impl(self.key)
impl.set(dest_state, dest_dict, value, None)
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index c6a91693e3..8757632b66 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -3151,6 +3151,7 @@ class Session(_SessionClassMethods):
if not load:
# remove any history
merged_state._commit_all(merged_dict, self.identity_map)
+ merged_state.manager.dispatch._sa_event_merge_wo_load(merged_state, None)
if new_instance:
merged_state.manager.dispatch.load(merged_state, None) |
Thanks @zzzeek I tried out your changes and they work, only that I had to remove the leading score in the event name since the project I'm working in uses SQLAlchemy v1.3. Do you plan to apply these changes in that version too? |
this would be in 1.4.41. releases for 1.3 have been over for about 18 months and is expected to be EOL sometime next year. |
Understood, thanks again! |
Mike Bayer has proposed a fix for this issue in the main branch: implement event for merge/load=False for mutable state setup https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/4056 |
Mike Bayer has proposed a fix for this issue in the rel_1_4 branch: implement event for merge/load=False for mutable state setup https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/4057 |
Fixed issue in :mod:`sqlalchemy.ext.mutable` extension where collection links to the parent object would be lost if the object were merged with :meth:`.Session.merge` while also passing :paramref:`.Session.merge.load` as False. The event added here is currently private for expediency, but is acceptable to become a public event at some point. Fixes: #8446 Change-Id: I9e5b9f1f5a0c5a9781f51635d5e57b1134c9e866 (cherry picked from commit e15cf45)
Describe the bug
Hello!
I have run into this issue.
Mutable
fieldsession.merge(instance, load=False)
Mutable
field in the persistent instance returned bysession.merge()
, those changes are not detected by the sessionHere's the snippet of code and the link to the full example
To Reproduce
I have created this example project: https://github.com/anler/sqlalchemy-mutable-example with branches for SA 1.3 and 1.4
Error
There is no error
Versions
Additional context
No response
Thank you! 🙏🏻
The text was updated successfully, but these errors were encountered: