]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Added loader options to session.merge, asyncsession.merge
authorDaniel Stone <me@danstone.uk>
Mon, 30 Aug 2021 15:15:25 +0000 (11:15 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 2 Sep 2021 19:18:23 +0000 (15:18 -0400)
Added loader options to :meth:`_orm.Session.merge` and
:meth:`_asyncio.AsyncSession.merge`, which will apply the given loader
options to the ``get()`` used internally by merge, allowing eager loading
of relationships etc. to be applied when the merge process loads a new
object. Pull request courtesy Daniel Stone.

Fixes: #6955
Closes: #6957
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/6957
Pull-request-sha: ab4d96cd5da9a5dd01112b8dcd6514db64aa8d9f

Change-Id: I5b94dfda1088a8bc6396e9fd9a072827df1f8680

doc/build/changelog/unreleased_14/6955.rst [new file with mode: 0644]
lib/sqlalchemy/ext/asyncio/session.py
lib/sqlalchemy/orm/session.py
test/ext/asyncio/test_session_py3k.py
test/orm/test_merge.py

diff --git a/doc/build/changelog/unreleased_14/6955.rst b/doc/build/changelog/unreleased_14/6955.rst
new file mode 100644 (file)
index 0000000..5896f3b
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: usecase, orm
+    :tickets: 6955
+
+    Added loader options to :meth:`_orm.Session.merge` and
+    :meth:`_asyncio.AsyncSession.merge`, which will apply the given loader
+    options to the ``get()`` used internally by merge, allowing eager loading
+    of relationships etc. to be applied when the merge process loads a new
+    object. Pull request courtesy Daniel Stone.
index 6b18e3d7c74349df5e8f1267980fe837cafe9d84..e4372f448450aa96c497c681e5fc0e300cd7b662 100644 (file)
@@ -133,6 +133,10 @@ class AsyncSession(ReversibleProxy):
         This is the async version of the :meth:`_orm.Session.refresh` method.
         See that method for a complete description of all options.
 
+        .. seealso::
+
+            :meth:`_orm.Session.refresh` - main documentation for refresh
+
         """
 
         return await greenlet_spawn(
@@ -179,6 +183,11 @@ class AsyncSession(ReversibleProxy):
     ):
         """Execute a statement and return a buffered
         :class:`_engine.Result` object.
+
+        .. seealso::
+
+            :meth:`_orm.Session.execute` - main documentation for execute
+
         """
 
         if execution_options:
@@ -205,7 +214,13 @@ class AsyncSession(ReversibleProxy):
         bind_arguments=None,
         **kw
     ):
-        """Execute a statement and return a scalar result."""
+        """Execute a statement and return a scalar result.
+
+        .. seealso::
+
+            :meth:`_orm.Session.scalar` - main documentation for scalar
+
+        """
 
         result = await self.execute(
             statement,
@@ -228,6 +243,10 @@ class AsyncSession(ReversibleProxy):
         """Return an instance based on the given primary key identifier,
         or ``None`` if not found.
 
+        .. seealso::
+
+            :meth:`_orm.Session.get` - main documentation for get
+
 
         """
         return await greenlet_spawn(
@@ -276,17 +295,24 @@ class AsyncSession(ReversibleProxy):
         As this operation may need to cascade along unloaded relationships,
         it is awaitable to allow for those queries to take place.
 
+        .. seealso::
+
+            :meth:`_orm.Session.delete` - main documentation for delete
 
         """
         return await greenlet_spawn(self.sync_session.delete, instance)
 
-    async def merge(self, instance, load=True):
+    async def merge(self, instance, load=True, options=None):
         """Copy the state of a given instance into a corresponding instance
         within this :class:`_asyncio.AsyncSession`.
 
+        .. seealso::
+
+            :meth:`_orm.Session.merge` - main documentation for merge
+
         """
         return await greenlet_spawn(
-            self.sync_session.merge, instance, load=load
+            self.sync_session.merge, instance, load=load, options=options
         )
 
     async def flush(self, objects=None):
@@ -294,7 +320,7 @@ class AsyncSession(ReversibleProxy):
 
         .. seealso::
 
-            :meth:`_orm.Session.flush`
+            :meth:`_orm.Session.flush` - main documentation for flush
 
         """
         await greenlet_spawn(self.sync_session.flush, objects=objects)
@@ -334,9 +360,17 @@ class AsyncSession(ReversibleProxy):
         r"""Return a :class:`_asyncio.AsyncConnection` object corresponding to
         this :class:`.Session` object's transactional state.
 
+        This method may also be used to establish execution options for the
+        database connection used by the current transaction.
+
         .. versionadded:: 1.4.24  Added **kw arguments which are passed through
            to the underlying :meth:`_orm.Session.connection` method.
 
+        .. seealso::
+
+            :meth:`_orm.Session.connection` - main documentation for
+            "connection"
+
         """
 
         sync_connection = await greenlet_spawn(
index 0bdd5cc959d4ee568cc469001b2f9bba366351b9..a93684126b5c6b7366cc5104239e29008b72ab46 100644 (file)
@@ -2843,7 +2843,7 @@ class Session(_SessionClassMethods):
             load_options=load_options,
         )
 
-    def merge(self, instance, load=True):
+    def merge(self, instance, load=True, options=None):
         """Copy the state of a given instance into a corresponding instance
         within this :class:`.Session`.
 
@@ -2889,6 +2889,11 @@ class Session(_SessionClassMethods):
          produced as "clean", so it is only appropriate that the given objects
          should be "clean" as well, else this suggests a mis-use of the
          method.
+        :param options: optional sequence of loader options which will be
+         applied to the :meth:`_orm.Session.get` method when the merge
+         operation loads the existing version of the object from the database.
+
+         .. versionadded:: 1.4.24
 
 
         .. seealso::
@@ -2916,6 +2921,7 @@ class Session(_SessionClassMethods):
                 attributes.instance_state(instance),
                 attributes.instance_dict(instance),
                 load=load,
+                options=options,
                 _recursive=_recursive,
                 _resolve_conflict_map=_resolve_conflict_map,
             )
