:ref:`change_2528`
+ .. change::
+ :tags: feature, orm
+ :tickets: 3512
+ :pullreq: github:193
+
+ Added new relationship loading strategy :func:`.orm.raiseload` (also
+ accessible via ``lazy='raise'``). This strategy behaves almost like
+ :func:`.orm.noload` but instead of returning ``None`` it raises an
+ InvalidRequestError. Pull request courtesy Adrian Moennich.
+
+ .. seealso::
+
+ :ref:`change_3512`
+
.. change::
:tags: bug, mssql
:tickets: 3504
:ticket:`3297`
+.. _change_3512:
+
+New "raise" loader strategy
+---------------------------
+
+To assist with the use case of preventing unwanted lazy loads from occurring
+after a series of objects are loaded, the new "lazy='raise'" strategy and
+corresponding loader option :func:`.orm.raiseload` may be applied to a
+relationship attribute which will cause it to raise ``InvalidRequestError``
+when a non-eagerly-loaded attribute is accessed for read::
+
+ >>> from sqlalchemy.orm import raiseload
+ >>> a1 = s.query(A).options(raiseload(A.bs)).first()
+ >>> a1.bs
+ Traceback (most recent call last):
+ ...
+ sqlalchemy.exc.InvalidRequestError: 'A.bs' is not available due to lazy='raise'
+
+:ticket:`3512`
+
New Features and Improvements - Core
====================================
relationships. Newer versions of SQLAlchemy emit warnings or exceptions
in these cases.
-Setting Noload
----------------
+.. _collections_noload_raiseload:
+
+Setting Noload, RaiseLoad
+-------------------------
A "noload" relationship never loads from the database, even when
accessed. It is configured using ``lazy='noload'``::
Above, the ``children`` collection is fully writeable, and changes to it will
be persisted to the database as well as locally available for reading at the
time they are added. However when instances of ``MyClass`` are freshly loaded
-from the database, the ``children`` collection stays empty.
+from the database, the ``children`` collection stays empty. The noload
+strategy is also available on a query option basis using the
+:func:`.orm.noload` loader option.
+
+Alternatively, a "raise"-loaded relationship will raise an
+:exc:`~sqlalchemy.exc.InvalidRequestError` where the attribute would normally
+emit a lazy load::
+
+ class MyClass(Base):
+ __tablename__ = 'some_table'
+
+ children = relationship(MyOtherClass, lazy='raise')
+
+Above, attribute access on the ``children`` collection will raise an exception
+if it was not previously eagerloaded. This includes read access but for
+collections will also affect write access, as collections can't be mutated
+without first loading them. The rationale for this is to ensure that an
+application is not emitting any unexpected lazy loads within a certain context.
+Rather than having to read through SQL logs to determine that all necessary
+attributes were eager loaded, the "raise" strategy will cause unloaded
+attributes to raise immediately if accessed. The raise strategy is
+also available on a query option basis using the :func:`.orm.raiseload`
+loader option.
+
+.. versionadded:: 1.1 added the "raise" loader strategy.
.. _passive_deletes:
Default loader strategies as a new feature.
Each of :func:`.joinedload`, :func:`.subqueryload`, :func:`.lazyload`,
-and :func:`.noload` can be used to set the default style of
-:func:`.relationship` loading
+:func:`.noload`, and :func:`.raiseload` can be used to set the default
+style of :func:`.relationship` loading
for a particular query, affecting all :func:`.relationship` -mapped
attributes not otherwise
specified in the :class:`.Query`. This feature is available by passing
.. autofunction:: noload
+.. autofunction:: raiseload
+
.. autofunction:: subqueryload
.. autofunction:: subqueryload_all
subqueryload_all = strategy_options.subqueryload_all._unbound_all_fn
immediateload = strategy_options.immediateload._unbound_fn
noload = strategy_options.noload._unbound_fn
+raiseload = strategy_options.raiseload._unbound_fn
defaultload = strategy_options.defaultload._unbound_fn
from .strategy_options import Load
support "write-only" attributes, or attributes which are
populated in some manner specific to the application.
+ * ``raise`` - lazy loading is disallowed; accessing
+ the attribute, if its value were not already loaded via eager
+ loading, will raise an :exc:`~sqlalchemy.exc.InvalidRequestError`.
+
+ .. versionadded:: 1.1
+
* ``dynamic`` - the attribute will return a pre-configured
:class:`.Query` object for all read
operations, onto which further filtering operations can be
:ref:`dynamic_relationship` - detail on the ``dynamic`` option.
+ :ref:`collections_noload_raiseload` - notes on "noload" and "raise"
+
:param load_on_pending=False:
Indicates loading behavior for transient or pending parent objects.
populators["new"].append((self.key, invoke_no_load))
+@log.class_logger
+@properties.RelationshipProperty.strategy_for(lazy="raise")
+class RaiseLoader(NoLoader):
+ """Provide loading behavior for a :class:`.RelationshipProperty`
+ with "lazy='raise'".
+
+ """
+
+ __slots__ = ()
+
+ def create_row_processor(
+ self, context, path, loadopt, mapper,
+ result, adapter, populators):
+
+ def invoke_raise_load(state, passive):
+ raise sa_exc.InvalidRequestError(
+ "'%s' is not available due to lazy='raise'" % self
+ )
+
+ set_lazy_callable = InstanceState._instance_level_callable_processor(
+ mapper.class_manager,
+ invoke_raise_load,
+ self.key
+ )
+ populators["new"].append((self.key, set_lazy_callable))
+
+
@log.class_logger
@properties.RelationshipProperty.strategy_for(lazy=True)
@properties.RelationshipProperty.strategy_for(lazy="select")
return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {})
+@loader_option()
+def raiseload(loadopt, attr):
+ """Indicate that the given relationship attribute should disallow lazy loads.
+
+ A relationship attribute configured with :func:`.orm.raiseload` will
+ raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The
+ typical way this is useful is when an application is attempting to ensure
+ that all relationship attributes that are accessed in a particular context
+ would have been already loaded via eager loading. Instead of having
+ to read through SQL logs to ensure lazy loads aren't occurring, this
+ strategy will cause them to raise immediately.
+
+ This function is part of the :class:`.Load` interface and supports
+ both method-chained and standalone operation.
+
+ :func:`.orm.raiseload` applies to :func:`.relationship` attributes only.
+
+ .. versionadded:: 1.1
+
+ """
+
+ return loadopt.set_relationship_strategy(attr, {"lazy": "raise"})
+
+
+@raiseload._add_unbound_fn
+def raiseload(*keys):
+ return _UnboundLoad._from_keys(_UnboundLoad.raiseload, keys, False, {})
+
+
@loader_option()
def defaultload(loadopt, attr):
"""Indicate an attribute should load using its default loader style.
self.sql_count_(0, go)
+class RaiseLoadTest(_fixtures.FixtureTest):
+ run_inserts = 'once'
+ run_deletes = None
+
+ def test_o2m_raiseload_mapper(self):
+ Address, addresses, users, User = (
+ self.classes.Address,
+ self.tables.addresses,
+ self.tables.users,
+ self.classes.User)
+
+ mapper(Address, addresses)
+ mapper(User, users, properties=dict(
+ addresses=relationship(Address, lazy='raise')
+ ))
+ q = create_session().query(User)
+ l = [None]
+
+ def go():
+ x = q.filter(User.id == 7).all()
+ assert_raises_message(
+ sa.exc.InvalidRequestError,
+ "'User.addresses' is not available due to lazy='raise'",
+ lambda: x[0].addresses)
+ l[0] = x
+ self.assert_sql_count(testing.db, go, 1)
+
+ self.assert_result(
+ l[0], User,
+ {'id': 7},
+ )
+
+ def test_o2m_raiseload_option(self):
+ Address, addresses, users, User = (
+ self.classes.Address,
+ self.tables.addresses,
+ self.tables.users,
+ self.classes.User)
+
+ mapper(Address, addresses)
+ mapper(User, users, properties=dict(
+ addresses=relationship(Address)
+ ))
+ q = create_session().query(User)
+ l = [None]
+
+ def go():
+ x = q.options(
+ sa.orm.raiseload(User.addresses)).filter(User.id == 7).all()
+ assert_raises_message(
+ sa.exc.InvalidRequestError,
+ "'User.addresses' is not available due to lazy='raise'",
+ lambda: x[0].addresses)
+ l[0] = x
+ self.assert_sql_count(testing.db, go, 1)
+
+ self.assert_result(
+ l[0], User,
+ {'id': 7},
+ )
+
+ def test_o2m_raiseload_lazyload_option(self):
+ Address, addresses, users, User = (
+ self.classes.Address,
+ self.tables.addresses,
+ self.tables.users,
+ self.classes.User)
+
+ mapper(Address, addresses)
+ mapper(User, users, properties=dict(
+ addresses=relationship(Address, lazy='raise')
+ ))
+ q = create_session().query(User).options(sa.orm.lazyload('addresses'))
+ l = [None]
+
+ def go():
+ x = q.filter(User.id == 7).all()
+ x[0].addresses
+ l[0] = x
+ self.sql_count_(2, go)
+
+ self.assert_result(
+ l[0], User,
+ {'id': 7, 'addresses': (Address, [{'id': 1}])},
+ )
+
+ def test_m2o_raiseload_option(self):
+ Address, addresses, users, User = (
+ self.classes.Address,
+ self.tables.addresses,
+ self.tables.users,
+ self.classes.User)
+ mapper(Address, addresses, properties={
+ 'user': relationship(User)
+ })
+ mapper(User, users)
+ s = Session()
+ a1 = s.query(Address).filter_by(id=1).options(
+ sa.orm.raiseload('user')).first()
+
+ def go():
+ assert_raises_message(
+ sa.exc.InvalidRequestError,
+ "'Address.user' is not available due to lazy='raise'",
+ lambda: a1.user)
+
+ self.sql_count_(0, go)
+
+
class RequirementsTest(fixtures.MappedTest):
"""Tests the contract for user classes."""