]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- made final refinements to the feature and we are 100% go on subquery loading.
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 24 Mar 2010 21:54:52 +0000 (17:54 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 24 Mar 2010 21:54:52 +0000 (17:54 -0400)
- 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.

CHANGES
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/strategies.py
test/orm/test_query.py

diff --git a/CHANGES b/CHANGES
index 799be65fb20003a337466a1bd07488ec8fb8b557..24c28771e809fc32696bdeaab76954fdbed3d40e 100644 (file)
--- 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
 ========
index 5cbdbe80d3d65e5aa8f88ad488d1b87985fe6e00..859333a39b20cdbe3ebc895a273973d89f03e998 100644 (file)
@@ -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
index c5d971313b78edbac1fcc42a0361a0b4d54c41a9..0665bdcb38834f4f29e02f17044eadf55447b2d1 100644 (file)
@@ -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)
 
index 5b8860f523a960999bd4094d1094d3d71196a980..c31c36183e4163deeb0ef1a466b0926f9b4c79a8 100644 (file)
@@ -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()..