summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2016-04-19 13:44:51 -0400
committerGerrit Code Review <gerrit2@ln3.zzzcomputing.com>2016-04-19 13:44:51 -0400
commit56dafa6c0dc1ebb7728a2120cce14f8227b2a97e (patch)
treecaf8f6f4e8a8235641c09c344a18180b7db0d81a
parent6f6e2c48ba0be827ee434891f54eb2173edf9bfc (diff)
parent33921261f8ebfd710ffa6e855d90c142ceb3303c (diff)
downloadsqlalchemy-56dafa6c0dc1ebb7728a2120cce14f8227b2a97e.tar.gz
Merge "Add raise/raiseload relationship loading strategy"
-rw-r--r--doc/build/changelog/changelog_11.rst14
-rw-r--r--doc/build/changelog/migration_11.rst20
-rw-r--r--doc/build/orm/collections.rst32
-rw-r--r--doc/build/orm/loading_relationships.rst6
-rw-r--r--lib/sqlalchemy/orm/__init__.py1
-rw-r--r--lib/sqlalchemy/orm/relationships.py8
-rw-r--r--lib/sqlalchemy/orm/strategies.py27
-rw-r--r--lib/sqlalchemy/orm/strategy_options.py29
-rw-r--r--test/orm/test_mapper.py109
9 files changed, 241 insertions, 5 deletions
diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst
index 426b552e2..959bf9ee4 100644
--- a/doc/build/changelog/changelog_11.rst
+++ b/doc/build/changelog/changelog_11.rst
@@ -861,6 +861,20 @@
: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
diff --git a/doc/build/changelog/migration_11.rst b/doc/build/changelog/migration_11.rst
index fffdc4a9d..2991d1414 100644
--- a/doc/build/changelog/migration_11.rst
+++ b/doc/build/changelog/migration_11.rst
@@ -880,6 +880,26 @@ added to the :ref:`mutable_toplevel` extension, to complement the existing
: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
====================================
diff --git a/doc/build/orm/collections.rst b/doc/build/orm/collections.rst
index 577cd233e..f37a36b40 100644
--- a/doc/build/orm/collections.rst
+++ b/doc/build/orm/collections.rst
@@ -91,8 +91,10 @@ Note that eager/lazy loading options cannot be used in conjunction dynamic relat
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'``::
@@ -105,7 +107,31 @@ 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:
diff --git a/doc/build/orm/loading_relationships.rst b/doc/build/orm/loading_relationships.rst
index 3a0026bbe..941edce2c 100644
--- a/doc/build/orm/loading_relationships.rst
+++ b/doc/build/orm/loading_relationships.rst
@@ -185,8 +185,8 @@ Default Loading Strategies
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
@@ -661,6 +661,8 @@ Relationship Loader API
.. autofunction:: noload
+.. autofunction:: raiseload
+
.. autofunction:: subqueryload
.. autofunction:: subqueryload_all
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py
index 7425737ce..d822c83cb 100644
--- a/lib/sqlalchemy/orm/__init__.py
+++ b/lib/sqlalchemy/orm/__init__.py
@@ -242,6 +242,7 @@ subqueryload = strategy_options.subqueryload._unbound_fn
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
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index 17f94d4af..4d5e5d29d 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -540,6 +540,12 @@ class RelationshipProperty(StrategizedProperty):
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
@@ -559,6 +565,8 @@ class RelationshipProperty(StrategizedProperty):
: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.
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 370cb974b..3c03a681d 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -354,6 +354,33 @@ class NoLoader(AbstractRelationshipLoader):
@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")
class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py
index b7084cc22..97d2c0f29 100644
--- a/lib/sqlalchemy/orm/strategy_options.py
+++ b/lib/sqlalchemy/orm/strategy_options.py
@@ -879,6 +879,35 @@ def noload(*keys):
@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.
diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py
index 6845ababb..d4eed4d92 100644
--- a/test/orm/test_mapper.py
+++ b/test/orm/test_mapper.py
@@ -2717,6 +2717,115 @@ class NoLoadTest(_fixtures.FixtureTest):
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."""