summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/orm/loading.py54
-rw-r--r--lib/sqlalchemy/orm/mapper.py76
2 files changed, 115 insertions, 15 deletions
diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py
index 54b96c215..7974d94c5 100644
--- a/lib/sqlalchemy/orm/loading.py
+++ b/lib/sqlalchemy/orm/loading.py
@@ -972,17 +972,17 @@ def _instance_processor(
if not refresh_state and _polymorphic_from is not None:
key = ("loader", path.path)
+
if key in context.attributes and context.attributes[key].strategy == (
("selectinload_polymorphic", True),
):
- selectin_load_via = mapper._should_selectin_load(
- context.attributes[key].local_opts["entities"],
- _polymorphic_from,
- )
+ option_entities = context.attributes[key].local_opts["entities"]
else:
- selectin_load_via = mapper._should_selectin_load(
- None, _polymorphic_from
- )
+ option_entities = None
+ selectin_load_via = mapper._should_selectin_load(
+ option_entities,
+ _polymorphic_from,
+ )
if selectin_load_via and selectin_load_via is not _polymorphic_from:
# only_load_props goes w/ refresh_state only, and in a refresh
@@ -990,8 +990,13 @@ def _instance_processor(
# loading does not apply
assert only_load_props is None
- callable_ = _load_subclass_via_in(context, path, selectin_load_via)
-
+ callable_ = _load_subclass_via_in(
+ context,
+ path,
+ selectin_load_via,
+ _polymorphic_from,
+ option_entities,
+ )
PostLoad.callable_for_path(
context,
load_path,
@@ -1212,17 +1217,42 @@ def _instance_processor(
return _instance
-def _load_subclass_via_in(context, path, entity):
+def _load_subclass_via_in(
+ context, path, entity, polymorphic_from, option_entities
+):
mapper = entity.mapper
+ # TODO: polymorphic_from seems to be a Mapper in all cases.
+ # this is likely not needed, but as we dont have typing in loading.py
+ # yet, err on the safe side
+ polymorphic_from_mapper = polymorphic_from.mapper
+ not_against_basemost = polymorphic_from_mapper.inherits is not None
+
zero_idx = len(mapper.base_mapper.primary_key) == 1
- if entity.is_aliased_class:
- q, enable_opt, disable_opt = mapper._subclass_load_via_in(entity)
+ if entity.is_aliased_class or not_against_basemost:
+ q, enable_opt, disable_opt = mapper._subclass_load_via_in(
+ entity, polymorphic_from
+ )
else:
q, enable_opt, disable_opt = mapper._subclass_load_via_in_mapper
def do_load(context, path, states, load_only, effective_entity):
+ if not option_entities:
+ # filter out states for those that would have selectinloaded
+ # from another loader
+ # TODO: we are currently ignoring the case where the
+ # "selectin_polymorphic" option is used, as this is much more
+ # complex / specific / very uncommon API use
+ states = [
+ (s, v)
+ for s, v in states
+ if s.mapper._would_selectin_load_only_from_given_mapper(mapper)
+ ]
+
+ if not states:
+ return
+
orig_query = context.query
options = (enable_opt,) + orig_query._with_options + (disable_opt,)
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index c0ff2ed10..2ae6dadcd 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -3698,6 +3698,65 @@ class Mapper(
if m is mapper:
break
+ @HasMemoized.memoized_attribute
+ def _would_selectinload_combinations_cache(self):
+ return {}
+
+ def _would_selectin_load_only_from_given_mapper(self, super_mapper):
+ """return True if this mapper would "selectin" polymorphic load based
+ on the given super mapper, and not from a setting from a subclass.
+
+ given::
+
+ class A:
+ ...
+
+ class B(A):
+ __mapper_args__ = {"polymorphic_load": "selectin"}
+
+ class C(B):
+ ...
+
+ class D(B):
+ __mapper_args__ = {"polymorphic_load": "selectin"}
+
+ ``inspect(C)._would_selectin_load_only_from_given_mapper(inspect(B))``
+ returns True, because C does selectin loading because of B's setting.
+
+ OTOH, ``inspect(D)
+ ._would_selectin_load_only_from_given_mapper(inspect(B))``
+ returns False, because D does selectin loading because of its own
+ setting; when we are doing a selectin poly load from B, we want to
+ filter out D because it would already have its own selectin poly load
+ set up separately.
+
+ Added as part of #9373.
+
+ """
+ cache = self._would_selectinload_combinations_cache
+
+ try:
+ return cache[super_mapper]
+ except KeyError:
+ pass
+
+ # assert that given object is a supermapper, meaning we already
+ # strong reference it directly or indirectly. this allows us
+ # to not worry that we are creating new strongrefs to unrelated
+ # mappers or other objects.
+ assert self.isa(super_mapper)
+
+ mapper = super_mapper
+ for m in self._iterate_to_target_viawpoly(mapper):
+ if m.polymorphic_load == "selectin":
+ retval = m is super_mapper
+ break
+ else:
+ retval = False
+
+ cache[super_mapper] = retval
+ return retval
+
def _should_selectin_load(self, enabled_via_opt, polymorphic_from):
if not enabled_via_opt:
# common case, takes place for all polymorphic loads
@@ -3721,7 +3780,7 @@ class Mapper(
return None
@util.preload_module("sqlalchemy.orm.strategy_options")
- def _subclass_load_via_in(self, entity):
+ def _subclass_load_via_in(self, entity, polymorphic_from):
"""Assemble a that can load the columns local to
this subclass as a SELECT with IN.
@@ -3739,6 +3798,16 @@ class Mapper(
disable_opt = strategy_options.Load(entity)
enable_opt = strategy_options.Load(entity)
+ classes_to_include = {self}
+ m: Optional[Mapper[Any]] = self.inherits
+ while (
+ m is not None
+ and m is not polymorphic_from
+ and m.polymorphic_load == "selectin"
+ ):
+ classes_to_include.add(m)
+ m = m.inherits
+
for prop in self.attrs:
# skip prop keys that are not instrumented on the mapped class.
@@ -3747,7 +3816,7 @@ class Mapper(
if prop.key not in self.class_manager:
continue
- if prop.parent is self or prop in keep_props:
+ if prop.parent in classes_to_include or prop in keep_props:
# "enable" options, to turn on the properties that we want to
# load by default (subject to options from the query)
if not isinstance(prop, StrategizedProperty):
@@ -3811,7 +3880,8 @@ class Mapper(
@HasMemoized.memoized_attribute
def _subclass_load_via_in_mapper(self):
- return self._subclass_load_via_in(self)
+ # the default is loading this mapper against the basemost mapper
+ return self._subclass_load_via_in(self, self.base_mapper)
def cascade_iterator(
self,