--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 6252
+
+ Fixed issue in the new feature of :meth:`_orm.Session.refresh` introduced
+ by :ticket:`1763` where eagerly loaded relationships are also refreshed,
+ where the ``lazy="raise"`` and ``lazy="raise_on_sql"`` loader strategies
+ would interfere with the :func:`_orm.immediateload` loader strategy, thus
+ breaking the feature for relationships that were loaded with
+ :func:`_orm.selectinload`, :func:`_orm.subqueryload` as well.
expected to change within the transaction outside of the local changes being
made, those use cases would be handled using explicit steps such as this method.
+Using ``populate_existing``, any set of objects that matches a query
+can be refreshed, and it also allows control over relationship loader options.
+E.g. to refresh an instance while also refreshing a related set of objects::
+
+ stmt = (
+ select(User).
+ where(User.name.in_(names)).
+ execution_options(populate_existing=True).
+ options(selectinload(User.addresses)
+ )
+ # will refresh all matching User objects as well as the related
+ # Address objects
+ users = session.execute(stmt).scalars().all()
+
Another use case for ``populate_existing`` is in support of various
attribute loading features that can change how an attribute is loaded on
a per-query basis. Options for which this apply include:
# reload obj1.attr1, obj1.attr2
session.refresh(obj1, ['attr1', 'attr2'])
-An alternative method of refreshing which is often more flexible is to
-use the :meth:`_orm.Query.populate_existing` method of :class:`_orm.Query`.
-With this option, all of the ORM objects returned by the :class:`_orm.Query`
-will be refreshed with the contents of what was loaded in the SELECT::
+.. tip::
- for user in session.query(User).populate_existing().filter(User.name.in_(['a', 'b', 'c'])):
- print(user) # will be refreshed for those columns that came back from the query
+ An alternative method of refreshing which is often more flexible is to
+ use the :ref:`orm_queryguide_populate_existing` feature of the ORM,
+ available for :term:`2.0 style` queries with :func:`_sql.select` as well
+ as from the :meth:`_orm.Query.populate_existing` method of :class:`_orm.Query`
+ within :term:`1.x style` queries. Using this execution option,
+ all of the ORM objects returned in the result set of the statement
+ will be refreshed with data from the database::
+
+ stmt = (
+ select(User).
+ execution_options(populate_existing=True).
+ where((User.name.in_(['a', 'b', 'c']))
+ )
+ for user in session.execute(stmt).scalars():
+ print(user) # will be refreshed for those columns that came back from the query
+
+ See :ref:`orm_queryguide_populate_existing` for further detail.
What Actually Loads
:meth:`.Session.refresh`
- :meth:`_orm.Query.populate_existing` - :class:`_orm.Query` method that refreshes
+ :ref:`orm_queryguide_populate_existing` - allows any ORM query
+ to refresh objects as they would be loaded normally, refreshing
all matching objects in the identity map against the results of a
SELECT statement.
util.raise_(e, with_traceback=sys.exc_info()[2])
def refresh(self, instance, attribute_names=None, with_for_update=None):
- """Expire and refresh the attributes on the given instance.
-
- A query will be issued to the database and all attributes will be
- refreshed with their current database value.
-
- Lazy-loaded relational attributes will remain lazily loaded, so that
- the instance-wide refresh operation will be followed immediately by
- the lazy load of that attribute.
-
- Eagerly-loaded relational attributes will eagerly load within the
- single refresh operation.
+ """Expire and refresh attributes on the given instance.
+
+ The selected attributes will first be expired as they would when using
+ :meth:`_orm.Session.expire`; then a SELECT statement will be issued to
+ the database to refresh column-oriented attributes with the current
+ value available in the current transaction.
+
+ :func:`_orm.relationship` oriented attributes will also be immediately
+ loaded if they were already eagerly loaded on the object, using the
+ same eager loading strategy that they were loaded with originally.
+ Unloaded relationship attributes will remain unloaded, as will
+ relationship attributes that were originally lazy loaded.
+
+ .. versionadded:: 1.4 - the :meth:`_orm.Session.refresh` method
+ can also refresh eagerly loaded attributes.
+
+ .. tip::
+
+ While the :meth:`_orm.Session.refresh` method is capable of
+ refreshing both column and relationship oriented attributes, its
+ primary focus is on refreshing of local column-oriented attributes
+ on a single instance. For more open ended "refresh" functionality,
+ including the ability to refresh the attributes on many objects at
+ once while having explicit control over relationship loader
+ strategies, use the
+ :ref:`populate existing <orm_queryguide_populate_existing>` feature
+ instead.
Note that a highly isolated transaction will return the same values as
were previously read in that same transaction, regardless of changes
- in database state outside of that transaction - usage of
- :meth:`~Session.refresh` usually only makes sense if non-ORM SQL
- statement were emitted in the ongoing transaction, or if autocommit
- mode is turned on.
+ in database state outside of that transaction. Refreshing
+ attributes usually only makes sense at the start of a transaction
+ where database rows have not yet been accessed.
:param attribute_names: optional. An iterable collection of
string attribute names indicating a subset of attributes to
:meth:`_query.Query.with_for_update`.
Supersedes the :paramref:`.Session.refresh.lockmode` parameter.
- .. versionadded:: 1.2
-
.. seealso::
:ref:`session_expire` - introductory material
:meth:`.Session.expire_all`
- :meth:`_orm.Query.populate_existing`
+ :ref:`orm_queryguide_populate_existing` - allows any ORM query
+ to refresh objects as they would be loaded normally.
"""
try:
)
if use_get:
- if self._raise_on_sql:
+ if self._raise_on_sql and not passive & attributes.NO_RAISE:
self._invoke_raise_load(state, passive, "raise_on_sql")
return loading.load_on_pk_identity(
elif util.has_intersection(orm_util._never_set, params.values()):
return None
- if self._raise_on_sql:
+ if self._raise_on_sql and not passive & attributes.NO_RAISE:
self._invoke_raise_load(state, passive, "raise_on_sql")
stmt = stmt.add_criteria(
populators,
):
def load_immediate(state, dict_, row):
- state.get_impl(self.key).get(state, dict_)
+ state.get_impl(self.key).get(
+ state, dict_, attributes.PASSIVE_OFF | attributes.NO_RAISE
+ )
if self._check_recursive_postload(context, path):
return
"""basic tests of lazy loaded attributes"""
+from sqlalchemy import testing
from sqlalchemy.orm import immediateload
from sqlalchemy.orm import mapper
from sqlalchemy.orm import relationship
run_inserts = "once"
run_deletes = None
- def test_basic_option(self):
+ @testing.combinations(
+ ("raise",),
+ ("raise_on_sql",),
+ ("select",),
+ ("immediate"),
+ )
+ def test_basic_option(self, default_lazy):
Address, addresses, users, User = (
self.classes.Address,
self.tables.addresses,
)
mapper(Address, addresses)
- mapper(User, users, properties={"addresses": relationship(Address)})
+ mapper(
+ User,
+ users,
+ properties={"addresses": relationship(Address, lazy=default_lazy)},
+ )
sess = fixture_session()
result = (
result,
)
+ @testing.combinations(
+ ("raise",),
+ ("raise_on_sql",),
+ ("select",),
+ ("immediate"),
+ )
+ def test_basic_option_m2o(self, default_lazy):
+ Address, addresses, users, User = (
+ self.classes.Address,
+ self.tables.addresses,
+ self.tables.users,
+ self.classes.User,
+ )
+
+ mapper(
+ Address,
+ addresses,
+ properties={"user": relationship(User, lazy=default_lazy)},
+ )
+ mapper(User, users)
+ sess = fixture_session()
+
+ result = (
+ sess.query(Address)
+ .options(immediateload(Address.user))
+ .filter(Address.id == 1)
+ .all()
+ )
+ eq_(len(sess.identity_map), 2)
+
+ sess.close()
+
+ eq_(
+ [Address(id=1, email_address="jack@bean.com", user=User(id=7))],
+ result,
+ )
+
def test_basic(self):
Address, addresses, users, User = (
self.classes.Address,
sess = fixture_session(autoflush=False)
return User, Order, Item, sess
- def _eager_config_fixture(self):
+ def _eager_config_fixture(self, default_lazy="selectin"):
User, Address = self.classes.User, self.classes.Address
mapper(
User,
self.tables.users,
- properties={"addresses": relationship(Address, lazy="selectin")},
+ properties={"addresses": relationship(Address, lazy=default_lazy)},
)
mapper(Address, self.tables.addresses)
sess = fixture_session(autoflush=False)
self.assert_sql_count(testing.db, go, 2)
assert "addresses" in u1.__dict__
+ @testing.combinations(
+ ("raise",),
+ ("raise_on_sql",),
+ ("select",),
+ ("immediate"),
+ )
+ def test_runs_query_on_option_refresh(self, default_lazy):
+ User, Address, sess = self._eager_config_fixture(
+ default_lazy=default_lazy
+ )
+
+ u1 = (
+ sess.query(User)
+ .options(selectinload(User.addresses))
+ .filter_by(id=8)
+ .first()
+ )
+ assert "addresses" in u1.__dict__
+ sess.expire(u1)
+
+ def go():
+ eq_(u1.id, 8)
+
+ self.assert_sql_count(testing.db, go, 2)
+ assert "addresses" in u1.__dict__
+
def test_no_query_on_deferred(self):
User, Address, sess = self._deferred_config_fixture()
u1 = sess.query(User).get(8)
sess = fixture_session(autoflush=False)
return User, Order, Item, sess
- def _eager_config_fixture(self):
+ def _eager_config_fixture(self, default_lazy="subquery"):
User, Address = self.classes.User, self.classes.Address
mapper(
User,
self.tables.users,
- properties={"addresses": relationship(Address, lazy="subquery")},
+ properties={"addresses": relationship(Address, lazy=default_lazy)},
)
mapper(Address, self.tables.addresses)
sess = fixture_session(autoflush=False)
self.assert_sql_count(testing.db, go, 2)
assert "addresses" in u1.__dict__
+ @testing.combinations(
+ ("raise",),
+ ("raise_on_sql",),
+ ("select",),
+ ("immediate"),
+ )
+ def test_runs_query_on_option_refresh(self, default_lazy):
+ User, Address, sess = self._eager_config_fixture(
+ default_lazy=default_lazy
+ )
+
+ u1 = (
+ sess.query(User)
+ .options(subqueryload(User.addresses))
+ .filter_by(id=8)
+ .first()
+ )
+ assert "addresses" in u1.__dict__
+ sess.expire(u1)
+
+ def go():
+ eq_(u1.id, 8)
+
+ self.assert_sql_count(testing.db, go, 2)
+ assert "addresses" in u1.__dict__
+
def test_no_query_on_deferred(self):
User, Address, sess = self._deferred_config_fixture()
u1 = sess.query(User).get(8)