summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrian Moennich <adrian@planetcoding.net>2015-08-11 23:08:28 +0200
committerAdrian Moennich <adrian@planetcoding.net>2015-08-12 18:39:43 +0200
commit6d334da16553c91cbe82de7134042dbe3c511630 (patch)
treedb66cc1463a64ec7cb2d9f986521d3d567e659da
parent5198b1de31029cc985102cd13569086a7056c2f1 (diff)
downloadsqlalchemy-pr/193.tar.gz
Add raise/raiseload relationship loading strategypr/193
- available via `lazy='raise'` or by setting the `raiseload` strategy via `options()` - behaves almost like `lazy='noload'`, but instead of returning `None` it raises `InvalidRequestError` - based on code from Mike Bayer that was posted to the sqlalchemy mailing list: https://groups.google.com/forum/#!topic/sqlalchemy/X_wA8K97smE
-rw-r--r--doc/build/changelog/changelog_11.rst11
-rw-r--r--doc/build/orm/collections.rst4
-rw-r--r--doc/build/orm/loading_relationships.rst6
-rw-r--r--lib/sqlalchemy/orm/__init__.py1
-rw-r--r--lib/sqlalchemy/orm/relationships.py4
-rw-r--r--lib/sqlalchemy/orm/strategies.py27
-rw-r--r--lib/sqlalchemy/orm/strategy_options.py23
-rw-r--r--test/orm/test_mapper.py79
8 files changed, 152 insertions, 3 deletions
diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst
index bb395a826..da6211d9e 100644
--- a/doc/build/changelog/changelog_11.rst
+++ b/doc/build/changelog/changelog_11.rst
@@ -22,6 +22,15 @@
:version: 1.1.0b1
.. change::
+ :tags: feature, orm
+ :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 instaed of returning ``None`` it raises an
+ InvalidRequestError. Pull request courtesy Adrian Moennich.
+
+ .. change::
:tags: bug, mssql
:tickets: 3504
@@ -37,4 +46,4 @@
.. seealso::
- :ref:`change_3504` \ No newline at end of file
+ :ref:`change_3504`
diff --git a/doc/build/orm/collections.rst b/doc/build/orm/collections.rst
index 7d474ce65..4ba50b28a 100644
--- a/doc/build/orm/collections.rst
+++ b/doc/build/orm/collections.rst
@@ -107,6 +107,10 @@ 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.
+If the application is expected to never access such an unloaded relationship,
+``lazy='raise'`` may be used. Accessing such a relationship will raise an
+:exc:`~sqlalchemy.exc.InvalidRequestError`.
+
.. _passive_deletes:
Using Passive Deletes
diff --git a/doc/build/orm/loading_relationships.rst b/doc/build/orm/loading_relationships.rst
index 297392f3e..8cb1f0e02 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
@@ -617,6 +617,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 e02a271e3..c18692095 100644
--- a/lib/sqlalchemy/orm/__init__.py
+++ b/lib/sqlalchemy/orm/__init__.py
@@ -237,6 +237,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 da0730f46..251930873 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -524,6 +524,10 @@ class RelationshipProperty(StrategizedProperty):
support "write-only" attributes, or attributes which are
populated in some manner specific to the application.
+ * ``raise`` - no loading should occur at any time, and accessing
+ the attribute will fail with an
+ :exc:`~sqlalchemy.exc.InvalidRequestError`.
+
* ``dynamic`` - the attribute will return a pre-configured
:class:`.Query` object for all read
operations, onto which further filtering operations can be
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 67dac1ccc..8fc0a8d68 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 cb7a5fef7..5b2a190b9 100644
--- a/lib/sqlalchemy/orm/strategy_options.py
+++ b/lib/sqlalchemy/orm/strategy_options.py
@@ -854,6 +854,29 @@ def noload(*keys):
@loader_option()
+def raiseload(loadopt, attr):
+ """Indicate that the given relationship attribute should remain unloaded.
+
+ Accessing the relationship attribute anyway raises an
+ :exc:`~sqlalchemy.exc.InvalidRequestError`.
+
+ 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.0
+ """
+
+ 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..237371dc7 100644
--- a/test/orm/test_mapper.py
+++ b/test/orm/test_mapper.py
@@ -2717,6 +2717,85 @@ class NoLoadTest(_fixtures.FixtureTest):
self.sql_count_(0, go)
+class RaiseLoadTest(_fixtures.FixtureTest):
+ run_inserts = 'once'
+ run_deletes = None
+
+ def test_o2m_raiseload(self):
+ Address, addresses, users, User = (
+ self.classes.Address,
+ self.tables.addresses,
+ self.tables.users,
+ self.classes.User)
+
+ m = mapper(User, users, properties=dict(
+ addresses=relationship(mapper(Address, addresses), lazy='raise')
+ ))
+ q = create_session().query(m)
+ 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
+ print(x)
+ self.assert_sql_count(testing.db, go, 1)
+
+ self.assert_result(
+ l[0], User,
+ {'id': 7},
+ )
+
+ def test_upgrade_o2m_raiseload_lazyload_option(self):
+ Address, addresses, users, User = (
+ self.classes.Address,
+ self.tables.addresses,
+ self.tables.users,
+ self.classes.User)
+
+ m = mapper(User, users, properties=dict(
+ addresses=relationship(mapper(Address, addresses), lazy='raise')
+ ))
+ q = create_session().query(m).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."""