diff options
author | Adrian Moennich <adrian@planetcoding.net> | 2015-08-11 23:08:28 +0200 |
---|---|---|
committer | Adrian Moennich <adrian@planetcoding.net> | 2015-08-12 18:39:43 +0200 |
commit | 6d334da16553c91cbe82de7134042dbe3c511630 (patch) | |
tree | db66cc1463a64ec7cb2d9f986521d3d567e659da | |
parent | 5198b1de31029cc985102cd13569086a7056c2f1 (diff) | |
download | sqlalchemy-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.rst | 11 | ||||
-rw-r--r-- | doc/build/orm/collections.rst | 4 | ||||
-rw-r--r-- | doc/build/orm/loading_relationships.rst | 6 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 1 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 27 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategy_options.py | 23 | ||||
-rw-r--r-- | test/orm/test_mapper.py | 79 |
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.""" |