Skip to content

Commit 40b0049

Browse files
committed
include columns from superclasses that indicate "selectin"
Added support for the :paramref:`_orm.Mapper.polymorphic_load` parameter to be applied to each mapper in an inheritance hierarchy more than one level deep, allowing columns to load for all classes in the hierarchy that indicate ``"selectin"`` using a single statement, rather than ignoring elements on those intermediary classes that nonetheless indicate they also would participate in ``"selectin"`` loading and were not part of the base-most SELECT statement. Fixes: #9373 Change-Id: If8dcba0f0191f6c2818ecd15870bccfdf5ce1112
1 parent 8b10829 commit 40b0049

4 files changed

Lines changed: 360 additions & 15 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.. change::
2+
:tags: bug, orm
3+
:tickets: 9373
4+
5+
Added support for the :paramref:`_orm.Mapper.polymorphic_load` parameter to
6+
be applied to each mapper in an inheritance hierarchy more than one level
7+
deep, allowing columns to load for all classes in the hierarchy that
8+
indicate ``"selectin"`` using a single statement, rather than ignoring
9+
elements on those intermediary classes that nonetheless indicate they also
10+
would participate in ``"selectin"`` loading and were not part of the
11+
base-most SELECT statement.

lib/sqlalchemy/orm/loading.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -972,26 +972,31 @@ def _instance_processor(
972972

973973
if not refresh_state and _polymorphic_from is not None:
974974
key = ("loader", path.path)
975+
975976
if key in context.attributes and context.attributes[key].strategy == (
976977
("selectinload_polymorphic", True),
977978
):
978-
selectin_load_via = mapper._should_selectin_load(
979-
context.attributes[key].local_opts["entities"],
980-
_polymorphic_from,
981-
)
979+
option_entities = context.attributes[key].local_opts["entities"]
982980
else:
983-
selectin_load_via = mapper._should_selectin_load(
984-
None, _polymorphic_from
985-
)
981+
option_entities = None
982+
selectin_load_via = mapper._should_selectin_load(
983+
option_entities,
984+
_polymorphic_from,
985+
)
986986

987987
if selectin_load_via and selectin_load_via is not _polymorphic_from:
988988
# only_load_props goes w/ refresh_state only, and in a refresh
989989
# we are a single row query for the exact entity; polymorphic
990990
# loading does not apply
991991
assert only_load_props is None
992992

993-
callable_ = _load_subclass_via_in(context, path, selectin_load_via)
994-
993+
callable_ = _load_subclass_via_in(
994+
context,
995+
path,
996+
selectin_load_via,
997+
_polymorphic_from,
998+
option_entities,
999+
)
9951000
PostLoad.callable_for_path(
9961001
context,
9971002
load_path,
@@ -1212,17 +1217,42 @@ def ensure_no_pk(row):
12121217
return _instance
12131218

12141219

1215-
def _load_subclass_via_in(context, path, entity):
1220+
def _load_subclass_via_in(
1221+
context, path, entity, polymorphic_from, option_entities
1222+
):
12161223
mapper = entity.mapper
12171224

1225+
# TODO: polymorphic_from seems to be a Mapper in all cases.
1226+
# this is likely not needed, but as we dont have typing in loading.py
1227+
# yet, err on the safe side
1228+
polymorphic_from_mapper = polymorphic_from.mapper
1229+
not_against_basemost = polymorphic_from_mapper.inherits is not None
1230+
12181231
zero_idx = len(mapper.base_mapper.primary_key) == 1
12191232

1220-
if entity.is_aliased_class:
1221-
q, enable_opt, disable_opt = mapper._subclass_load_via_in(entity)
1233+
if entity.is_aliased_class or not_against_basemost:
1234+
q, enable_opt, disable_opt = mapper._subclass_load_via_in(
1235+
entity, polymorphic_from
1236+
)
12221237
else:
12231238
q, enable_opt, disable_opt = mapper._subclass_load_via_in_mapper
12241239

12251240
def do_load(context, path, states, load_only, effective_entity):
1241+
if not option_entities:
1242+
# filter out states for those that would have selectinloaded
1243+
# from another loader
1244+
# TODO: we are currently ignoring the case where the
1245+
# "selectin_polymorphic" option is used, as this is much more
1246+
# complex / specific / very uncommon API use
1247+
states = [
1248+
(s, v)
1249+
for s, v in states
1250+
if s.mapper._would_selectin_load_only_from_given_mapper(mapper)
1251+
]
1252+
1253+
if not states:
1254+
return
1255+
12261256
orig_query = context.query
12271257

12281258
options = (enable_opt,) + orig_query._with_options + (disable_opt,)

lib/sqlalchemy/orm/mapper.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3698,6 +3698,65 @@ def _iterate_to_target_viawpoly(self, mapper):
36983698
if m is mapper:
36993699
break
37003700

3701+
@HasMemoized.memoized_attribute
3702+
def _would_selectinload_combinations_cache(self):
3703+
return {}
3704+
3705+
def _would_selectin_load_only_from_given_mapper(self, super_mapper):
3706+
"""return True if this mapper would "selectin" polymorphic load based
3707+
on the given super mapper, and not from a setting from a subclass.
3708+
3709+
given::
3710+
3711+
class A:
3712+
...
3713+
3714+
class B(A):
3715+
__mapper_args__ = {"polymorphic_load": "selectin"}
3716+
3717+
class C(B):
3718+
...
3719+
3720+
class D(B):
3721+
__mapper_args__ = {"polymorphic_load": "selectin"}
3722+
3723+
``inspect(C)._would_selectin_load_only_from_given_mapper(inspect(B))``
3724+
returns True, because C does selectin loading because of B's setting.
3725+
3726+
OTOH, ``inspect(D)
3727+
._would_selectin_load_only_from_given_mapper(inspect(B))``
3728+
returns False, because D does selectin loading because of its own
3729+
setting; when we are doing a selectin poly load from B, we want to
3730+
filter out D because it would already have its own selectin poly load
3731+
set up separately.
3732+
3733+
Added as part of #9373.
3734+
3735+
"""
3736+
cache = self._would_selectinload_combinations_cache
3737+
3738+
try:
3739+
return cache[super_mapper]
3740+
except KeyError:
3741+
pass
3742+
3743+
# assert that given object is a supermapper, meaning we already
3744+
# strong reference it directly or indirectly. this allows us
3745+
# to not worry that we are creating new strongrefs to unrelated
3746+
# mappers or other objects.
3747+
assert self.isa(super_mapper)
3748+
3749+
mapper = super_mapper
3750+
for m in self._iterate_to_target_viawpoly(mapper):
3751+
if m.polymorphic_load == "selectin":
3752+
retval = m is super_mapper
3753+
break
3754+
else:
3755+
retval = False
3756+
3757+
cache[super_mapper] = retval
3758+
return retval
3759+
37013760
def _should_selectin_load(self, enabled_via_opt, polymorphic_from):
37023761
if not enabled_via_opt:
37033762
# common case, takes place for all polymorphic loads
@@ -3721,7 +3780,7 @@ def _should_selectin_load(self, enabled_via_opt, polymorphic_from):
37213780
return None
37223781

37233782
@util.preload_module("sqlalchemy.orm.strategy_options")
3724-
def _subclass_load_via_in(self, entity):
3783+
def _subclass_load_via_in(self, entity, polymorphic_from):
37253784
"""Assemble a that can load the columns local to
37263785
this subclass as a SELECT with IN.
37273786
@@ -3739,6 +3798,16 @@ def _subclass_load_via_in(self, entity):
37393798
disable_opt = strategy_options.Load(entity)
37403799
enable_opt = strategy_options.Load(entity)
37413800

3801+
classes_to_include = {self}
3802+
m: Optional[Mapper[Any]] = self.inherits
3803+
while (
3804+
m is not None
3805+
and m is not polymorphic_from
3806+
and m.polymorphic_load == "selectin"
3807+
):
3808+
classes_to_include.add(m)
3809+
m = m.inherits
3810+
37423811
for prop in self.attrs:
37433812

37443813
# skip prop keys that are not instrumented on the mapped class.
@@ -3747,7 +3816,7 @@ def _subclass_load_via_in(self, entity):
37473816
if prop.key not in self.class_manager:
37483817
continue
37493818

3750-
if prop.parent is self or prop in keep_props:
3819+
if prop.parent in classes_to_include or prop in keep_props:
37513820
# "enable" options, to turn on the properties that we want to
37523821
# load by default (subject to options from the query)
37533822
if not isinstance(prop, StrategizedProperty):
@@ -3811,7 +3880,8 @@ def _subclass_load_via_in(self, entity):
38113880

38123881
@HasMemoized.memoized_attribute
38133882
def _subclass_load_via_in_mapper(self):
3814-
return self._subclass_load_via_in(self)
3883+
# the default is loading this mapper against the basemost mapper
3884+
return self._subclass_load_via_in(self, self.base_mapper)
38153885

38163886
def cascade_iterator(
38173887
self,

0 commit comments

Comments
 (0)