From: Mike Bayer Date: Wed, 24 Mar 2010 21:54:52 +0000 (-0400) Subject: - made final refinements to the feature and we are 100% go on subquery loading. X-Git-Tag: rel_0_6beta3~12^2~19 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=290a1596ce6f806aa6f25dd754cf0d2197f160ff;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - made final refinements to the feature and we are 100% go on subquery loading. - 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. --- diff --git a/CHANGES b/CHANGES index 799be65fb2..24c28771e8 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,20 @@ CHANGES ======== - 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 @@ -30,6 +44,11 @@ CHANGES - 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 ======== diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 5cbdbe80d3..859333a39b 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -1030,6 +1030,18 @@ class Query(object): 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: @@ -1039,7 +1051,7 @@ class Query(object): right_entity = of_type else: right_entity = onclause.property.mapper - + left_entity = onclause.parententity prop = onclause.property diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index c5d971313b..0665bdcb38 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -667,15 +667,18 @@ class SubqueryLoader(AbstractRelationshipLoader): 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 = [ @@ -695,64 +698,67 @@ class SubqueryLoader(AbstractRelationshipLoader): 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) diff --git a/test/orm/test_query.py b/test/orm/test_query.py index 5b8860f523..c31c36183e 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -3347,8 +3347,12 @@ class CustomJoinTest(QueryTest): 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' @@ -3405,6 +3409,54 @@ class SelfReferentialTest(_base.MappedTest, AssertsCompiledSQL): 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()..