From: Mike Bayer Date: Mon, 27 Mar 2017 17:48:40 +0000 (-0400) Subject: Allow aliased() to be passed to Query.select_entity_from(). X-Git-Tag: rel_1_1_7~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6bdfeb73f2ebb8cf4817550e749c2f7103961d0e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Allow aliased() to be passed to Query.select_entity_from(). 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 (cherry picked from commit b5577b6fb3decda0293399a630e6601e86e08726) --- diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst index 69bdd32742..82fb1d0519 100644 --- a/doc/build/changelog/changelog_11.rst +++ b/doc/build/changelog/changelog_11.rst @@ -21,6 +21,18 @@ .. 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 diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 2fae36272b..e8bd717723 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -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) diff --git a/test/orm/test_froms.py b/test/orm/test_froms.py index 44d87822e0..9bfa8e8eb7 100644 --- a/test/orm/test_froms.py +++ b/test/orm/test_froms.py @@ -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."""