From 22d5a1e415d603d253870719466152b9e817e1e5 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 6 Oct 2013 20:12:28 -0400 Subject: [PATCH] 11th hour realization that Load() needs to do the _chop_path() thing as well. this probably has some bugs --- doc/build/orm/mapper_config.rst | 21 ++++--- lib/sqlalchemy/orm/interfaces.py | 3 + lib/sqlalchemy/orm/strategy_options.py | 77 +++++++++++++++++-------- test/orm/test_deferred.py | 79 ++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 32 deletions(-) diff --git a/doc/build/orm/mapper_config.rst b/doc/build/orm/mapper_config.rst index 4090152007..64d7d4735f 100644 --- a/doc/build/orm/mapper_config.rst +++ b/doc/build/orm/mapper_config.rst @@ -319,13 +319,6 @@ level using options, including :func:`.orm.defer` and :func:`.orm.undefer`:: query = query.options(undefer('excerpt')) query.all() -An arbitrary set of columns can be selected as "load only" columns, which will -be loaded while deferring all other columns on a given entity, using :func:`.orm.load_only`:: - - from sqlalchemy.orm import load_only - - session.query(Book).options(load_only("summary", "excerpt")) - :func:`.orm.deferred` attributes which are marked with a "group" can be undeferred using :func:`.orm.undefer_group`, sending in the group name:: @@ -334,6 +327,18 @@ using :func:`.orm.undefer_group`, sending in the group name:: query = session.query(Book) query.options(undefer_group('photos')).all() +Load Only Cols +--------------- + +An arbitrary set of columns can be selected as "load only" columns, which will +be loaded while deferring all other columns on a given entity, using :func:`.orm.load_only`:: + + from sqlalchemy.orm import load_only + + session.query(Book).options(load_only("summary", "excerpt")) + +.. versionadded:: 0.9.0 + Deferred Loading with Multiple Entities --------------------------------------- @@ -371,6 +376,8 @@ unchanged, use :func:`.orm.defaultload`:: defaultload(Book.author).load_only("summary", "excerpt"), ) +.. versionadded:: 0.9.0 support for :class:`.Load` and other options which + allow for better targeting of deferral options. Column Deferral API ------------------- diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 18723e4f6e..f613967502 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -430,6 +430,9 @@ class StrategizedProperty(MapperProperty): # that the path is stated in terms of our base search_path = dict.__getitem__(path, self) + #if self.key == "email_address": + # import pdb + # pdb.set_trace() # search among: exact match, "attr.*", "default" strategy # if any. for path_key in ( diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index 5f7eb2c257..c066c2dffb 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -92,7 +92,14 @@ class Load(Generative, MapperOption): self._process(query, False) def _process(self, query, raiseerr): - query._attributes.update(self.context) + current_path = query._current_path + if current_path: + for (token, start_path), loader in self.context.items(): + chopped_start_path = self._chop_path(start_path, current_path) + if chopped_start_path is not None: + query._attributes[(token, chopped_start_path)] = loader + else: + query._attributes.update(self.context) def _generate_path(self, path, attr, wildcard_key, raiseerr=True): if raiseerr and not path.has_entity: @@ -205,6 +212,25 @@ class Load(Generative, MapperOption): self.__dict__.update(state) self.path = PathRegistry.deserialize(self.path) + def _chop_path(self, to_chop, path): + i = -1 + + for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)): + if isinstance(c_token, util.string_types): + # TODO: this is approximated from the _UnboundLoad + # version and probably has issues, not fully covered. + + if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN): + return to_chop + elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_token.key: + return None + + if c_token is p_token: + continue + else: + return None + return to_chop[i+1:] + class _UnboundLoad(Load): """Represent a loader option that isn't tied to a root entity. @@ -289,6 +315,23 @@ class _UnboundLoad(Load): return opt + def _chop_path(self, to_chop, path): + i = -1 + for i, (c_token, (p_mapper, p_prop)) in enumerate(zip(to_chop, path.pairs())): + if isinstance(c_token, util.string_types): + if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN): + return to_chop + elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_prop.key: + return None + elif isinstance(c_token, PropComparator): + if c_token.property is not p_prop: + return None + else: + i += 1 + + return to_chop[i:] + + def _bind_loader(self, query, context, raiseerr): start_path = self.path # _current_path implies we're in a @@ -349,22 +392,6 @@ class _UnboundLoad(Load): else: effective_path.set(context, "loader", loader) - def _chop_path(self, to_chop, path): - i = -1 - for i, (c_token, (p_mapper, p_prop)) in enumerate(zip(to_chop, path.pairs())): - if isinstance(c_token, util.string_types): - if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN): - return to_chop - elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_prop.key: - return None - elif isinstance(c_token, PropComparator): - if c_token.property is not p_prop: - return None - else: - i += 1 - - return to_chop[i:] - def _find_entity_prop_comparator(self, query, token, mapper, raiseerr): if _is_aliased_class(mapper): searchfor = mapper @@ -749,7 +776,7 @@ def defaultload(*keys): return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {}) @loader_option() -def defer(loadopt, key, *addl_attrs): +def defer(loadopt, key): """Indicate that the given column-oriented attribute should be deferred, e.g. not loaded until accessed. @@ -801,17 +828,17 @@ def defer(loadopt, key, *addl_attrs): """ return loadopt.set_column_strategy( - (key, ) + addl_attrs, + (key, ), {"deferred": True, "instrument": True} ) @defer._add_unbound_fn -def defer(*key): - return _UnboundLoad._from_keys(_UnboundLoad.defer, key, False, {}) +def defer(key, *addl_attrs): + return _UnboundLoad._from_keys(_UnboundLoad.defer, (key, ) + addl_attrs, False, {}) @loader_option() -def undefer(loadopt, key, *addl_attrs): +def undefer(loadopt, key): """Indicate that the given column-oriented attribute should be undeferred, e.g. specified within the SELECT statement of the entity as a whole. @@ -845,13 +872,13 @@ def undefer(loadopt, key, *addl_attrs): """ return loadopt.set_column_strategy( - (key, ) + addl_attrs, + (key, ), {"deferred": False, "instrument": True} ) @undefer._add_unbound_fn -def undefer(*key): - return _UnboundLoad._from_keys(_UnboundLoad.undefer, key, False, {}) +def undefer(key, *addl_attrs): + return _UnboundLoad._from_keys(_UnboundLoad.undefer, (key, ) + addl_attrs, False, {}) @loader_option() def undefer_group(loadopt, name): diff --git a/test/orm/test_deferred.py b/test/orm/test_deferred.py index 2dcd821dcf..3336f03b53 100644 --- a/test/orm/test_deferred.py +++ b/test/orm/test_deferred.py @@ -374,6 +374,43 @@ class DeferredOptionsTest(AssertsCompiledSQL, _fixtures.FixtureTest): self.sql_count_(0, go) eq_(item.description, 'item 4') + def test_path_entity(self): + """test the legacy *addl_attrs argument.""" + + User = self.classes.User + Order = self.classes.Order + Item = self.classes.Item + + users = self.tables.users + orders = self.tables.orders + items = self.tables.items + order_items = self.tables.order_items + + mapper(User, users, properties={ + "orders": relationship(Order, lazy="joined") + }) + mapper(Order, orders, properties={ + "items": relationship(Item, secondary=order_items, lazy="joined") + }) + mapper(Item, items) + + sess = create_session() + + exp = ("SELECT users.id AS users_id, users.name AS users_name, " + "items_1.id AS items_1_id, orders_1.id AS orders_1_id, " + "orders_1.user_id AS orders_1_user_id, orders_1.address_id " + "AS orders_1_address_id, orders_1.description AS " + "orders_1_description, orders_1.isopen AS orders_1_isopen " + "FROM users LEFT OUTER JOIN orders AS orders_1 " + "ON users.id = orders_1.user_id LEFT OUTER JOIN " + "(order_items AS order_items_1 JOIN items AS items_1 " + "ON items_1.id = order_items_1.item_id) " + "ON orders_1.id = order_items_1.order_id") + + q = sess.query(User).options(defer(User.orders, Order.items, Item.description)) + self.assert_compile(q, exp) + + def test_chained_multi_col_options(self): users, User = self.tables.users, self.classes.User orders, Order = self.tables.orders, self.classes.Order @@ -422,6 +459,48 @@ class DeferredOptionsTest(AssertsCompiledSQL, _fixtures.FixtureTest): "orders.user_id AS orders_user_id, " "orders.isopen AS orders_isopen FROM orders") + def test_load_only_propagate_unbound(self): + self._test_load_only_propagate(False) + + def test_load_only_propagate_bound(self): + self._test_load_only_propagate(True) + + def _test_load_only_propagate(self, use_load): + User = self.classes.User + Address = self.classes.Address + + users = self.tables.users + addresses = self.tables.addresses + + mapper(User, users, properties={ + "addresses": relationship(Address) + }) + mapper(Address, addresses) + + sess = create_session() + expected = [ + ("SELECT users.id AS users_id, users.name AS users_name " + "FROM users WHERE users.id IN (:id_1, :id_2)", {'id_2': 8, 'id_1': 7}), + ("SELECT addresses.id AS addresses_id, " + "addresses.email_address AS addresses_email_address " + "FROM addresses WHERE :param_1 = addresses.user_id", {'param_1': 7}), + ("SELECT addresses.id AS addresses_id, " + "addresses.email_address AS addresses_email_address " + "FROM addresses WHERE :param_1 = addresses.user_id", {'param_1': 8}), + ] + + if use_load: + opt = Load(User).defaultload(User.addresses).load_only("id", "email_address") + else: + opt = defaultload(User.addresses).load_only("id", "email_address") + q = sess.query(User).options(opt).filter(User.id.in_([7, 8])) + def go(): + for user in q: + user.addresses + + self.sql_eq_(go, expected) + + def test_load_only_parent_specific(self): User = self.classes.User Address = self.classes.Address -- 2.47.3