Skip to content

Commit

Permalink
Add raise/raiseload relationship loading strategy
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
ThiefMaster committed Aug 11, 2015
1 parent 5198b1d commit 24f2f71
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 2 deletions.
9 changes: 9 additions & 0 deletions doc/build/changelog/changelog_10.rst
Expand Up @@ -18,6 +18,15 @@
.. changelog::
:version: 1.0.9

.. 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, orm
:tickets: 3510
Expand Down
4 changes: 4 additions & 0 deletions doc/build/orm/collections.rst
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions doc/build/orm/loading_relationships.rst
Expand Up @@ -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
Expand Down Expand Up @@ -617,6 +617,8 @@ Relationship Loader API

.. autofunction:: noload

.. autofunction:: raiseload

.. autofunction:: subqueryload

.. autofunction:: subqueryload_all
1 change: 1 addition & 0 deletions lib/sqlalchemy/orm/__init__.py
Expand Up @@ -237,6 +237,7 @@ def clear_mappers():
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
Expand Down
4 changes: 4 additions & 0 deletions lib/sqlalchemy/orm/relationships.py
Expand Up @@ -524,6 +524,10 @@ class Parent(Base):
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
Expand Down
27 changes: 27 additions & 0 deletions lib/sqlalchemy/orm/strategies.py
Expand Up @@ -353,6 +353,33 @@ def invoke_no_load(state, dict_, row):
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")
Expand Down
23 changes: 23 additions & 0 deletions lib/sqlalchemy/orm/strategy_options.py
Expand Up @@ -853,6 +853,29 @@ def noload(*keys):
return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {})


@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.0.9
"""

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.
Expand Down
79 changes: 79 additions & 0 deletions test/orm/test_mapper.py
Expand Up @@ -2717,6 +2717,85 @@ def go():
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."""
Expand Down

0 comments on commit 24f2f71

Please sign in to comment.