========
- orm
+ - Major feature: Added new "subquery" loading capability to
+ relationship(). This is an eager loading option which
+ generates a second SELECT for each collection represented
+ in a query, across all parents at once. The query
+ re-issues the original end-user query wrapped in a subquery,
+ applies joins out to the target collection, and loads
+ all those collections fully in one result, similar to
+ eager loading but using all inner joins and not re-fetching
+ full parent rows repeatedly (as most DBAPIs seem to do,
+ even if columns are skipped). Subquery loading is available
+ at mapper config level using "lazy='subquery'" and at the query
+ options level using "subqueryload(props..)",
+ "subqueryload_all(props...)". [ticket:1675]
+
- Fixed bug in Query whereby calling q.join(prop).from_self(...).
join(prop) would fail to render the second join outside the
subquery, when joining on the same criterion as was on the
- Query.join() will detect if the end result will be
"FROM A JOIN A", and will raise an error if so.
+
+ - Query.join(Cls.propname, from_joinpoint=True) will check more
+ carefully that "Cls" is compatible with the current joinpoint,
+ and act the same way as Query.join("propname", from_joinpoint=True)
+ in that regard.
0.6beta2
========
descriptor, prop = _entity_descriptor(left_entity, onclause)
onclause = descriptor
+
+ # check for q.join(Class.propname, from_joinpoint=True)
+ # and Class is that of the current joinpoint
+ elif from_joinpoint and isinstance(onclause, interfaces.PropComparator):
+ left_entity = onclause.parententity
+
+ left_mapper, left_selectable, left_is_aliased = \
+ _entity_info(self._joinpoint_zero())
+ if left_mapper is left_entity:
+ left_entity = self._joinpoint_zero()
+ descriptor, prop = _entity_descriptor(left_entity, onclause.key)
+ onclause = descriptor
if isinstance(onclause, interfaces.PropComparator):
if right_entity is None:
right_entity = of_type
else:
right_entity = onclause.property.mapper
-
+
left_entity = onclause.parententity
prop = onclause.property
if self.mapper.base_mapper in reduced_path:
return
- orig_query = context.attributes.get(("orig_query", SubqueryLoader), context.query)
+ orig_query = context.attributes.get(
+ ("orig_query", SubqueryLoader),
+ context.query)
# determine attributes of the leftmost mapper
if self.parent.isa(subq_path[0]) and self.key==subq_path[1]:
leftmost_mapper, leftmost_prop = \
- self.parent, self.parent_property
+ self.parent, self.parent_property
else:
leftmost_mapper, leftmost_prop = \
- subq_path[0], subq_path[0].get_property(subq_path[1])
+ subq_path[0], \
+ subq_path[0].get_property(subq_path[1])
leftmost_cols, remote_cols = self._local_remote_columns(leftmost_prop)
leftmost_attr = [
if q._limit is None and q._offset is None:
q._order_by = None
- # new query will join to an aliased entity
- # of the modified original query
+ # the original query now becomes a subquery
+ # which we'll join onto.
embed_q = q.with_labels().subquery()
left_alias = mapperutil.AliasedClass(leftmost_mapper, embed_q)
- # new query, request endpoint columns
- q = q.session.query(self.mapper)
+ # q becomes a new query. basically doing a longhand
+ # "from_self()". (from_self() itself not quite industrial
+ # strength enough for all contingencies...but very close)
- q._attributes = {}
- q._attributes[("orig_query", SubqueryLoader)] = orig_query
- q._attributes[('subquery_path', None)] = subq_path
+ q = q.session.query(self.mapper)
+ q._attributes = {
+ ("orig_query", SubqueryLoader): orig_query,
+ ('subquery_path', None) : subq_path
+ }
# figure out what's being joined. a.k.a. the fun part
to_join = [
(subq_path[i], subq_path[i+1])
for i in xrange(0, len(subq_path), 2)
]
- local_cols, remote_cols = self._local_remote_columns(self.parent_property)
if len(to_join) < 2:
- local_attr = [
- getattr(left_alias, self.parent._get_col_to_prop(c).key)
- for c in local_cols
- ]
+ parent_alias = left_alias
else:
parent_alias = mapperutil.AliasedClass(self.parent)
- local_attr = [
- getattr(parent_alias, self.parent._get_col_to_prop(c).key)
- for c in local_cols
- ]
+
+ local_cols, remote_cols = \
+ self._local_remote_columns(self.parent_property)
+
+ local_attr = [
+ getattr(parent_alias, self.parent._get_col_to_prop(c).key)
+ for c in local_cols
+ ]
q = q.order_by(*local_attr)
q = q.add_columns(*local_attr)
for i, (mapper, key) in enumerate(to_join):
- alias_join = i < len(to_join) - 1
- second_to_last = i == len(to_join) - 2
- # we need to use query.join() here because of the
- # rich behavior it brings when dealing with "with_polymorphic"
- # mappers, otherwise we get broken aliasing and subquerying if
- # using orm.join directly. _joinpoint_zero() is because
- # from_joinpoint doesn't seem to be totally working with self-ref,
- # and/or we should not use aliased=True, instead use AliasedClass()
- # for everything.
- # three TODOs: 1. make orm.join() work with rich polymorphic (huge)
- # 2. make from_joinpoint work completely 3. use AliasedClass() here
+ # we need to use query.join() as opposed to
+ # orm.join() here because of the
+ # rich behavior it brings when dealing with
+ # "with_polymorphic" mappers. "aliased"
+ # and "from_joinpoint" take care of most of
+ # the chaining and aliasing for us.
+
+ first = i == 0
+ middle = i < len(to_join) - 1
+ second_to_last = i == len(to_join) - 2
- if i == 0:
+ if first:
attr = getattr(left_alias, key)
else:
- attr = getattr(q._joinpoint_zero(), key)
+ attr = key
if second_to_last:
- q = q.join((parent_alias, attr))
+ q = q.join((parent_alias, attr), from_joinpoint=True)
else:
- q = q.join(attr, aliased=alias_join)
+ q = q.join(attr, aliased=middle, from_joinpoint=True)
- # propagate loader options etc. to the new query
+ # propagate loader options etc. to the new query.
+ # these will fire relative to subq_path.
q = q._with_current_path(subq_path)
q = q._conditional_options(*orig_query._with_options)
closed_orders = relationship(Order, primaryjoin = and_(orders.c.isopen == 0, users.c.id==orders.c.user_id), lazy=True)
))
q = create_session().query(User)
-
- assert [User(id=7)] == q.join('open_orders', 'items', aliased=True).filter(Item.id==4).join('closed_orders', 'items', aliased=True).filter(Item.id==3).all()
+
+ eq_(
+ q.join('open_orders', 'items', aliased=True).filter(Item.id==4).\
+ join('closed_orders', 'items', aliased=True).filter(Item.id==3).all(),
+ [User(id=7)]
+ )
class SelfReferentialTest(_base.MappedTest, AssertsCompiledSQL):
run_setup_mappers = 'once'
join('parent', aliased=True, from_joinpoint=True).filter_by(data='n1').first()
assert node.data == 'n122'
+ def test_string_or_prop_aliased(self):
+ """test that join('foo') behaves the same as join(Cls.foo) in a self
+ referential scenario.
+
+ """
+
+ sess = create_session()
+ nalias = aliased(Node, sess.query(Node).filter_by(data='n1').subquery())
+
+ q1 = sess.query(nalias).join(nalias.children, aliased=True).\
+ join(Node.children, from_joinpoint=True)
+
+ q2 = sess.query(nalias).join(nalias.children, aliased=True).\
+ join("children", from_joinpoint=True)
+
+ for q in (q1, q2):
+ self.assert_compile(
+ q,
+ "SELECT anon_1.id AS anon_1_id, anon_1.parent_id AS "
+ "anon_1_parent_id, anon_1.data AS anon_1_data FROM "
+ "(SELECT nodes.id AS id, nodes.parent_id AS parent_id, "
+ "nodes.data AS data FROM nodes WHERE nodes.data = :data_1) "
+ "AS anon_1 JOIN nodes AS nodes_1 ON anon_1.id = "
+ "nodes_1.parent_id JOIN nodes ON nodes_1.id = nodes.parent_id",
+ use_default_dialect=True
+ )
+
+ q1 = sess.query(Node).join(nalias.children, aliased=True).\
+ join(Node.children, aliased=True, from_joinpoint=True).\
+ join(Node.children, from_joinpoint=True)
+
+ q2 = sess.query(Node).join(nalias.children, aliased=True).\
+ join("children", aliased=True, from_joinpoint=True).\
+ join("children", from_joinpoint=True)
+
+ for q in (q1, q2):
+ self.assert_compile(
+ q,
+ "SELECT nodes.id AS nodes_id, nodes.parent_id AS "
+ "nodes_parent_id, nodes.data AS nodes_data FROM (SELECT "
+ "nodes.id AS id, nodes.parent_id AS parent_id, nodes.data "
+ "AS data FROM nodes WHERE nodes.data = :data_1) AS anon_1 "
+ "JOIN nodes AS nodes_1 ON anon_1.id = nodes_1.parent_id "
+ "JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id "
+ "JOIN nodes ON nodes_2.id = nodes.parent_id",
+ use_default_dialect=True
+ )
+
def test_from_self_inside_excludes_outside(self):
"""test the propagation of aliased() from inside to outside
on a from_self()..