@@ -2927,6 +2933,7 @@ class Session(_SessionClassMethods):
         state,
         state_dict,
         load=True,
+        options=None,
         _recursive=None,
         _resolve_conflict_map=None,
     ):
@@ -2990,7 +2997,12 @@ class Session(_SessionClassMethods):
                 new_instance = True
 
             elif key_is_persistent:
-                merged = self.get(mapper.class_, key[1], identity_token=key[2])
+                merged = self.get(
+                    mapper.class_,
+                    key[1],
+                    identity_token=key[2],
+                    options=options,
+                )
 
         if merged is None:
             merged = mapper.class_manager.new_instance()
index 4165991d41893404e24b34e1b405029d6b5959ad..cd90547406cb7df8484881841601b2e7dedcf4dc 100644 (file)
@@ -106,6 +106,17 @@ class AsyncSessionQueryTest(AsyncFixture):
         u3 = await async_session.get(User, 12)
         is_(u3, None)
 
+    @async_test
+    async def test_get_loader_options(self, async_session):
+        User = self.classes.User
+
+        u = await async_session.get(
+            User, 7, options=[selectinload(User.addresses)]
+        )
+
+        eq_(u.name, "jack")
+        eq_(len(u.__dict__["addresses"]), 1)
+
     @async_test
     @testing.requires.independent_cursors
     @testing.combinations(
@@ -335,6 +346,28 @@ class AsyncSessionTransactionTest(AsyncFixture):
             is_(new_u_merged, u1)
             eq_(u1.name, "new u1")
 
+    @async_test
+    async def test_merge_loader_options(self, async_session):
+        User = self.classes.User
+        Address = self.classes.Address
+
+        async with async_session.begin():
+            u1 = User(id=1, name="u1", addresses=[Address(email_address="e1")])
+
+            async_session.add(u1)
+
+        await async_session.close()
+
+        async with async_session.begin():
+            new_u1 = User(id=1, name="new u1")
+
+            new_u_merged = await async_session.merge(
+                new_u1, options=[selectinload(User.addresses)]
+            )
+
+            eq_(new_u_merged.name, "new u1")
+            eq_(len(new_u_merged.__dict__["addresses"]), 1)
+
     @async_test
     async def test_join_to_external_transaction(self, async_engine):
         User = self.classes.User
index a0cb5426f3adacd52735bf59fe5bc6ae8ee43c03..cf4e81c98e0e272fc5ea47f6b8d5598dfb927b64 100644 (file)
@@ -18,6 +18,7 @@ from sqlalchemy.orm import deferred
 from sqlalchemy.orm import foreign
 from sqlalchemy.orm import mapper
 from sqlalchemy.orm import relationship
+from sqlalchemy.orm import selectinload
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import synonym
 from sqlalchemy.orm.collections import attribute_mapped_collection
@@ -52,6 +53,36 @@ class MergeTest(_fixtures.FixtureTest):
 
         return canary
 
+    def test_loader_options(self):
+        User, Address, addresses, users = (
+            self.classes.User,
+            self.classes.Address,
+            self.tables.addresses,
+            self.tables.users,
+        )
+
+        mapper(
+            User,
+            users,
+            properties={"addresses": relationship(Address, backref="user")},
+        )
+        mapper(Address, addresses)
+
+        s = fixture_session()
+        u = User(
+            id=7,
+            name="fred",
+            addresses=[Address(id=1, email_address="jack@bean.com")],
+        )
+        s.add(u)
+        s.commit()
+        s.close()
+
+        u = User(id=7, name="fred")
+        u2 = s.merge(u, options=[selectinload(User.addresses)])
+
+        eq_(len(u2.__dict__["addresses"]), 1)
+
     def test_transient_to_pending(self):
         User, users = self.classes.User, self.tables.users