]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Allow aliased() to be passed to Query.select_entity_from().
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 27 Mar 2017 17:48:40 +0000 (13:48 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 27 Mar 2017 18:39:51 +0000 (14:39 -0400)
An :func:`.aliased()` construct can now be passed to the
:meth:`.Query.select_entity_from` method.   Entities will be pulled
from the selectable represented by the :func:`.aliased` construct.
This allows special options for :func:`.aliased` such as
:paramref:`.aliased.adapt_on_names` to be used in conjunction with
:meth:`.Query.select_entity_from`.

Additionally rewrote the docstring for :meth:`.Query.select_entity_from`,
including starting with explicit use of :func:`.aliased` as the
usual idiomatic pattern.  An example using text().columns() is added
as well as the use case from :ticket:`3933` using name matching.

Change-Id: If7e182965236993064a2a086e3b6d55a4f097ca8
Fixes: #3933
doc/build/changelog/changelog_11.rst
lib/sqlalchemy/orm/query.py
test/orm/test_froms.py

index 69bdd32742cbb6b6fcaf768ff1fe174626b2baf4..82fb1d0519b0d693aa98bbc8162ef9a9d9e516d2 100644 (file)
 .. changelog::
     :version: 1.1.7
 
+    .. change::
+        :tags: feature, orm
+        :tickets: 3933
+        :versions: 1.2.0b1
+
+        An :func:`.aliased()` construct can now be passed to the
+        :meth:`.Query.select_entity_from` method.   Entities will be pulled
+        from the selectable represented by the :func:`.aliased` construct.
+        This allows special options for :func:`.aliased` such as
+        :paramref:`.aliased.adapt_on_names` to be used in conjunction with
+        :meth:`.Query.select_entity_from`.
+
     .. change::
         :tags: bug, engine
         :tickets: 3946
index f6459de2c0d00218bed861cdb1802f5a596ea046..bbfa393f8c0fbcca749274021877498aacfddbf1 100644 (file)
@@ -194,7 +194,7 @@ class Query(object):
             if hasattr(info, 'mapper') and \
                     (info.is_mapper or info.is_aliased_class):
                 self._select_from_entity = info
-                if set_base_alias:
+                if set_base_alias and not info.is_aliased_class:
                     raise sa_exc.ArgumentError(
                         "A selectable (FromClause) instance is "
                         "expected when the base alias is being set.")
@@ -218,6 +218,11 @@ class Query(object):
             equivs = self.__all_equivs()
             self._from_obj_alias = sql_util.ColumnAdapter(
                 self._from_obj[0], equivs)
+        elif set_base_alias and \
+                len(self._from_obj) == 1 and \
+                hasattr(info, "mapper") and \
+                info.is_aliased_class:
+            self._from_obj_alias = info._adapter
 
     def _reset_polymorphic_adapter(self, mapper):
         for m2 in mapper._with_polymorphic_mappers:
@@ -2422,29 +2427,35 @@ class Query(object):
         core selectable, applying it as a replacement FROM clause
         for corresponding mapped entities.
 
-        This method is similar to the :meth:`.Query.select_from`
-        method, in that it sets the FROM clause of the query.  However,
-        where :meth:`.Query.select_from` only affects what is placed
-        in the FROM, this method also applies the given selectable
-        to replace the FROM which the selected entities would normally
-        select from.
-
-        The given ``from_obj`` must be an instance of a :class:`.FromClause`,
-        e.g. a :func:`.select` or :class:`.Alias` construct.
+        The :meth:`.Query.select_entity_from` method supplies an alternative
+        approach to the use case of applying an :func:`.aliased` construct
+        explicitly throughout a query.  Instead of referring to the
+        :func:`.aliased` construct explicitly,
+        :meth:`.Query.select_entity_from` automatically *adapts* all occurences
+        of the entity to the target selectable.
 
-        An example would be a :class:`.Query` that selects ``User`` entities,
-        but uses :meth:`.Query.select_entity_from` to have the entities
-        selected from a :func:`.select` construct instead of the
-        base ``user`` table::
+        Given a case for :func:`.aliased` such as selecting ``User``
+        objects from a SELECT statement::
 
             select_stmt = select([User]).where(User.id == 7)
+            user_alias = aliased(User, select_stmt)
+
+            q = session.query(user_alias).\
+                filter(user_alias.name == 'ed')
+
+        Above, we apply the ``user_alias`` object explicitly throughout the
+        query.  When it's not feasible for ``user_alias`` to be referenced
+        explicitly in many places, :meth:`.Query.select_entity_from` may be
+        used at the start of the query to adapt the existing ``User`` entity::
 
             q = session.query(User).\
-                    select_entity_from(select_stmt).\
-                    filter(User.name == 'ed')
+                select_entity_from(select_stmt).\
+                filter(User.name == 'ed')
+
+        Above, the generated SQL will show that the ``User`` entity is
+        adapted to our statement, even in the case of the WHERE clause:
 
-        The query generated will select ``User`` entities directly
-        from the given :func:`.select` construct, and will be::
+        .. sourcecode:: sql
 
             SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name
             FROM (SELECT "user".id AS id, "user".name AS name
@@ -2452,51 +2463,64 @@ class Query(object):
             WHERE "user".id = :id_1) AS anon_1
             WHERE anon_1.name = :name_1
 
-        Notice above that even the WHERE criterion was "adapted" such that
-        the ``anon_1`` subquery effectively replaces all references to the
-        ``user`` table, except for the one that it refers to internally.
+        The :meth:`.Query.select_entity_from` method is similar to the
+        :meth:`.Query.select_from` method, in that it sets the FROM clause
+        of the query.  The difference is that it additionally applies
+        adaptation to the other parts of the query that refer to the
+        primary entity.  If above we had used :meth:`.Query.select_from`
+        instead, the SQL generated would have been:
 
-        Compare this to :meth:`.Query.select_from`, which as of
-        version 0.9, does not affect existing entities.  The
-        statement below::
-
-            q = session.query(User).\
-                    select_from(select_stmt).\
-                    filter(User.name == 'ed')
-
-        Produces SQL where both the ``user`` table as well as the
-        ``select_stmt`` construct are present as separate elements
-        in the FROM clause.  No "adaptation" of the ``user`` table
-        is applied::
+        .. sourcecode:: sql
 
+            -- uses plain select_from(), not select_entity_from()
             SELECT "user".id AS user_id, "user".name AS user_name
             FROM "user", (SELECT "user".id AS id, "user".name AS name
             FROM "user"
             WHERE "user".id = :id_1) AS anon_1
             WHERE "user".name = :name_1
 
-        :meth:`.Query.select_entity_from` maintains an older
-        behavior of :meth:`.Query.select_from`.  In modern usage,
-        similar results can also be achieved using :func:`.aliased`::
-
-            select_stmt = select([User]).where(User.id == 7)
-            user_from_select = aliased(User, select_stmt.alias())
+        To supply textual SQL to the :meth:`.Query.select_entity_from` method,
+        we can make use of the :func:`.text` construct.  However, the
+        :func:`.text` construct needs to be aligned with the columns of our
+        entity, which is achieved by making use of the
+        :meth:`.TextClause.columns` method::
+
+            text_stmt = text("select id, name from user").columns(
+                User.id, User.name)
+            q = session.query(User).select_entity_from(text_stmt)
+
+        :meth:`.Query.select_entity_from` itself accepts an :func:`.aliased`
+        object, so that the special options of :func:`.aliased` such as
+        :paramref:`.aliased.adapt_on_names` may be used within the
+        scope of the :meth:`.Query.select_entity_from` method's adaptation
+        services.  Suppose
+        a view ``user_view`` also returns rows from ``user``.    If
+        we reflect this view into a :class:`.Table`, this view has no
+        relationship to the :class:`.Table` to which we are mapped, however
+        we can use name matching to select from it::
+
+            user_view = Table('user_view', metadata,
+                              autoload_with=engine)
+            user_view_alias = aliased(
+                User, user_view, adapt_on_names=True)
+            q = session.query(User).\
+                select_entity_from(user_view_alias).\
+                order_by(User.name)
 
-            q = session.query(user_from_select)
+        .. versionchanged:: 1.1.7 The :meth:`.Query.select_entity_from`
+           method now accepts an :func:`.aliased` object as an alternative
+           to a :class:`.FromClause` object.
 
         :param from_obj: a :class:`.FromClause` object that will replace
-         the FROM clause of this :class:`.Query`.
+         the FROM clause of this :class:`.Query`.  It also may be an instance
+         of :func:`.aliased`.
+
+
 
         .. seealso::
 
             :meth:`.Query.select_from`
 
-        .. versionadded:: 0.8
-            :meth:`.Query.select_entity_from` was added to specify
-            the specific behavior of entity replacement, however
-            the :meth:`.Query.select_from` maintains this behavior
-            as well until 0.9.
-
         """
 
         self._set_select_from([from_obj], True)
index 44d87822e0e9b33725fa6471b875a67176532b23..9bfa8e8eb75550d3c382bf0fae4b238de7f2e1c4 100644 (file)
@@ -1852,6 +1852,31 @@ class SelectFromTest(QueryTest, AssertsCompiledSQL):
             options(joinedload('addresses')).first(),
             User(name='jack', addresses=[Address(id=1)]))
 
+    def test_select_from_aliased(self):
+        User, users = self.classes.User, self.tables.users
+
+        mapper(User, users)
+
+        sess = create_session()
+
+        not_users = table('users', column('id'), column('name'))
+        ua = aliased(
+            User,
+            select([not_users]).alias(),
+            adapt_on_names=True
+        )
+
+        q = sess.query(User.name).select_entity_from(ua).order_by(User.name)
+        self.assert_compile(
+            q,
+            "SELECT anon_1.name AS anon_1_name FROM (SELECT users.id AS id, "
+            "users.name AS name FROM users) AS anon_1 ORDER BY anon_1.name"
+        )
+        eq_(
+            q.all(),
+            [('chuck',), ('ed',), ('fred',), ('jack',)]
+        )
+
     @testing.uses_deprecated("Mapper.order_by")
     def test_join_mapper_order_by(self):
         """test that mapper-level order_by is adapted to a selectable."""