From: Mike Bayer Date: Mon, 24 Nov 2014 23:49:32 +0000 (-0500) Subject: - The :meth:`.PropComparator.of_type` modifier has been X-Git-Tag: rel_1_0_0~19^2~17 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=de11f9498258182cbb6668b72067ec3f43a90415;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - The :meth:`.PropComparator.of_type` modifier has been improved in conjunction with loader directives such as :func:`.joinedload` and :func:`.contains_eager` such that if two :meth:`.PropComparator.of_type` modifiers of the same base type/path are encountered, they will be joined together into a single "polymorphic" entity, rather than replacing the entity of type A with the one of type B. E.g. a joinedload of ``A.b.of_type(BSub1)->BSub1.c`` combined with joinedload of ``A.b.of_type(BSub2)->BSub2.c`` will create a single joinedload of ``A.b.of_type((BSub1, BSub2)) -> BSub1.c, BSub2.c``, without the need for the ``with_polymorphic`` to be explicit in the query. fixes #3256 --- diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst index 4e5e1ba1d9..c0197a6917 100644 --- a/doc/build/changelog/changelog_10.rst +++ b/doc/build/changelog/changelog_10.rst @@ -21,6 +21,28 @@ series as well. For changes that are specific to 1.0 with an emphasis on compatibility concerns, see :doc:`/changelog/migration_10`. + .. change:: + :tags: bug, orm + :tickets: 3256 + + The :meth:`.PropComparator.of_type` modifier has been + improved in conjunction with loader directives such as + :func:`.joinedload` and :func:`.contains_eager` such that if + two :meth:`.PropComparator.of_type` modifiers of the same + base type/path are encountered, they will be joined together + into a single "polymorphic" entity, rather than replacing + the entity of type A with the one of type B. E.g. + a joinedload of ``A.b.of_type(BSub1)->BSub1.c`` combined with + joinedload of ``A.b.of_type(BSub2)->BSub2.c`` will create a + single joinedload of ``A.b.of_type((BSub1, BSub2)) -> BSub1.c, BSub2.c``, + without the need for the ``with_polymorphic`` to be explicit + in the query. + + .. seealso:: + + :ref:`eagerloading_polymorphic_subtypes` - contains an updated + example illustrating the new format. + .. change:: :tags: bug, sql :tickets: 3245 diff --git a/doc/build/orm/inheritance.rst b/doc/build/orm/inheritance.rst index 9f01a3e24f..0713634bc4 100644 --- a/doc/build/orm/inheritance.rst +++ b/doc/build/orm/inheritance.rst @@ -475,6 +475,8 @@ subselect back to the parent ``companies`` table. :func:`.orm.aliased` and :func:`.orm.with_polymorphic` constructs in conjunction with :meth:`.Query.join`, ``any()`` and ``has()``. +.. _eagerloading_polymorphic_subtypes: + Eager Loading of Specific or Polymorphic Subtypes ++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -491,7 +493,7 @@ objects, querying the ``employee`` and ``engineer`` tables simultaneously:: ) ) -As is the case with :meth:`.Query.join`, :func:`~sqlalchemy.orm.interfaces.PropComparator.of_type` +As is the case with :meth:`.Query.join`, :meth:`~PropComparator.of_type` also can be used with eager loading and :func:`.orm.with_polymorphic` at the same time, so that all sub-attributes of all referenced subtypes can be loaded:: @@ -513,6 +515,23 @@ can be loaded:: :func:`~sqlalchemy.orm.interfaces.PropComparator.of_type`, supporting single target types as well as :func:`.orm.with_polymorphic` targets. +Another option for the above query is to state the two subtypes separately; +the :func:`.joinedload` directive should detect this and create the +above ``with_polymorphic`` construct automatically:: + + session.query(Company).\ + options( + joinedload(Company.employees.of_type(Manager)), + joinedload(Company.employees.of_type(Engineer)), + ) + ) + +.. versionadded:: 1.0 + Eager loaders such as :func:`.joinedload` will create a polymorphic + entity when multiple overlapping :meth:`~PropComparator.of_type` + directives are encountered. + + Single Table Inheritance ------------------------ diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index a4107202eb..276da2ae0f 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -161,11 +161,14 @@ class Load(Generative, MapperOption): ext_info = inspect(ac) path_element = ext_info.mapper + existing = path.entity_path[prop].get( + self.context, "path_with_polymorphic") if not ext_info.is_aliased_class: ac = orm_util.with_polymorphic( ext_info.mapper.base_mapper, ext_info.mapper, aliased=True, - _use_mapper_path=True) + _use_mapper_path=True, + _existing_alias=existing) path.entity_path[prop].set( self.context, "path_with_polymorphic", inspect(ac)) path = path[prop][path_element] diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index ad610a4ac9..4be8d19ff3 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -543,8 +543,13 @@ class AliasedInsp(InspectionAttr): mapper, self) def __repr__(self): - return '' % ( - id(self), self.class_.__name__) + if self.with_polymorphic_mappers: + with_poly = "(%s)" % ", ".join( + mp.class_.__name__ for mp in self.with_polymorphic_mappers) + else: + with_poly = "" + return '' % ( + id(self), self.class_.__name__, with_poly) inspection._inspects(AliasedClass)(lambda target: target._aliased_insp) @@ -648,7 +653,8 @@ def aliased(element, alias=None, name=None, flat=False, adapt_on_names=False): def with_polymorphic(base, classes, selectable=False, flat=False, polymorphic_on=None, aliased=False, - innerjoin=False, _use_mapper_path=False): + innerjoin=False, _use_mapper_path=False, + _existing_alias=None): """Produce an :class:`.AliasedClass` construct which specifies columns for descendant mappers of the given base. @@ -713,6 +719,16 @@ def with_polymorphic(base, classes, selectable=False, only be specified if querying for one specific subtype only """ primary_mapper = _class_to_mapper(base) + if _existing_alias: + assert _existing_alias.mapper is primary_mapper + classes = util.to_set(classes) + new_classes = set([ + mp.class_ for mp in + _existing_alias.with_polymorphic_mappers]) + if classes == new_classes: + return _existing_alias + else: + classes = classes.union(new_classes) mappers, selectable = primary_mapper.\ _with_polymorphic_args(classes, selectable, innerjoin=innerjoin) diff --git a/lib/sqlalchemy/util/_collections.py b/lib/sqlalchemy/util/_collections.py index a1fbc0fa09..d368526980 100644 --- a/lib/sqlalchemy/util/_collections.py +++ b/lib/sqlalchemy/util/_collections.py @@ -10,9 +10,10 @@ from __future__ import absolute_import import weakref import operator -from .compat import threading, itertools_filterfalse +from .compat import threading, itertools_filterfalse, string_types from . import py2k import types +import collections EMPTY_SET = frozenset() @@ -779,10 +780,12 @@ def coerce_generator_arg(arg): def to_list(x, default=None): if x is None: return default - if not isinstance(x, (list, tuple)): + if not isinstance(x, collections.Iterable) or isinstance(x, string_types): return [x] - else: + elif isinstance(x, list): return x + else: + return list(x) def to_set(x): diff --git a/test/base/test_utils.py b/test/base/test_utils.py index f75c5cbe9c..df61d78747 100644 --- a/test/base/test_utils.py +++ b/test/base/test_utils.py @@ -8,6 +8,7 @@ from sqlalchemy.util import classproperty, WeakSequence, get_callable_argspec from sqlalchemy.sql import column from sqlalchemy.util import langhelpers + class _KeyedTupleTest(object): def _fixture(self, values, labels): @@ -283,6 +284,35 @@ class MemoizedAttrTest(fixtures.TestBase): eq_(val[0], 21) +class ToListTest(fixtures.TestBase): + def test_from_string(self): + eq_( + util.to_list("xyz"), + ["xyz"] + ) + + def test_from_set(self): + spec = util.to_list(set([1, 2, 3])) + assert isinstance(spec, list) + eq_( + sorted(spec), + [1, 2, 3] + ) + + def test_from_dict(self): + spec = util.to_list({1: "a", 2: "b", 3: "c"}) + assert isinstance(spec, list) + eq_( + sorted(spec), + [1, 2, 3] + ) + + def test_from_tuple(self): + eq_( + util.to_list((1, 2, 3)), + [1, 2, 3] + ) + class ColumnCollectionTest(fixtures.TestBase): def test_in(self): diff --git a/test/orm/test_of_type.py b/test/orm/test_of_type.py index 836d85cc73..b9ebc2daf7 100644 --- a/test/orm/test_of_type.py +++ b/test/orm/test_of_type.py @@ -14,6 +14,7 @@ from .inheritance._poly_fixtures import Company, Person, Engineer, Manager, Boss _PolymorphicPolymorphic, _PolymorphicUnions, _PolymorphicJoins,\ _PolymorphicAliasedJoins + class _PolymorphicTestBase(object): __dialect__ = 'default' @@ -191,6 +192,21 @@ class _PolymorphicTestBase(object): ) self.assert_sql_count(testing.db, go, 3) + def test_joinedload_stacked_of_type(self): + sess = Session() + + def go(): + eq_( + sess.query(Company). + filter_by(company_id=1). + options( + joinedload(Company.employees.of_type(Manager)), + joinedload(Company.employees.of_type(Engineer)) + ).all(), + [self._company_with_emps_fixture()[0]] + ) + self.assert_sql_count(testing.db, go, 2) + class PolymorphicPolymorphicTest(_PolymorphicTestBase, _PolymorphicPolymorphic): def _polymorphic_join_target(self, cls):