]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The :meth:`.PropComparator.of_type` modifier has been
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 24 Nov 2014 23:49:32 +0000 (18:49 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 24 Nov 2014 23:49:32 +0000 (18:49 -0500)
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

doc/build/changelog/changelog_10.rst
doc/build/orm/inheritance.rst
lib/sqlalchemy/orm/strategy_options.py
lib/sqlalchemy/orm/util.py
lib/sqlalchemy/util/_collections.py
test/base/test_utils.py
test/orm/test_of_type.py

index 4e5e1ba1d94b448a368e9864b94dbc9acb55db6c..c0197a691745aed6d87bf4ac63a2100b05473001 100644 (file)
     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
index 9f01a3e24f66380dd12c0cbab5a17a4063c3a6d7..0713634bc468591f18387f78b65a0974c5c1e029 100644 (file)
@@ -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
 ------------------------
index a4107202eb88eefa897aa29b3ecbaef41180fd2b..276da2ae0f6f61955d635645e042a2cdb4c37fea 100644 (file)
@@ -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]
index ad610a4ac9b26812d8441da0ca321106efadf0f5..4be8d19ff3f9b3df3f426432c51387ada817d88c 100644 (file)
@@ -543,8 +543,13 @@ class AliasedInsp(InspectionAttr):
                 mapper, self)
 
     def __repr__(self):
-        return '<AliasedInsp at 0x%x; %s>' % (
-            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 '<AliasedInsp at 0x%x; %s%s>' % (
+            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)
index a1fbc0fa09bb215a38dfb909762e817f93bfd979..d368526980285ee2da75d41dad6f361590b1689f 100644 (file)
 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):
index f75c5cbe9cfdec52b72d61c97425d352982aa511..df61d787473e2789af72cdb67cbcd773b704a425 100644 (file)
@@ -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):
index 836d85cc73ee68fe90d30dec39fb512d115df2c6..b9ebc2daf7969d96465e86e8cb39fa3f2cf36048 100644 (file)
@@ -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):