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::
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
---------------------------------------
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
-------------------
# 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 (
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:
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.
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
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
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.
"""
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.
"""
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):
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
"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