You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3113 line
104KB

  1. # orm/strategies.py
  2. # Copyright (C) 2005-2021 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  7. """sqlalchemy.orm.interfaces.LoaderStrategy
  8. implementations, and related MapperOptions."""
  9. from __future__ import absolute_import
  10. import collections
  11. import itertools
  12. from . import attributes
  13. from . import exc as orm_exc
  14. from . import interfaces
  15. from . import loading
  16. from . import path_registry
  17. from . import properties
  18. from . import query
  19. from . import relationships
  20. from . import unitofwork
  21. from . import util as orm_util
  22. from .base import _DEFER_FOR_STATE
  23. from .base import _RAISE_FOR_STATE
  24. from .base import _SET_DEFERRED_EXPIRED
  25. from .context import _column_descriptions
  26. from .context import ORMCompileState
  27. from .context import QueryContext
  28. from .interfaces import LoaderStrategy
  29. from .interfaces import StrategizedProperty
  30. from .session import _state_session
  31. from .state import InstanceState
  32. from .util import _none_set
  33. from .util import aliased
  34. from .. import event
  35. from .. import exc as sa_exc
  36. from .. import inspect
  37. from .. import log
  38. from .. import sql
  39. from .. import util
  40. from ..sql import util as sql_util
  41. from ..sql import visitors
  42. from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
  43. def _register_attribute(
  44. prop,
  45. mapper,
  46. useobject,
  47. compare_function=None,
  48. typecallable=None,
  49. callable_=None,
  50. proxy_property=None,
  51. active_history=False,
  52. impl_class=None,
  53. **kw
  54. ):
  55. listen_hooks = []
  56. uselist = useobject and prop.uselist
  57. if useobject and prop.single_parent:
  58. listen_hooks.append(single_parent_validator)
  59. if prop.key in prop.parent.validators:
  60. fn, opts = prop.parent.validators[prop.key]
  61. listen_hooks.append(
  62. lambda desc, prop: orm_util._validator_events(
  63. desc, prop.key, fn, **opts
  64. )
  65. )
  66. if useobject:
  67. listen_hooks.append(unitofwork.track_cascade_events)
  68. # need to assemble backref listeners
  69. # after the singleparentvalidator, mapper validator
  70. if useobject:
  71. backref = prop.back_populates
  72. if backref and prop._effective_sync_backref:
  73. listen_hooks.append(
  74. lambda desc, prop: attributes.backref_listeners(
  75. desc, backref, uselist
  76. )
  77. )
  78. # a single MapperProperty is shared down a class inheritance
  79. # hierarchy, so we set up attribute instrumentation and backref event
  80. # for each mapper down the hierarchy.
  81. # typically, "mapper" is the same as prop.parent, due to the way
  82. # the configure_mappers() process runs, however this is not strongly
  83. # enforced, and in the case of a second configure_mappers() run the
  84. # mapper here might not be prop.parent; also, a subclass mapper may
  85. # be called here before a superclass mapper. That is, can't depend
  86. # on mappers not already being set up so we have to check each one.
  87. for m in mapper.self_and_descendants:
  88. if prop is m._props.get(
  89. prop.key
  90. ) and not m.class_manager._attr_has_impl(prop.key):
  91. desc = attributes.register_attribute_impl(
  92. m.class_,
  93. prop.key,
  94. parent_token=prop,
  95. uselist=uselist,
  96. compare_function=compare_function,
  97. useobject=useobject,
  98. trackparent=useobject
  99. and (
  100. prop.single_parent
  101. or prop.direction is interfaces.ONETOMANY
  102. ),
  103. typecallable=typecallable,
  104. callable_=callable_,
  105. active_history=active_history,
  106. impl_class=impl_class,
  107. send_modified_events=not useobject or not prop.viewonly,
  108. doc=prop.doc,
  109. **kw
  110. )
  111. for hook in listen_hooks:
  112. hook(desc, prop)
  113. @properties.ColumnProperty.strategy_for(instrument=False, deferred=False)
  114. class UninstrumentedColumnLoader(LoaderStrategy):
  115. """Represent a non-instrumented MapperProperty.
  116. The polymorphic_on argument of mapper() often results in this,
  117. if the argument is against the with_polymorphic selectable.
  118. """
  119. __slots__ = ("columns",)
  120. def __init__(self, parent, strategy_key):
  121. super(UninstrumentedColumnLoader, self).__init__(parent, strategy_key)
  122. self.columns = self.parent_property.columns
  123. def setup_query(
  124. self,
  125. compile_state,
  126. query_entity,
  127. path,
  128. loadopt,
  129. adapter,
  130. column_collection=None,
  131. **kwargs
  132. ):
  133. for c in self.columns:
  134. if adapter:
  135. c = adapter.columns[c]
  136. column_collection.append(c)
  137. def create_row_processor(
  138. self,
  139. context,
  140. query_entity,
  141. path,
  142. loadopt,
  143. mapper,
  144. result,
  145. adapter,
  146. populators,
  147. ):
  148. pass
  149. @log.class_logger
  150. @properties.ColumnProperty.strategy_for(instrument=True, deferred=False)
  151. class ColumnLoader(LoaderStrategy):
  152. """Provide loading behavior for a :class:`.ColumnProperty`."""
  153. __slots__ = "columns", "is_composite"
  154. def __init__(self, parent, strategy_key):
  155. super(ColumnLoader, self).__init__(parent, strategy_key)
  156. self.columns = self.parent_property.columns
  157. self.is_composite = hasattr(self.parent_property, "composite_class")
  158. def setup_query(
  159. self,
  160. compile_state,
  161. query_entity,
  162. path,
  163. loadopt,
  164. adapter,
  165. column_collection,
  166. memoized_populators,
  167. check_for_adapt=False,
  168. **kwargs
  169. ):
  170. for c in self.columns:
  171. if adapter:
  172. if check_for_adapt:
  173. c = adapter.adapt_check_present(c)
  174. if c is None:
  175. return
  176. else:
  177. c = adapter.columns[c]
  178. column_collection.append(c)
  179. fetch = self.columns[0]
  180. if adapter:
  181. fetch = adapter.columns[fetch]
  182. memoized_populators[self.parent_property] = fetch
  183. def init_class_attribute(self, mapper):
  184. self.is_class_level = True
  185. coltype = self.columns[0].type
  186. # TODO: check all columns ? check for foreign key as well?
  187. active_history = (
  188. self.parent_property.active_history
  189. or self.columns[0].primary_key
  190. or (
  191. mapper.version_id_col is not None
  192. and mapper._columntoproperty.get(mapper.version_id_col, None)
  193. is self.parent_property
  194. )
  195. )
  196. _register_attribute(
  197. self.parent_property,
  198. mapper,
  199. useobject=False,
  200. compare_function=coltype.compare_values,
  201. active_history=active_history,
  202. )
  203. def create_row_processor(
  204. self,
  205. context,
  206. query_entity,
  207. path,
  208. loadopt,
  209. mapper,
  210. result,
  211. adapter,
  212. populators,
  213. ):
  214. # look through list of columns represented here
  215. # to see which, if any, is present in the row.
  216. for col in self.columns:
  217. if adapter:
  218. col = adapter.columns[col]
  219. getter = result._getter(col, False)
  220. if getter:
  221. populators["quick"].append((self.key, getter))
  222. break
  223. else:
  224. populators["expire"].append((self.key, True))
  225. @log.class_logger
  226. @properties.ColumnProperty.strategy_for(query_expression=True)
  227. class ExpressionColumnLoader(ColumnLoader):
  228. def __init__(self, parent, strategy_key):
  229. super(ExpressionColumnLoader, self).__init__(parent, strategy_key)
  230. # compare to the "default" expression that is mapped in
  231. # the column. If it's sql.null, we don't need to render
  232. # unless an expr is passed in the options.
  233. null = sql.null().label(None)
  234. self._have_default_expression = any(
  235. not c.compare(null) for c in self.parent_property.columns
  236. )
  237. def setup_query(
  238. self,
  239. compile_state,
  240. query_entity,
  241. path,
  242. loadopt,
  243. adapter,
  244. column_collection,
  245. memoized_populators,
  246. **kwargs
  247. ):
  248. columns = None
  249. if loadopt and "expression" in loadopt.local_opts:
  250. columns = [loadopt.local_opts["expression"]]
  251. elif self._have_default_expression:
  252. columns = self.parent_property.columns
  253. if columns is None:
  254. return
  255. for c in columns:
  256. if adapter:
  257. c = adapter.columns[c]
  258. column_collection.append(c)
  259. fetch = columns[0]
  260. if adapter:
  261. fetch = adapter.columns[fetch]
  262. memoized_populators[self.parent_property] = fetch
  263. def create_row_processor(
  264. self,
  265. context,
  266. query_entity,
  267. path,
  268. loadopt,
  269. mapper,
  270. result,
  271. adapter,
  272. populators,
  273. ):
  274. # look through list of columns represented here
  275. # to see which, if any, is present in the row.
  276. if loadopt and "expression" in loadopt.local_opts:
  277. columns = [loadopt.local_opts["expression"]]
  278. for col in columns:
  279. if adapter:
  280. col = adapter.columns[col]
  281. getter = result._getter(col, False)
  282. if getter:
  283. populators["quick"].append((self.key, getter))
  284. break
  285. else:
  286. populators["expire"].append((self.key, True))
  287. def init_class_attribute(self, mapper):
  288. self.is_class_level = True
  289. _register_attribute(
  290. self.parent_property,
  291. mapper,
  292. useobject=False,
  293. compare_function=self.columns[0].type.compare_values,
  294. accepts_scalar_loader=False,
  295. )
  296. @log.class_logger
  297. @properties.ColumnProperty.strategy_for(deferred=True, instrument=True)
  298. @properties.ColumnProperty.strategy_for(
  299. deferred=True, instrument=True, raiseload=True
  300. )
  301. @properties.ColumnProperty.strategy_for(do_nothing=True)
  302. class DeferredColumnLoader(LoaderStrategy):
  303. """Provide loading behavior for a deferred :class:`.ColumnProperty`."""
  304. __slots__ = "columns", "group", "raiseload"
  305. def __init__(self, parent, strategy_key):
  306. super(DeferredColumnLoader, self).__init__(parent, strategy_key)
  307. if hasattr(self.parent_property, "composite_class"):
  308. raise NotImplementedError(
  309. "Deferred loading for composite " "types not implemented yet"
  310. )
  311. self.raiseload = self.strategy_opts.get("raiseload", False)
  312. self.columns = self.parent_property.columns
  313. self.group = self.parent_property.group
  314. def create_row_processor(
  315. self,
  316. context,
  317. query_entity,
  318. path,
  319. loadopt,
  320. mapper,
  321. result,
  322. adapter,
  323. populators,
  324. ):
  325. # for a DeferredColumnLoader, this method is only used during a
  326. # "row processor only" query; see test_deferred.py ->
  327. # tests with "rowproc_only" in their name. As of the 1.0 series,
  328. # loading._instance_processor doesn't use a "row processing" function
  329. # to populate columns, instead it uses data in the "populators"
  330. # dictionary. Normally, the DeferredColumnLoader.setup_query()
  331. # sets up that data in the "memoized_populators" dictionary
  332. # and "create_row_processor()" here is never invoked.
  333. if not self.is_class_level:
  334. if self.raiseload:
  335. set_deferred_for_local_state = (
  336. self.parent_property._raise_column_loader
  337. )
  338. else:
  339. set_deferred_for_local_state = (
  340. self.parent_property._deferred_column_loader
  341. )
  342. populators["new"].append((self.key, set_deferred_for_local_state))
  343. else:
  344. populators["expire"].append((self.key, False))
  345. def init_class_attribute(self, mapper):
  346. self.is_class_level = True
  347. _register_attribute(
  348. self.parent_property,
  349. mapper,
  350. useobject=False,
  351. compare_function=self.columns[0].type.compare_values,
  352. callable_=self._load_for_state,
  353. load_on_unexpire=False,
  354. )
  355. def setup_query(
  356. self,
  357. compile_state,
  358. query_entity,
  359. path,
  360. loadopt,
  361. adapter,
  362. column_collection,
  363. memoized_populators,
  364. only_load_props=None,
  365. **kw
  366. ):
  367. if (
  368. (
  369. compile_state.compile_options._render_for_subquery
  370. and self.parent_property._renders_in_subqueries
  371. )
  372. or (
  373. loadopt
  374. and "undefer_pks" in loadopt.local_opts
  375. and set(self.columns).intersection(
  376. self.parent._should_undefer_in_wildcard
  377. )
  378. )
  379. or (
  380. loadopt
  381. and self.group
  382. and loadopt.local_opts.get(
  383. "undefer_group_%s" % self.group, False
  384. )
  385. )
  386. or (only_load_props and self.key in only_load_props)
  387. ):
  388. self.parent_property._get_strategy(
  389. (("deferred", False), ("instrument", True))
  390. ).setup_query(
  391. compile_state,
  392. query_entity,
  393. path,
  394. loadopt,
  395. adapter,
  396. column_collection,
  397. memoized_populators,
  398. **kw
  399. )
  400. elif self.is_class_level:
  401. memoized_populators[self.parent_property] = _SET_DEFERRED_EXPIRED
  402. elif not self.raiseload:
  403. memoized_populators[self.parent_property] = _DEFER_FOR_STATE
  404. else:
  405. memoized_populators[self.parent_property] = _RAISE_FOR_STATE
  406. def _load_for_state(self, state, passive):
  407. if not state.key:
  408. return attributes.ATTR_EMPTY
  409. if not passive & attributes.SQL_OK:
  410. return attributes.PASSIVE_NO_RESULT
  411. localparent = state.manager.mapper
  412. if self.group:
  413. toload = [
  414. p.key
  415. for p in localparent.iterate_properties
  416. if isinstance(p, StrategizedProperty)
  417. and isinstance(p.strategy, DeferredColumnLoader)
  418. and p.group == self.group
  419. ]
  420. else:
  421. toload = [self.key]
  422. # narrow the keys down to just those which have no history
  423. group = [k for k in toload if k in state.unmodified]
  424. session = _state_session(state)
  425. if session is None:
  426. raise orm_exc.DetachedInstanceError(
  427. "Parent instance %s is not bound to a Session; "
  428. "deferred load operation of attribute '%s' cannot proceed"
  429. % (orm_util.state_str(state), self.key)
  430. )
  431. if self.raiseload:
  432. self._invoke_raise_load(state, passive, "raise")
  433. if (
  434. loading.load_on_ident(
  435. session,
  436. sql.select(localparent).set_label_style(
  437. LABEL_STYLE_TABLENAME_PLUS_COL
  438. ),
  439. state.key,
  440. only_load_props=group,
  441. refresh_state=state,
  442. )
  443. is None
  444. ):
  445. raise orm_exc.ObjectDeletedError(state)
  446. return attributes.ATTR_WAS_SET
  447. def _invoke_raise_load(self, state, passive, lazy):
  448. raise sa_exc.InvalidRequestError(
  449. "'%s' is not available due to raiseload=True" % (self,)
  450. )
  451. class LoadDeferredColumns(object):
  452. """serializable loader object used by DeferredColumnLoader"""
  453. def __init__(self, key, raiseload=False):
  454. self.key = key
  455. self.raiseload = raiseload
  456. def __call__(self, state, passive=attributes.PASSIVE_OFF):
  457. key = self.key
  458. localparent = state.manager.mapper
  459. prop = localparent._props[key]
  460. if self.raiseload:
  461. strategy_key = (
  462. ("deferred", True),
  463. ("instrument", True),
  464. ("raiseload", True),
  465. )
  466. else:
  467. strategy_key = (("deferred", True), ("instrument", True))
  468. strategy = prop._get_strategy(strategy_key)
  469. return strategy._load_for_state(state, passive)
  470. class AbstractRelationshipLoader(LoaderStrategy):
  471. """LoaderStratgies which deal with related objects."""
  472. __slots__ = "mapper", "target", "uselist", "entity"
  473. def __init__(self, parent, strategy_key):
  474. super(AbstractRelationshipLoader, self).__init__(parent, strategy_key)
  475. self.mapper = self.parent_property.mapper
  476. self.entity = self.parent_property.entity
  477. self.target = self.parent_property.target
  478. self.uselist = self.parent_property.uselist
  479. @log.class_logger
  480. @relationships.RelationshipProperty.strategy_for(do_nothing=True)
  481. class DoNothingLoader(LoaderStrategy):
  482. """Relationship loader that makes no change to the object's state.
  483. Compared to NoLoader, this loader does not initialize the
  484. collection/attribute to empty/none; the usual default LazyLoader will
  485. take effect.
  486. """
  487. @log.class_logger
  488. @relationships.RelationshipProperty.strategy_for(lazy="noload")
  489. @relationships.RelationshipProperty.strategy_for(lazy=None)
  490. class NoLoader(AbstractRelationshipLoader):
  491. """Provide loading behavior for a :class:`.RelationshipProperty`
  492. with "lazy=None".
  493. """
  494. __slots__ = ()
  495. def init_class_attribute(self, mapper):
  496. self.is_class_level = True
  497. _register_attribute(
  498. self.parent_property,
  499. mapper,
  500. useobject=True,
  501. typecallable=self.parent_property.collection_class,
  502. )
  503. def create_row_processor(
  504. self,
  505. context,
  506. query_entity,
  507. path,
  508. loadopt,
  509. mapper,
  510. result,
  511. adapter,
  512. populators,
  513. ):
  514. def invoke_no_load(state, dict_, row):
  515. if self.uselist:
  516. attributes.init_state_collection(state, dict_, self.key)
  517. else:
  518. dict_[self.key] = None
  519. populators["new"].append((self.key, invoke_no_load))
  520. @log.class_logger
  521. @relationships.RelationshipProperty.strategy_for(lazy=True)
  522. @relationships.RelationshipProperty.strategy_for(lazy="select")
  523. @relationships.RelationshipProperty.strategy_for(lazy="raise")
  524. @relationships.RelationshipProperty.strategy_for(lazy="raise_on_sql")
  525. @relationships.RelationshipProperty.strategy_for(lazy="baked_select")
  526. class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
  527. """Provide loading behavior for a :class:`.RelationshipProperty`
  528. with "lazy=True", that is loads when first accessed.
  529. """
  530. __slots__ = (
  531. "_lazywhere",
  532. "_rev_lazywhere",
  533. "_lazyload_reverse_option",
  534. "_order_by",
  535. "use_get",
  536. "is_aliased_class",
  537. "_bind_to_col",
  538. "_equated_columns",
  539. "_rev_bind_to_col",
  540. "_rev_equated_columns",
  541. "_simple_lazy_clause",
  542. "_raise_always",
  543. "_raise_on_sql",
  544. "_lambda_cache",
  545. )
  546. def __init__(self, parent, strategy_key):
  547. super(LazyLoader, self).__init__(parent, strategy_key)
  548. self._raise_always = self.strategy_opts["lazy"] == "raise"
  549. self._raise_on_sql = self.strategy_opts["lazy"] == "raise_on_sql"
  550. self.is_aliased_class = inspect(self.entity).is_aliased_class
  551. join_condition = self.parent_property._join_condition
  552. (
  553. self._lazywhere,
  554. self._bind_to_col,
  555. self._equated_columns,
  556. ) = join_condition.create_lazy_clause()
  557. (
  558. self._rev_lazywhere,
  559. self._rev_bind_to_col,
  560. self._rev_equated_columns,
  561. ) = join_condition.create_lazy_clause(reverse_direction=True)
  562. if self.parent_property.order_by:
  563. self._order_by = [
  564. sql_util._deep_annotate(elem, {"_orm_adapt": True})
  565. for elem in util.to_list(self.parent_property.order_by)
  566. ]
  567. else:
  568. self._order_by = None
  569. self.logger.info("%s lazy loading clause %s", self, self._lazywhere)
  570. # determine if our "lazywhere" clause is the same as the mapper's
  571. # get() clause. then we can just use mapper.get()
  572. #
  573. # TODO: the "not self.uselist" can be taken out entirely; a m2o
  574. # load that populates for a list (very unusual, but is possible with
  575. # the API) can still set for "None" and the attribute system will
  576. # populate as an empty list.
  577. self.use_get = (
  578. not self.is_aliased_class
  579. and not self.uselist
  580. and self.entity._get_clause[0].compare(
  581. self._lazywhere,
  582. use_proxies=True,
  583. compare_keys=False,
  584. equivalents=self.mapper._equivalent_columns,
  585. )
  586. )
  587. if self.use_get:
  588. for col in list(self._equated_columns):
  589. if col in self.mapper._equivalent_columns:
  590. for c in self.mapper._equivalent_columns[col]:
  591. self._equated_columns[c] = self._equated_columns[col]
  592. self.logger.info(
  593. "%s will use Session.get() to " "optimize instance loads", self
  594. )
  595. def init_class_attribute(self, mapper):
  596. self.is_class_level = True
  597. active_history = (
  598. self.parent_property.active_history
  599. or self.parent_property.direction is not interfaces.MANYTOONE
  600. or not self.use_get
  601. )
  602. # MANYTOONE currently only needs the
  603. # "old" value for delete-orphan
  604. # cascades. the required _SingleParentValidator
  605. # will enable active_history
  606. # in that case. otherwise we don't need the
  607. # "old" value during backref operations.
  608. _register_attribute(
  609. self.parent_property,
  610. mapper,
  611. useobject=True,
  612. callable_=self._load_for_state,
  613. typecallable=self.parent_property.collection_class,
  614. active_history=active_history,
  615. )
  616. def _memoized_attr__simple_lazy_clause(self):
  617. lazywhere = sql_util._deep_annotate(
  618. self._lazywhere, {"_orm_adapt": True}
  619. )
  620. criterion, bind_to_col = (lazywhere, self._bind_to_col)
  621. params = []
  622. def visit_bindparam(bindparam):
  623. bindparam.unique = False
  624. visitors.traverse(criterion, {}, {"bindparam": visit_bindparam})
  625. def visit_bindparam(bindparam):
  626. if bindparam._identifying_key in bind_to_col:
  627. params.append(
  628. (
  629. bindparam.key,
  630. bind_to_col[bindparam._identifying_key],
  631. None,
  632. )
  633. )
  634. elif bindparam.callable is None:
  635. params.append((bindparam.key, None, bindparam.value))
  636. criterion = visitors.cloned_traverse(
  637. criterion, {}, {"bindparam": visit_bindparam}
  638. )
  639. return criterion, params
  640. def _generate_lazy_clause(self, state, passive):
  641. criterion, param_keys = self._simple_lazy_clause
  642. if state is None:
  643. return sql_util.adapt_criterion_to_null(
  644. criterion, [key for key, ident, value in param_keys]
  645. )
  646. mapper = self.parent_property.parent
  647. o = state.obj() # strong ref
  648. dict_ = attributes.instance_dict(o)
  649. if passive & attributes.INIT_OK:
  650. passive ^= attributes.INIT_OK
  651. params = {}
  652. for key, ident, value in param_keys:
  653. if ident is not None:
  654. if passive and passive & attributes.LOAD_AGAINST_COMMITTED:
  655. value = mapper._get_committed_state_attr_by_column(
  656. state, dict_, ident, passive
  657. )
  658. else:
  659. value = mapper._get_state_attr_by_column(
  660. state, dict_, ident, passive
  661. )
  662. params[key] = value
  663. return criterion, params
  664. def _invoke_raise_load(self, state, passive, lazy):
  665. raise sa_exc.InvalidRequestError(
  666. "'%s' is not available due to lazy='%s'" % (self, lazy)
  667. )
  668. def _load_for_state(self, state, passive, loadopt=None, extra_criteria=()):
  669. if not state.key and (
  670. (
  671. not self.parent_property.load_on_pending
  672. and not state._load_pending
  673. )
  674. or not state.session_id
  675. ):
  676. return attributes.ATTR_EMPTY
  677. pending = not state.key
  678. primary_key_identity = None
  679. use_get = self.use_get and (not loadopt or not loadopt._extra_criteria)
  680. if (not passive & attributes.SQL_OK and not use_get) or (
  681. not passive & attributes.NON_PERSISTENT_OK and pending
  682. ):
  683. return attributes.PASSIVE_NO_RESULT
  684. if (
  685. # we were given lazy="raise"
  686. self._raise_always
  687. # the no_raise history-related flag was not passed
  688. and not passive & attributes.NO_RAISE
  689. and (
  690. # if we are use_get and related_object_ok is disabled,
  691. # which means we are at most looking in the identity map
  692. # for history purposes or otherwise returning
  693. # PASSIVE_NO_RESULT, don't raise. This is also a
  694. # history-related flag
  695. not use_get
  696. or passive & attributes.RELATED_OBJECT_OK
  697. )
  698. ):
  699. self._invoke_raise_load(state, passive, "raise")
  700. session = _state_session(state)
  701. if not session:
  702. if passive & attributes.NO_RAISE:
  703. return attributes.PASSIVE_NO_RESULT
  704. raise orm_exc.DetachedInstanceError(
  705. "Parent instance %s is not bound to a Session; "
  706. "lazy load operation of attribute '%s' cannot proceed"
  707. % (orm_util.state_str(state), self.key)
  708. )
  709. # if we have a simple primary key load, check the
  710. # identity map without generating a Query at all
  711. if use_get:
  712. primary_key_identity = self._get_ident_for_use_get(
  713. session, state, passive
  714. )
  715. if attributes.PASSIVE_NO_RESULT in primary_key_identity:
  716. return attributes.PASSIVE_NO_RESULT
  717. elif attributes.NEVER_SET in primary_key_identity:
  718. return attributes.NEVER_SET
  719. if _none_set.issuperset(primary_key_identity):
  720. return None
  721. if self.key in state.dict:
  722. return attributes.ATTR_WAS_SET
  723. # look for this identity in the identity map. Delegate to the
  724. # Query class in use, as it may have special rules for how it
  725. # does this, including how it decides what the correct
  726. # identity_token would be for this identity.
  727. instance = session._identity_lookup(
  728. self.entity,
  729. primary_key_identity,
  730. passive=passive,
  731. lazy_loaded_from=state,
  732. )
  733. if instance is not None:
  734. if instance is attributes.PASSIVE_CLASS_MISMATCH:
  735. return None
  736. else:
  737. return instance
  738. elif (
  739. not passive & attributes.SQL_OK
  740. or not passive & attributes.RELATED_OBJECT_OK
  741. ):
  742. return attributes.PASSIVE_NO_RESULT
  743. return self._emit_lazyload(
  744. session,
  745. state,
  746. primary_key_identity,
  747. passive,
  748. loadopt,
  749. extra_criteria,
  750. )
  751. def _get_ident_for_use_get(self, session, state, passive):
  752. instance_mapper = state.manager.mapper
  753. if passive & attributes.LOAD_AGAINST_COMMITTED:
  754. get_attr = instance_mapper._get_committed_state_attr_by_column
  755. else:
  756. get_attr = instance_mapper._get_state_attr_by_column
  757. dict_ = state.dict
  758. return [
  759. get_attr(state, dict_, self._equated_columns[pk], passive=passive)
  760. for pk in self.mapper.primary_key
  761. ]
  762. def _memoized_attr__lambda_cache(self):
  763. # cache is per lazy loader, and is used for caching of
  764. # sqlalchemy.sql.lambdas.AnalyzedCode and
  765. # sqlalchemy.sql.lambdas.AnalyzedFunction objects which are generated
  766. # from the StatementLambda used.
  767. return util.LRUCache(30)
  768. @util.preload_module("sqlalchemy.orm.strategy_options")
  769. def _emit_lazyload(
  770. self,
  771. session,
  772. state,
  773. primary_key_identity,
  774. passive,
  775. loadopt,
  776. extra_criteria,
  777. ):
  778. strategy_options = util.preloaded.orm_strategy_options
  779. stmt = sql.lambda_stmt(
  780. lambda: sql.select(self.entity)
  781. .set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL)
  782. ._set_compile_options(ORMCompileState.default_compile_options),
  783. global_track_bound_values=False,
  784. lambda_cache=self._lambda_cache,
  785. track_on=(self,),
  786. )
  787. if not self.parent_property.bake_queries:
  788. stmt = stmt.spoil()
  789. load_options = QueryContext.default_load_options
  790. load_options += {
  791. "_invoke_all_eagers": False,
  792. "_lazy_loaded_from": state,
  793. }
  794. if self.parent_property.secondary is not None:
  795. stmt = stmt.add_criteria(
  796. lambda stmt: stmt.select_from(
  797. self.mapper, self.parent_property.secondary
  798. ),
  799. track_on=[self.parent_property],
  800. )
  801. pending = not state.key
  802. # don't autoflush on pending
  803. if pending or passive & attributes.NO_AUTOFLUSH:
  804. stmt += lambda stmt: stmt.execution_options(autoflush=False)
  805. use_get = self.use_get
  806. if state.load_options or (loadopt and loadopt._extra_criteria):
  807. effective_path = state.load_path[self.parent_property]
  808. opts = list(state.load_options)
  809. if loadopt and loadopt._extra_criteria:
  810. use_get = False
  811. opts += (
  812. orm_util.LoaderCriteriaOption(self.entity, extra_criteria),
  813. )
  814. stmt += lambda stmt: stmt.options(*opts)
  815. else:
  816. # this path is used if there are not already any options
  817. # in the query, but an event may want to add them
  818. effective_path = state.mapper._path_registry[self.parent_property]
  819. stmt += lambda stmt: stmt._update_compile_options(
  820. {"_current_path": effective_path}
  821. )
  822. if use_get:
  823. if self._raise_on_sql and not passive & attributes.NO_RAISE:
  824. self._invoke_raise_load(state, passive, "raise_on_sql")
  825. return loading.load_on_pk_identity(
  826. session, stmt, primary_key_identity, load_options=load_options
  827. )
  828. if self._order_by:
  829. stmt = stmt.add_criteria(
  830. lambda stmt: stmt.order_by(*self._order_by), track_on=[self]
  831. )
  832. def _lazyload_reverse(compile_context):
  833. for rev in self.parent_property._reverse_property:
  834. # reverse props that are MANYTOONE are loading *this*
  835. # object from get(), so don't need to eager out to those.
  836. if (
  837. rev.direction is interfaces.MANYTOONE
  838. and rev._use_get
  839. and not isinstance(rev.strategy, LazyLoader)
  840. ):
  841. strategy_options.Load.for_existing_path(
  842. compile_context.compile_options._current_path[
  843. rev.parent
  844. ]
  845. ).lazyload(rev).process_compile_state(compile_context)
  846. stmt = stmt.add_criteria(
  847. lambda stmt: stmt._add_context_option(
  848. _lazyload_reverse, self.parent_property
  849. ),
  850. track_on=[self],
  851. )
  852. lazy_clause, params = self._generate_lazy_clause(state, passive)
  853. execution_options = {
  854. "_sa_orm_load_options": load_options,
  855. }
  856. if self.key in state.dict:
  857. return attributes.ATTR_WAS_SET
  858. if pending:
  859. if util.has_intersection(orm_util._none_set, params.values()):
  860. return None
  861. elif util.has_intersection(orm_util._never_set, params.values()):
  862. return None
  863. if self._raise_on_sql and not passive & attributes.NO_RAISE:
  864. self._invoke_raise_load(state, passive, "raise_on_sql")
  865. stmt = stmt.add_criteria(
  866. lambda stmt: stmt.where(lazy_clause), enable_tracking=False
  867. )
  868. result = session.execute(
  869. stmt, params, execution_options=execution_options
  870. )
  871. result = result.unique().scalars().all()
  872. if self.uselist:
  873. return result
  874. else:
  875. l = len(result)
  876. if l:
  877. if l > 1:
  878. util.warn(
  879. "Multiple rows returned with "
  880. "uselist=False for lazily-loaded attribute '%s' "
  881. % self.parent_property
  882. )
  883. return result[0]
  884. else:
  885. return None
  886. def create_row_processor(
  887. self,
  888. context,
  889. query_entity,
  890. path,
  891. loadopt,
  892. mapper,
  893. result,
  894. adapter,
  895. populators,
  896. ):
  897. key = self.key
  898. if not self.is_class_level or (loadopt and loadopt._extra_criteria):
  899. # we are not the primary manager for this attribute
  900. # on this class - set up a
  901. # per-instance lazyloader, which will override the
  902. # class-level behavior.
  903. # this currently only happens when using a
  904. # "lazyload" option on a "no load"
  905. # attribute - "eager" attributes always have a
  906. # class-level lazyloader installed.
  907. set_lazy_callable = (
  908. InstanceState._instance_level_callable_processor
  909. )(
  910. mapper.class_manager,
  911. LoadLazyAttribute(
  912. key,
  913. self,
  914. loadopt,
  915. loadopt._generate_extra_criteria(context)
  916. if loadopt._extra_criteria
  917. else None,
  918. ),
  919. key,
  920. )
  921. populators["new"].append((self.key, set_lazy_callable))
  922. elif context.populate_existing or mapper.always_refresh:
  923. def reset_for_lazy_callable(state, dict_, row):
  924. # we are the primary manager for this attribute on
  925. # this class - reset its
  926. # per-instance attribute state, so that the class-level
  927. # lazy loader is
  928. # executed when next referenced on this instance.
  929. # this is needed in
  930. # populate_existing() types of scenarios to reset
  931. # any existing state.
  932. state._reset(dict_, key)
  933. populators["new"].append((self.key, reset_for_lazy_callable))
  934. class LoadLazyAttribute(object):
  935. """semi-serializable loader object used by LazyLoader
  936. Historically, this object would be carried along with instances that
  937. needed to run lazyloaders, so it had to be serializable to support
  938. cached instances.
  939. this is no longer a general requirement, and the case where this object
  940. is used is exactly the case where we can't really serialize easily,
  941. which is when extra criteria in the loader option is present.
  942. We can't reliably serialize that as it refers to mapped entities and
  943. AliasedClass objects that are local to the current process, which would
  944. need to be matched up on deserialize e.g. the sqlalchemy.ext.serializer
  945. approach.
  946. """
  947. def __init__(self, key, initiating_strategy, loadopt, extra_criteria):
  948. self.key = key
  949. self.strategy_key = initiating_strategy.strategy_key
  950. self.loadopt = loadopt
  951. self.extra_criteria = extra_criteria
  952. def __getstate__(self):
  953. if self.extra_criteria is not None:
  954. util.warn(
  955. "Can't reliably serialize a lazyload() option that "
  956. "contains additional criteria; please use eager loading "
  957. "for this case"
  958. )
  959. return {
  960. "key": self.key,
  961. "strategy_key": self.strategy_key,
  962. "loadopt": self.loadopt,
  963. "extra_criteria": (),
  964. }
  965. def __call__(self, state, passive=attributes.PASSIVE_OFF):
  966. key = self.key
  967. instance_mapper = state.manager.mapper
  968. prop = instance_mapper._props[key]
  969. strategy = prop._strategies[self.strategy_key]
  970. return strategy._load_for_state(
  971. state,
  972. passive,
  973. loadopt=self.loadopt,
  974. extra_criteria=self.extra_criteria,
  975. )
  976. class PostLoader(AbstractRelationshipLoader):
  977. """A relationship loader that emits a second SELECT statement."""
  978. def _check_recursive_postload(self, context, path, join_depth=None):
  979. effective_path = (
  980. context.compile_state.current_path or orm_util.PathRegistry.root
  981. ) + path
  982. if loading.PostLoad.path_exists(
  983. context, effective_path, self.parent_property
  984. ):
  985. return True
  986. path_w_prop = path[self.parent_property]
  987. effective_path_w_prop = effective_path[self.parent_property]
  988. if not path_w_prop.contains(context.attributes, "loader"):
  989. if join_depth:
  990. if effective_path_w_prop.length / 2 > join_depth:
  991. return True
  992. elif effective_path_w_prop.contains_mapper(self.mapper):
  993. return True
  994. return False
  995. def _immediateload_create_row_processor(
  996. self,
  997. context,
  998. query_entity,
  999. path,
  1000. loadopt,
  1001. mapper,
  1002. result,
  1003. adapter,
  1004. populators,
  1005. ):
  1006. return self.parent_property._get_strategy(
  1007. (("lazy", "immediate"),)
  1008. ).create_row_processor(
  1009. context,
  1010. query_entity,
  1011. path,
  1012. loadopt,
  1013. mapper,
  1014. result,
  1015. adapter,
  1016. populators,
  1017. )
  1018. @relationships.RelationshipProperty.strategy_for(lazy="immediate")
  1019. class ImmediateLoader(PostLoader):
  1020. __slots__ = ()
  1021. def init_class_attribute(self, mapper):
  1022. self.parent_property._get_strategy(
  1023. (("lazy", "select"),)
  1024. ).init_class_attribute(mapper)
  1025. def create_row_processor(
  1026. self,
  1027. context,
  1028. query_entity,
  1029. path,
  1030. loadopt,
  1031. mapper,
  1032. result,
  1033. adapter,
  1034. populators,
  1035. ):
  1036. def load_immediate(state, dict_, row):
  1037. state.get_impl(self.key).get(state, dict_, flags)
  1038. if self._check_recursive_postload(context, path):
  1039. # this will not emit SQL and will only emit for a many-to-one
  1040. # "use get" load. the "_RELATED" part means it may return
  1041. # instance even if its expired, since this is a mutually-recursive
  1042. # load operation.
  1043. flags = attributes.PASSIVE_NO_FETCH_RELATED | attributes.NO_RAISE
  1044. else:
  1045. flags = attributes.PASSIVE_OFF | attributes.NO_RAISE
  1046. populators["delayed"].append((self.key, load_immediate))
  1047. @log.class_logger
  1048. @relationships.RelationshipProperty.strategy_for(lazy="subquery")
  1049. class SubqueryLoader(PostLoader):
  1050. __slots__ = ("join_depth",)
  1051. def __init__(self, parent, strategy_key):
  1052. super(SubqueryLoader, self).__init__(parent, strategy_key)
  1053. self.join_depth = self.parent_property.join_depth
  1054. def init_class_attribute(self, mapper):
  1055. self.parent_property._get_strategy(
  1056. (("lazy", "select"),)
  1057. ).init_class_attribute(mapper)
  1058. def _get_leftmost(
  1059. self,
  1060. orig_query_entity_index,
  1061. subq_path,
  1062. current_compile_state,
  1063. is_root,
  1064. ):
  1065. given_subq_path = subq_path
  1066. subq_path = subq_path.path
  1067. subq_mapper = orm_util._class_to_mapper(subq_path[0])
  1068. # determine attributes of the leftmost mapper
  1069. if (
  1070. self.parent.isa(subq_mapper)
  1071. and self.parent_property is subq_path[1]
  1072. ):
  1073. leftmost_mapper, leftmost_prop = self.parent, self.parent_property
  1074. else:
  1075. leftmost_mapper, leftmost_prop = subq_mapper, subq_path[1]
  1076. if is_root:
  1077. # the subq_path is also coming from cached state, so when we start
  1078. # building up this path, it has to also be converted to be in terms
  1079. # of the current state. this is for the specific case of the entity
  1080. # is an AliasedClass against a subquery that's not otherwise going
  1081. # to adapt
  1082. new_subq_path = current_compile_state._entities[
  1083. orig_query_entity_index
  1084. ].entity_zero._path_registry[leftmost_prop]
  1085. additional = len(subq_path) - len(new_subq_path)
  1086. if additional:
  1087. new_subq_path += path_registry.PathRegistry.coerce(
  1088. subq_path[-additional:]
  1089. )
  1090. else:
  1091. new_subq_path = given_subq_path
  1092. leftmost_cols = leftmost_prop.local_columns
  1093. leftmost_attr = [
  1094. getattr(
  1095. new_subq_path.path[0].entity,
  1096. leftmost_mapper._columntoproperty[c].key,
  1097. )
  1098. for c in leftmost_cols
  1099. ]
  1100. return leftmost_mapper, leftmost_attr, leftmost_prop, new_subq_path
  1101. def _generate_from_original_query(
  1102. self,
  1103. orig_compile_state,
  1104. orig_query,
  1105. leftmost_mapper,
  1106. leftmost_attr,
  1107. leftmost_relationship,
  1108. orig_entity,
  1109. ):
  1110. # reformat the original query
  1111. # to look only for significant columns
  1112. q = orig_query._clone().correlate(None)
  1113. # LEGACY: make a Query back from the select() !!
  1114. # This suits at least two legacy cases:
  1115. # 1. applications which expect before_compile() to be called
  1116. # below when we run .subquery() on this query (Keystone)
  1117. # 2. applications which are doing subqueryload with complex
  1118. # from_self() queries, as query.subquery() / .statement
  1119. # has to do the full compile context for multiply-nested
  1120. # from_self() (Neutron) - see test_subqload_from_self
  1121. # for demo.
  1122. q2 = query.Query.__new__(query.Query)
  1123. q2.__dict__.update(q.__dict__)
  1124. q = q2
  1125. # set the query's "FROM" list explicitly to what the
  1126. # FROM list would be in any case, as we will be limiting
  1127. # the columns in the SELECT list which may no longer include
  1128. # all entities mentioned in things like WHERE, JOIN, etc.
  1129. if not q._from_obj:
  1130. q._enable_assertions = False
  1131. q.select_from.non_generative(
  1132. q,
  1133. *{
  1134. ent["entity"]
  1135. for ent in _column_descriptions(
  1136. orig_query, compile_state=orig_compile_state
  1137. )
  1138. if ent["entity"] is not None
  1139. }
  1140. )
  1141. # select from the identity columns of the outer (specifically, these
  1142. # are the 'local_cols' of the property). This will remove other
  1143. # columns from the query that might suggest the right entity which is
  1144. # why we do set select_from above. The attributes we have are
  1145. # coerced and adapted using the original query's adapter, which is
  1146. # needed only for the case of adapting a subclass column to
  1147. # that of a polymorphic selectable, e.g. we have
  1148. # Engineer.primary_language and the entity is Person. All other
  1149. # adaptations, e.g. from_self, select_entity_from(), will occur
  1150. # within the new query when it compiles, as the compile_state we are
  1151. # using here is only a partial one. If the subqueryload is from a
  1152. # with_polymorphic() or other aliased() object, left_attr will already
  1153. # be the correct attributes so no adaptation is needed.
  1154. target_cols = orig_compile_state._adapt_col_list(
  1155. [
  1156. sql.coercions.expect(sql.roles.ColumnsClauseRole, o)
  1157. for o in leftmost_attr
  1158. ],
  1159. orig_compile_state._get_current_adapter(),
  1160. )
  1161. q._raw_columns = target_cols
  1162. distinct_target_key = leftmost_relationship.distinct_target_key
  1163. if distinct_target_key is True:
  1164. q._distinct = True
  1165. elif distinct_target_key is None:
  1166. # if target_cols refer to a non-primary key or only
  1167. # part of a composite primary key, set the q as distinct
  1168. for t in set(c.table for c in target_cols):
  1169. if not set(target_cols).issuperset(t.primary_key):
  1170. q._distinct = True
  1171. break
  1172. # don't need ORDER BY if no limit/offset
  1173. if not q._has_row_limiting_clause:
  1174. q._order_by_clauses = ()
  1175. if q._distinct is True and q._order_by_clauses:
  1176. # the logic to automatically add the order by columns to the query
  1177. # when distinct is True is deprecated in the query
  1178. to_add = sql_util.expand_column_list_from_order_by(
  1179. target_cols, q._order_by_clauses
  1180. )
  1181. if to_add:
  1182. q._set_entities(target_cols + to_add)
  1183. # the original query now becomes a subquery
  1184. # which we'll join onto.
  1185. # LEGACY: as "q" is a Query, the before_compile() event is invoked
  1186. # here.
  1187. embed_q = q.set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL).subquery()
  1188. left_alias = orm_util.AliasedClass(
  1189. leftmost_mapper, embed_q, use_mapper_path=True
  1190. )
  1191. return left_alias
  1192. def _prep_for_joins(self, left_alias, subq_path):
  1193. # figure out what's being joined. a.k.a. the fun part
  1194. to_join = []
  1195. pairs = list(subq_path.pairs())
  1196. for i, (mapper, prop) in enumerate(pairs):
  1197. if i > 0:
  1198. # look at the previous mapper in the chain -
  1199. # if it is as or more specific than this prop's
  1200. # mapper, use that instead.
  1201. # note we have an assumption here that
  1202. # the non-first element is always going to be a mapper,
  1203. # not an AliasedClass
  1204. prev_mapper = pairs[i - 1][1].mapper
  1205. to_append = prev_mapper if prev_mapper.isa(mapper) else mapper
  1206. else:
  1207. to_append = mapper
  1208. to_join.append((to_append, prop.key))
  1209. # determine the immediate parent class we are joining from,
  1210. # which needs to be aliased.
  1211. if len(to_join) < 2:
  1212. # in the case of a one level eager load, this is the
  1213. # leftmost "left_alias".
  1214. parent_alias = left_alias
  1215. else:
  1216. info = inspect(to_join[-1][0])
  1217. if info.is_aliased_class:
  1218. parent_alias = info.entity
  1219. else:
  1220. # alias a plain mapper as we may be
  1221. # joining multiple times
  1222. parent_alias = orm_util.AliasedClass(
  1223. info.entity, use_mapper_path=True
  1224. )
  1225. local_cols = self.parent_property.local_columns
  1226. local_attr = [
  1227. getattr(parent_alias, self.parent._columntoproperty[c].key)
  1228. for c in local_cols
  1229. ]
  1230. return to_join, local_attr, parent_alias
  1231. def _apply_joins(
  1232. self, q, to_join, left_alias, parent_alias, effective_entity
  1233. ):
  1234. ltj = len(to_join)
  1235. if ltj == 1:
  1236. to_join = [
  1237. getattr(left_alias, to_join[0][1]).of_type(effective_entity)
  1238. ]
  1239. elif ltj == 2:
  1240. to_join = [
  1241. getattr(left_alias, to_join[0][1]).of_type(parent_alias),
  1242. getattr(parent_alias, to_join[-1][1]).of_type(
  1243. effective_entity
  1244. ),
  1245. ]
  1246. elif ltj > 2:
  1247. middle = [
  1248. (
  1249. orm_util.AliasedClass(item[0])
  1250. if not inspect(item[0]).is_aliased_class
  1251. else item[0].entity,
  1252. item[1],
  1253. )
  1254. for item in to_join[1:-1]
  1255. ]
  1256. inner = []
  1257. while middle:
  1258. item = middle.pop(0)
  1259. attr = getattr(item[0], item[1])
  1260. if middle:
  1261. attr = attr.of_type(middle[0][0])
  1262. else:
  1263. attr = attr.of_type(parent_alias)
  1264. inner.append(attr)
  1265. to_join = (
  1266. [getattr(left_alias, to_join[0][1]).of_type(inner[0].parent)]
  1267. + inner
  1268. + [
  1269. getattr(parent_alias, to_join[-1][1]).of_type(
  1270. effective_entity
  1271. )
  1272. ]
  1273. )
  1274. for attr in to_join:
  1275. q = q.join(attr)
  1276. return q
  1277. def _setup_options(
  1278. self,
  1279. context,
  1280. q,
  1281. subq_path,
  1282. rewritten_path,
  1283. orig_query,
  1284. effective_entity,
  1285. loadopt,
  1286. ):
  1287. opts = orig_query._with_options
  1288. if loadopt and loadopt._extra_criteria:
  1289. opts += (
  1290. orm_util.LoaderCriteriaOption(
  1291. self.entity,
  1292. loadopt._generate_extra_criteria(context),
  1293. ),
  1294. )
  1295. # propagate loader options etc. to the new query.
  1296. # these will fire relative to subq_path.
  1297. q = q._with_current_path(rewritten_path)
  1298. q = q.options(*opts)
  1299. return q
  1300. def _setup_outermost_orderby(self, q):
  1301. if self.parent_property.order_by:
  1302. def _setup_outermost_orderby(compile_context):
  1303. compile_context.eager_order_by += tuple(
  1304. util.to_list(self.parent_property.order_by)
  1305. )
  1306. q = q._add_context_option(
  1307. _setup_outermost_orderby, self.parent_property
  1308. )
  1309. return q
  1310. class _SubqCollections(object):
  1311. """Given a :class:`_query.Query` used to emit the "subquery load",
  1312. provide a load interface that executes the query at the
  1313. first moment a value is needed.
  1314. """
  1315. __slots__ = (
  1316. "session",
  1317. "execution_options",
  1318. "load_options",
  1319. "params",
  1320. "subq",
  1321. "_data",
  1322. )
  1323. def __init__(self, context, subq):
  1324. # avoid creating a cycle by storing context
  1325. # even though that's preferable
  1326. self.session = context.session
  1327. self.execution_options = context.execution_options
  1328. self.load_options = context.load_options
  1329. self.params = context.params or {}
  1330. self.subq = subq
  1331. self._data = None
  1332. def get(self, key, default):
  1333. if self._data is None:
  1334. self._load()
  1335. return self._data.get(key, default)
  1336. def _load(self):
  1337. self._data = collections.defaultdict(list)
  1338. q = self.subq
  1339. assert q.session is None
  1340. q = q.with_session(self.session)
  1341. if self.load_options._populate_existing:
  1342. q = q.populate_existing()
  1343. # to work with baked query, the parameters may have been
  1344. # updated since this query was created, so take these into account
  1345. rows = list(q.params(self.params))
  1346. for k, v in itertools.groupby(rows, lambda x: x[1:]):
  1347. self._data[k].extend(vv[0] for vv in v)
  1348. def loader(self, state, dict_, row):
  1349. if self._data is None:
  1350. self._load()
  1351. def _setup_query_from_rowproc(
  1352. self,
  1353. context,
  1354. query_entity,
  1355. path,
  1356. entity,
  1357. loadopt,
  1358. adapter,
  1359. ):
  1360. compile_state = context.compile_state
  1361. if (
  1362. not compile_state.compile_options._enable_eagerloads
  1363. or compile_state.compile_options._for_refresh_state
  1364. ):
  1365. return
  1366. orig_query_entity_index = compile_state._entities.index(query_entity)
  1367. context.loaders_require_buffering = True
  1368. path = path[self.parent_property]
  1369. # build up a path indicating the path from the leftmost
  1370. # entity to the thing we're subquery loading.
  1371. with_poly_entity = path.get(
  1372. compile_state.attributes, "path_with_polymorphic", None
  1373. )
  1374. if with_poly_entity is not None:
  1375. effective_entity = with_poly_entity
  1376. else:
  1377. effective_entity = self.entity
  1378. subq_path, rewritten_path = context.query._execution_options.get(
  1379. ("subquery_paths", None),
  1380. (orm_util.PathRegistry.root, orm_util.PathRegistry.root),
  1381. )
  1382. is_root = subq_path is orm_util.PathRegistry.root
  1383. subq_path = subq_path + path
  1384. rewritten_path = rewritten_path + path
  1385. # if not via query option, check for
  1386. # a cycle
  1387. # TODO: why is this here??? this is now handled
  1388. # by the _check_recursive_postload call
  1389. if not path.contains(compile_state.attributes, "loader"):
  1390. if self.join_depth:
  1391. if (
  1392. (
  1393. compile_state.current_path.length
  1394. if compile_state.current_path
  1395. else 0
  1396. )
  1397. + path.length
  1398. ) / 2 > self.join_depth:
  1399. return
  1400. elif subq_path.contains_mapper(self.mapper):
  1401. return
  1402. # use the current query being invoked, not the compile state
  1403. # one. this is so that we get the current parameters. however,
  1404. # it means we can't use the existing compile state, we have to make
  1405. # a new one. other approaches include possibly using the
  1406. # compiled query but swapping the params, seems only marginally
  1407. # less time spent but more complicated
  1408. orig_query = context.query._execution_options.get(
  1409. ("orig_query", SubqueryLoader), context.query
  1410. )
  1411. # make a new compile_state for the query that's probably cached, but
  1412. # we're sort of undoing a bit of that caching :(
  1413. compile_state_cls = ORMCompileState._get_plugin_class_for_plugin(
  1414. orig_query, "orm"
  1415. )
  1416. if orig_query._is_lambda_element:
  1417. if context.load_options._lazy_loaded_from is None:
  1418. util.warn(
  1419. 'subqueryloader for "%s" must invoke lambda callable '
  1420. "at %r in "
  1421. "order to produce a new query, decreasing the efficiency "
  1422. "of caching for this statement. Consider using "
  1423. "selectinload() for more effective full-lambda caching"
  1424. % (self, orig_query)
  1425. )
  1426. orig_query = orig_query._resolved
  1427. # this is the more "quick" version, however it's not clear how
  1428. # much of this we need. in particular I can't get a test to
  1429. # fail if the "set_base_alias" is missing and not sure why that is.
  1430. orig_compile_state = compile_state_cls._create_entities_collection(
  1431. orig_query, legacy=False
  1432. )
  1433. (
  1434. leftmost_mapper,
  1435. leftmost_attr,
  1436. leftmost_relationship,
  1437. rewritten_path,
  1438. ) = self._get_leftmost(
  1439. orig_query_entity_index,
  1440. rewritten_path,
  1441. orig_compile_state,
  1442. is_root,
  1443. )
  1444. # generate a new Query from the original, then
  1445. # produce a subquery from it.
  1446. left_alias = self._generate_from_original_query(
  1447. orig_compile_state,
  1448. orig_query,
  1449. leftmost_mapper,
  1450. leftmost_attr,
  1451. leftmost_relationship,
  1452. entity,
  1453. )
  1454. # generate another Query that will join the
  1455. # left alias to the target relationships.
  1456. # basically doing a longhand
  1457. # "from_self()". (from_self() itself not quite industrial
  1458. # strength enough for all contingencies...but very close)
  1459. q = query.Query(effective_entity)
  1460. q._execution_options = q._execution_options.union(
  1461. {
  1462. ("orig_query", SubqueryLoader): orig_query,
  1463. ("subquery_paths", None): (subq_path, rewritten_path),
  1464. }
  1465. )
  1466. q = q._set_enable_single_crit(False)
  1467. to_join, local_attr, parent_alias = self._prep_for_joins(
  1468. left_alias, subq_path
  1469. )
  1470. q = q.add_columns(*local_attr)
  1471. q = self._apply_joins(
  1472. q, to_join, left_alias, parent_alias, effective_entity
  1473. )
  1474. q = self._setup_options(
  1475. context,
  1476. q,
  1477. subq_path,
  1478. rewritten_path,
  1479. orig_query,
  1480. effective_entity,
  1481. loadopt,
  1482. )
  1483. q = self._setup_outermost_orderby(q)
  1484. return q
  1485. def create_row_processor(
  1486. self,
  1487. context,
  1488. query_entity,
  1489. path,
  1490. loadopt,
  1491. mapper,
  1492. result,
  1493. adapter,
  1494. populators,
  1495. ):
  1496. if context.refresh_state:
  1497. return self._immediateload_create_row_processor(
  1498. context,
  1499. query_entity,
  1500. path,
  1501. loadopt,
  1502. mapper,
  1503. result,
  1504. adapter,
  1505. populators,
  1506. )
  1507. # the subqueryloader does a similar check in setup_query() unlike
  1508. # the other post loaders, however we have this here for consistency
  1509. elif self._check_recursive_postload(context, path, self.join_depth):
  1510. return
  1511. if not self.parent.class_manager[self.key].impl.supports_population:
  1512. raise sa_exc.InvalidRequestError(
  1513. "'%s' does not support object "
  1514. "population - eager loading cannot be applied." % self
  1515. )
  1516. # a little dance here as the "path" is still something that only
  1517. # semi-tracks the exact series of things we are loading, still not
  1518. # telling us about with_polymorphic() and stuff like that when it's at
  1519. # the root.. the initial MapperEntity is more accurate for this case.
  1520. if len(path) == 1:
  1521. if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
  1522. return
  1523. elif not orm_util._entity_isa(path[-1], self.parent):
  1524. return
  1525. subq = self._setup_query_from_rowproc(
  1526. context,
  1527. query_entity,
  1528. path,
  1529. path[-1],
  1530. loadopt,
  1531. adapter,
  1532. )
  1533. if subq is None:
  1534. return
  1535. assert subq.session is None
  1536. path = path[self.parent_property]
  1537. local_cols = self.parent_property.local_columns
  1538. # cache the loaded collections in the context
  1539. # so that inheriting mappers don't re-load when they
  1540. # call upon create_row_processor again
  1541. collections = path.get(context.attributes, "collections")
  1542. if collections is None:
  1543. collections = self._SubqCollections(context, subq)
  1544. path.set(context.attributes, "collections", collections)
  1545. if adapter:
  1546. local_cols = [adapter.columns[c] for c in local_cols]
  1547. if self.uselist:
  1548. self._create_collection_loader(
  1549. context, result, collections, local_cols, populators
  1550. )
  1551. else:
  1552. self._create_scalar_loader(
  1553. context, result, collections, local_cols, populators
  1554. )
  1555. def _create_collection_loader(
  1556. self, context, result, collections, local_cols, populators
  1557. ):
  1558. tuple_getter = result._tuple_getter(local_cols)
  1559. def load_collection_from_subq(state, dict_, row):
  1560. collection = collections.get(tuple_getter(row), ())
  1561. state.get_impl(self.key).set_committed_value(
  1562. state, dict_, collection
  1563. )
  1564. def load_collection_from_subq_existing_row(state, dict_, row):
  1565. if self.key not in dict_:
  1566. load_collection_from_subq(state, dict_, row)
  1567. populators["new"].append((self.key, load_collection_from_subq))
  1568. populators["existing"].append(
  1569. (self.key, load_collection_from_subq_existing_row)
  1570. )
  1571. if context.invoke_all_eagers:
  1572. populators["eager"].append((self.key, collections.loader))
  1573. def _create_scalar_loader(
  1574. self, context, result, collections, local_cols, populators
  1575. ):
  1576. tuple_getter = result._tuple_getter(local_cols)
  1577. def load_scalar_from_subq(state, dict_, row):
  1578. collection = collections.get(tuple_getter(row), (None,))
  1579. if len(collection) > 1:
  1580. util.warn(
  1581. "Multiple rows returned with "
  1582. "uselist=False for eagerly-loaded attribute '%s' " % self
  1583. )
  1584. scalar = collection[0]
  1585. state.get_impl(self.key).set_committed_value(state, dict_, scalar)
  1586. def load_scalar_from_subq_existing_row(state, dict_, row):
  1587. if self.key not in dict_:
  1588. load_scalar_from_subq(state, dict_, row)
  1589. populators["new"].append((self.key, load_scalar_from_subq))
  1590. populators["existing"].append(
  1591. (self.key, load_scalar_from_subq_existing_row)
  1592. )
  1593. if context.invoke_all_eagers:
  1594. populators["eager"].append((self.key, collections.loader))
  1595. @log.class_logger
  1596. @relationships.RelationshipProperty.strategy_for(lazy="joined")
  1597. @relationships.RelationshipProperty.strategy_for(lazy=False)
  1598. class JoinedLoader(AbstractRelationshipLoader):
  1599. """Provide loading behavior for a :class:`.RelationshipProperty`
  1600. using joined eager loading.
  1601. """
  1602. __slots__ = "join_depth", "_aliased_class_pool"
  1603. def __init__(self, parent, strategy_key):
  1604. super(JoinedLoader, self).__init__(parent, strategy_key)
  1605. self.join_depth = self.parent_property.join_depth
  1606. self._aliased_class_pool = []
  1607. def init_class_attribute(self, mapper):
  1608. self.parent_property._get_strategy(
  1609. (("lazy", "select"),)
  1610. ).init_class_attribute(mapper)
  1611. def setup_query(
  1612. self,
  1613. compile_state,
  1614. query_entity,
  1615. path,
  1616. loadopt,
  1617. adapter,
  1618. column_collection=None,
  1619. parentmapper=None,
  1620. chained_from_outerjoin=False,
  1621. **kwargs
  1622. ):
  1623. """Add a left outer join to the statement that's being constructed."""
  1624. if not compile_state.compile_options._enable_eagerloads:
  1625. return
  1626. elif self.uselist:
  1627. compile_state.multi_row_eager_loaders = True
  1628. path = path[self.parent_property]
  1629. with_polymorphic = None
  1630. user_defined_adapter = (
  1631. self._init_user_defined_eager_proc(
  1632. loadopt, compile_state, compile_state.attributes
  1633. )
  1634. if loadopt
  1635. else False
  1636. )
  1637. if user_defined_adapter is not False:
  1638. (
  1639. clauses,
  1640. adapter,
  1641. add_to_collection,
  1642. ) = self._setup_query_on_user_defined_adapter(
  1643. compile_state,
  1644. query_entity,
  1645. path,
  1646. adapter,
  1647. user_defined_adapter,
  1648. )
  1649. else:
  1650. # if not via query option, check for
  1651. # a cycle
  1652. if not path.contains(compile_state.attributes, "loader"):
  1653. if self.join_depth:
  1654. if path.length / 2 > self.join_depth:
  1655. return
  1656. elif path.contains_mapper(self.mapper):
  1657. return
  1658. (
  1659. clauses,
  1660. adapter,
  1661. add_to_collection,
  1662. chained_from_outerjoin,
  1663. ) = self._generate_row_adapter(
  1664. compile_state,
  1665. query_entity,
  1666. path,
  1667. loadopt,
  1668. adapter,
  1669. column_collection,
  1670. parentmapper,
  1671. chained_from_outerjoin,
  1672. )
  1673. with_poly_entity = path.get(
  1674. compile_state.attributes, "path_with_polymorphic", None
  1675. )
  1676. if with_poly_entity is not None:
  1677. with_polymorphic = inspect(
  1678. with_poly_entity
  1679. ).with_polymorphic_mappers
  1680. else:
  1681. with_polymorphic = None
  1682. path = path[self.entity]
  1683. loading._setup_entity_query(
  1684. compile_state,
  1685. self.mapper,
  1686. query_entity,
  1687. path,
  1688. clauses,
  1689. add_to_collection,
  1690. with_polymorphic=with_polymorphic,
  1691. parentmapper=self.mapper,
  1692. chained_from_outerjoin=chained_from_outerjoin,
  1693. )
  1694. if with_poly_entity is not None and None in set(
  1695. compile_state.secondary_columns
  1696. ):
  1697. raise sa_exc.InvalidRequestError(
  1698. "Detected unaliased columns when generating joined "
  1699. "load. Make sure to use aliased=True or flat=True "
  1700. "when using joined loading with with_polymorphic()."
  1701. )
  1702. def _init_user_defined_eager_proc(
  1703. self, loadopt, compile_state, target_attributes
  1704. ):
  1705. # check if the opt applies at all
  1706. if "eager_from_alias" not in loadopt.local_opts:
  1707. # nope
  1708. return False
  1709. path = loadopt.path.parent
  1710. # the option applies. check if the "user_defined_eager_row_processor"
  1711. # has been built up.
  1712. adapter = path.get(
  1713. compile_state.attributes, "user_defined_eager_row_processor", False
  1714. )
  1715. if adapter is not False:
  1716. # just return it
  1717. return adapter
  1718. # otherwise figure it out.
  1719. alias = loadopt.local_opts["eager_from_alias"]
  1720. root_mapper, prop = path[-2:]
  1721. if alias is not None:
  1722. if isinstance(alias, str):
  1723. alias = prop.target.alias(alias)
  1724. adapter = sql_util.ColumnAdapter(
  1725. alias, equivalents=prop.mapper._equivalent_columns
  1726. )
  1727. else:
  1728. if path.contains(
  1729. compile_state.attributes, "path_with_polymorphic"
  1730. ):
  1731. with_poly_entity = path.get(
  1732. compile_state.attributes, "path_with_polymorphic"
  1733. )
  1734. adapter = orm_util.ORMAdapter(
  1735. with_poly_entity,
  1736. equivalents=prop.mapper._equivalent_columns,
  1737. )
  1738. else:
  1739. adapter = compile_state._polymorphic_adapters.get(
  1740. prop.mapper, None
  1741. )
  1742. path.set(
  1743. target_attributes,
  1744. "user_defined_eager_row_processor",
  1745. adapter,
  1746. )
  1747. return adapter
  1748. def _setup_query_on_user_defined_adapter(
  1749. self, context, entity, path, adapter, user_defined_adapter
  1750. ):
  1751. # apply some more wrapping to the "user defined adapter"
  1752. # if we are setting up the query for SQL render.
  1753. adapter = entity._get_entity_clauses(context)
  1754. if adapter and user_defined_adapter:
  1755. user_defined_adapter = user_defined_adapter.wrap(adapter)
  1756. path.set(
  1757. context.attributes,
  1758. "user_defined_eager_row_processor",
  1759. user_defined_adapter,
  1760. )
  1761. elif adapter:
  1762. user_defined_adapter = adapter
  1763. path.set(
  1764. context.attributes,
  1765. "user_defined_eager_row_processor",
  1766. user_defined_adapter,
  1767. )
  1768. add_to_collection = context.primary_columns
  1769. return user_defined_adapter, adapter, add_to_collection
  1770. def _gen_pooled_aliased_class(self, context):
  1771. # keep a local pool of AliasedClass objects that get re-used.
  1772. # we need one unique AliasedClass per query per appearance of our
  1773. # entity in the query.
  1774. if inspect(self.entity).is_aliased_class:
  1775. alt_selectable = inspect(self.entity).selectable
  1776. else:
  1777. alt_selectable = None
  1778. key = ("joinedloader_ac", self)
  1779. if key not in context.attributes:
  1780. context.attributes[key] = idx = 0
  1781. else:
  1782. context.attributes[key] = idx = context.attributes[key] + 1
  1783. if idx >= len(self._aliased_class_pool):
  1784. to_adapt = orm_util.AliasedClass(
  1785. self.mapper,
  1786. alias=alt_selectable._anonymous_fromclause(flat=True)
  1787. if alt_selectable is not None
  1788. else None,
  1789. flat=True,
  1790. use_mapper_path=True,
  1791. )
  1792. # load up the .columns collection on the Alias() before
  1793. # the object becomes shared among threads. this prevents
  1794. # races for column identities.
  1795. inspect(to_adapt).selectable.c
  1796. self._aliased_class_pool.append(to_adapt)
  1797. return self._aliased_class_pool[idx]
  1798. def _generate_row_adapter(
  1799. self,
  1800. compile_state,
  1801. entity,
  1802. path,
  1803. loadopt,
  1804. adapter,
  1805. column_collection,
  1806. parentmapper,
  1807. chained_from_outerjoin,
  1808. ):
  1809. with_poly_entity = path.get(
  1810. compile_state.attributes, "path_with_polymorphic", None
  1811. )
  1812. if with_poly_entity:
  1813. to_adapt = with_poly_entity
  1814. else:
  1815. to_adapt = self._gen_pooled_aliased_class(compile_state)
  1816. clauses = inspect(to_adapt)._memo(
  1817. ("joinedloader_ormadapter", self),
  1818. orm_util.ORMAdapter,
  1819. to_adapt,
  1820. equivalents=self.mapper._equivalent_columns,
  1821. adapt_required=True,
  1822. allow_label_resolve=False,
  1823. anonymize_labels=True,
  1824. )
  1825. assert clauses.aliased_class is not None
  1826. innerjoin = (
  1827. loadopt.local_opts.get("innerjoin", self.parent_property.innerjoin)
  1828. if loadopt is not None
  1829. else self.parent_property.innerjoin
  1830. )
  1831. if not innerjoin:
  1832. # if this is an outer join, all non-nested eager joins from
  1833. # this path must also be outer joins
  1834. chained_from_outerjoin = True
  1835. compile_state.create_eager_joins.append(
  1836. (
  1837. self._create_eager_join,
  1838. entity,
  1839. path,
  1840. adapter,
  1841. parentmapper,
  1842. clauses,
  1843. innerjoin,
  1844. chained_from_outerjoin,
  1845. loadopt._extra_criteria if loadopt else (),
  1846. )
  1847. )
  1848. add_to_collection = compile_state.secondary_columns
  1849. path.set(compile_state.attributes, "eager_row_processor", clauses)
  1850. return clauses, adapter, add_to_collection, chained_from_outerjoin
  1851. def _create_eager_join(
  1852. self,
  1853. compile_state,
  1854. query_entity,
  1855. path,
  1856. adapter,
  1857. parentmapper,
  1858. clauses,
  1859. innerjoin,
  1860. chained_from_outerjoin,
  1861. extra_criteria,
  1862. ):
  1863. if parentmapper is None:
  1864. localparent = query_entity.mapper
  1865. else:
  1866. localparent = parentmapper
  1867. # whether or not the Query will wrap the selectable in a subquery,
  1868. # and then attach eager load joins to that (i.e., in the case of
  1869. # LIMIT/OFFSET etc.)
  1870. should_nest_selectable = (
  1871. compile_state.multi_row_eager_loaders
  1872. and compile_state._should_nest_selectable
  1873. )
  1874. query_entity_key = None
  1875. if (
  1876. query_entity not in compile_state.eager_joins
  1877. and not should_nest_selectable
  1878. and compile_state.from_clauses
  1879. ):
  1880. indexes = sql_util.find_left_clause_that_matches_given(
  1881. compile_state.from_clauses, query_entity.selectable
  1882. )
  1883. if len(indexes) > 1:
  1884. # for the eager load case, I can't reproduce this right
  1885. # now. For query.join() I can.
  1886. raise sa_exc.InvalidRequestError(
  1887. "Can't identify which query entity in which to joined "
  1888. "eager load from. Please use an exact match when "
  1889. "specifying the join path."
  1890. )
  1891. if indexes:
  1892. clause = compile_state.from_clauses[indexes[0]]
  1893. # join to an existing FROM clause on the query.
  1894. # key it to its list index in the eager_joins dict.
  1895. # Query._compile_context will adapt as needed and
  1896. # append to the FROM clause of the select().
  1897. query_entity_key, default_towrap = indexes[0], clause
  1898. if query_entity_key is None:
  1899. query_entity_key, default_towrap = (
  1900. query_entity,
  1901. query_entity.selectable,
  1902. )
  1903. towrap = compile_state.eager_joins.setdefault(
  1904. query_entity_key, default_towrap
  1905. )
  1906. if adapter:
  1907. if getattr(adapter, "aliased_class", None):
  1908. # joining from an adapted entity. The adapted entity
  1909. # might be a "with_polymorphic", so resolve that to our
  1910. # specific mapper's entity before looking for our attribute
  1911. # name on it.
  1912. efm = inspect(adapter.aliased_class)._entity_for_mapper(
  1913. localparent
  1914. if localparent.isa(self.parent)
  1915. else self.parent
  1916. )
  1917. # look for our attribute on the adapted entity, else fall back
  1918. # to our straight property
  1919. onclause = getattr(efm.entity, self.key, self.parent_property)
  1920. else:
  1921. onclause = getattr(
  1922. orm_util.AliasedClass(
  1923. self.parent, adapter.selectable, use_mapper_path=True
  1924. ),
  1925. self.key,
  1926. self.parent_property,
  1927. )
  1928. else:
  1929. onclause = self.parent_property
  1930. assert clauses.aliased_class is not None
  1931. attach_on_outside = (
  1932. not chained_from_outerjoin
  1933. or not innerjoin
  1934. or innerjoin == "unnested"
  1935. or query_entity.entity_zero.represents_outer_join
  1936. )
  1937. extra_join_criteria = extra_criteria
  1938. additional_entity_criteria = compile_state.global_attributes.get(
  1939. ("additional_entity_criteria", self.mapper), ()
  1940. )
  1941. if additional_entity_criteria:
  1942. extra_join_criteria += tuple(
  1943. ae._resolve_where_criteria(self.mapper)
  1944. for ae in additional_entity_criteria
  1945. if ae.propagate_to_loaders
  1946. )
  1947. if attach_on_outside:
  1948. # this is the "classic" eager join case.
  1949. eagerjoin = orm_util._ORMJoin(
  1950. towrap,
  1951. clauses.aliased_class,
  1952. onclause,
  1953. isouter=not innerjoin
  1954. or query_entity.entity_zero.represents_outer_join
  1955. or (chained_from_outerjoin and isinstance(towrap, sql.Join)),
  1956. _left_memo=self.parent,
  1957. _right_memo=self.mapper,
  1958. _extra_criteria=extra_join_criteria,
  1959. )
  1960. else:
  1961. # all other cases are innerjoin=='nested' approach
  1962. eagerjoin = self._splice_nested_inner_join(
  1963. path, towrap, clauses, onclause, extra_join_criteria
  1964. )
  1965. compile_state.eager_joins[query_entity_key] = eagerjoin
  1966. # send a hint to the Query as to where it may "splice" this join
  1967. eagerjoin.stop_on = query_entity.selectable
  1968. if not parentmapper:
  1969. # for parentclause that is the non-eager end of the join,
  1970. # ensure all the parent cols in the primaryjoin are actually
  1971. # in the
  1972. # columns clause (i.e. are not deferred), so that aliasing applied
  1973. # by the Query propagates those columns outward.
  1974. # This has the effect
  1975. # of "undefering" those columns.
  1976. for col in sql_util._find_columns(
  1977. self.parent_property.primaryjoin
  1978. ):
  1979. if localparent.persist_selectable.c.contains_column(col):
  1980. if adapter:
  1981. col = adapter.columns[col]
  1982. compile_state.primary_columns.append(col)
  1983. if self.parent_property.order_by:
  1984. compile_state.eager_order_by += tuple(
  1985. (eagerjoin._target_adapter.copy_and_process)(
  1986. util.to_list(self.parent_property.order_by)
  1987. )
  1988. )
  1989. def _splice_nested_inner_join(
  1990. self, path, join_obj, clauses, onclause, extra_criteria, splicing=False
  1991. ):
  1992. if splicing is False:
  1993. # first call is always handed a join object
  1994. # from the outside
  1995. assert isinstance(join_obj, orm_util._ORMJoin)
  1996. elif isinstance(join_obj, sql.selectable.FromGrouping):
  1997. return self._splice_nested_inner_join(
  1998. path,
  1999. join_obj.element,
  2000. clauses,
  2001. onclause,
  2002. extra_criteria,
  2003. splicing,
  2004. )
  2005. elif not isinstance(join_obj, orm_util._ORMJoin):
  2006. if path[-2] is splicing:
  2007. return orm_util._ORMJoin(
  2008. join_obj,
  2009. clauses.aliased_class,
  2010. onclause,
  2011. isouter=False,
  2012. _left_memo=splicing,
  2013. _right_memo=path[-1].mapper,
  2014. _extra_criteria=extra_criteria,
  2015. )
  2016. else:
  2017. # only here if splicing == True
  2018. return None
  2019. target_join = self._splice_nested_inner_join(
  2020. path,
  2021. join_obj.right,
  2022. clauses,
  2023. onclause,
  2024. extra_criteria,
  2025. join_obj._right_memo,
  2026. )
  2027. if target_join is None:
  2028. right_splice = False
  2029. target_join = self._splice_nested_inner_join(
  2030. path,
  2031. join_obj.left,
  2032. clauses,
  2033. onclause,
  2034. extra_criteria,
  2035. join_obj._left_memo,
  2036. )
  2037. if target_join is None:
  2038. # should only return None when recursively called,
  2039. # e.g. splicing==True
  2040. assert (
  2041. splicing is not False
  2042. ), "assertion failed attempting to produce joined eager loads"
  2043. return None
  2044. else:
  2045. right_splice = True
  2046. if right_splice:
  2047. # for a right splice, attempt to flatten out
  2048. # a JOIN b JOIN c JOIN .. to avoid needless
  2049. # parenthesis nesting
  2050. if not join_obj.isouter and not target_join.isouter:
  2051. eagerjoin = join_obj._splice_into_center(target_join)
  2052. else:
  2053. eagerjoin = orm_util._ORMJoin(
  2054. join_obj.left,
  2055. target_join,
  2056. join_obj.onclause,
  2057. isouter=join_obj.isouter,
  2058. _left_memo=join_obj._left_memo,
  2059. )
  2060. else:
  2061. eagerjoin = orm_util._ORMJoin(
  2062. target_join,
  2063. join_obj.right,
  2064. join_obj.onclause,
  2065. isouter=join_obj.isouter,
  2066. _right_memo=join_obj._right_memo,
  2067. )
  2068. eagerjoin._target_adapter = target_join._target_adapter
  2069. return eagerjoin
  2070. def _create_eager_adapter(self, context, result, adapter, path, loadopt):
  2071. compile_state = context.compile_state
  2072. user_defined_adapter = (
  2073. self._init_user_defined_eager_proc(
  2074. loadopt, compile_state, context.attributes
  2075. )
  2076. if loadopt
  2077. else False
  2078. )
  2079. if user_defined_adapter is not False:
  2080. decorator = user_defined_adapter
  2081. # user defined eagerloads are part of the "primary"
  2082. # portion of the load.
  2083. # the adapters applied to the Query should be honored.
  2084. if compile_state.compound_eager_adapter and decorator:
  2085. decorator = decorator.wrap(
  2086. compile_state.compound_eager_adapter
  2087. )
  2088. elif compile_state.compound_eager_adapter:
  2089. decorator = compile_state.compound_eager_adapter
  2090. else:
  2091. decorator = path.get(
  2092. compile_state.attributes, "eager_row_processor"
  2093. )
  2094. if decorator is None:
  2095. return False
  2096. if self.mapper._result_has_identity_key(result, decorator):
  2097. return decorator
  2098. else:
  2099. # no identity key - don't return a row
  2100. # processor, will cause a degrade to lazy
  2101. return False
  2102. def create_row_processor(
  2103. self,
  2104. context,
  2105. query_entity,
  2106. path,
  2107. loadopt,
  2108. mapper,
  2109. result,
  2110. adapter,
  2111. populators,
  2112. ):
  2113. if not self.parent.class_manager[self.key].impl.supports_population:
  2114. raise sa_exc.InvalidRequestError(
  2115. "'%s' does not support object "
  2116. "population - eager loading cannot be applied." % self
  2117. )
  2118. if self.uselist:
  2119. context.loaders_require_uniquing = True
  2120. our_path = path[self.parent_property]
  2121. eager_adapter = self._create_eager_adapter(
  2122. context, result, adapter, our_path, loadopt
  2123. )
  2124. if eager_adapter is not False:
  2125. key = self.key
  2126. _instance = loading._instance_processor(
  2127. query_entity,
  2128. self.mapper,
  2129. context,
  2130. result,
  2131. our_path[self.entity],
  2132. eager_adapter,
  2133. )
  2134. if not self.uselist:
  2135. self._create_scalar_loader(context, key, _instance, populators)
  2136. else:
  2137. self._create_collection_loader(
  2138. context, key, _instance, populators
  2139. )
  2140. else:
  2141. self.parent_property._get_strategy(
  2142. (("lazy", "select"),)
  2143. ).create_row_processor(
  2144. context,
  2145. query_entity,
  2146. path,
  2147. loadopt,
  2148. mapper,
  2149. result,
  2150. adapter,
  2151. populators,
  2152. )
  2153. def _create_collection_loader(self, context, key, _instance, populators):
  2154. def load_collection_from_joined_new_row(state, dict_, row):
  2155. # note this must unconditionally clear out any existing collection.
  2156. # an existing collection would be present only in the case of
  2157. # populate_existing().
  2158. collection = attributes.init_state_collection(state, dict_, key)
  2159. result_list = util.UniqueAppender(
  2160. collection, "append_without_event"
  2161. )
  2162. context.attributes[(state, key)] = result_list
  2163. inst = _instance(row)
  2164. if inst is not None:
  2165. result_list.append(inst)
  2166. def load_collection_from_joined_existing_row(state, dict_, row):
  2167. if (state, key) in context.attributes:
  2168. result_list = context.attributes[(state, key)]
  2169. else:
  2170. # appender_key can be absent from context.attributes
  2171. # with isnew=False when self-referential eager loading
  2172. # is used; the same instance may be present in two
  2173. # distinct sets of result columns
  2174. collection = attributes.init_state_collection(
  2175. state, dict_, key
  2176. )
  2177. result_list = util.UniqueAppender(
  2178. collection, "append_without_event"
  2179. )
  2180. context.attributes[(state, key)] = result_list
  2181. inst = _instance(row)
  2182. if inst is not None:
  2183. result_list.append(inst)
  2184. def load_collection_from_joined_exec(state, dict_, row):
  2185. _instance(row)
  2186. populators["new"].append(
  2187. (self.key, load_collection_from_joined_new_row)
  2188. )
  2189. populators["existing"].append(
  2190. (self.key, load_collection_from_joined_existing_row)
  2191. )
  2192. if context.invoke_all_eagers:
  2193. populators["eager"].append(
  2194. (self.key, load_collection_from_joined_exec)
  2195. )
  2196. def _create_scalar_loader(self, context, key, _instance, populators):
  2197. def load_scalar_from_joined_new_row(state, dict_, row):
  2198. # set a scalar object instance directly on the parent
  2199. # object, bypassing InstrumentedAttribute event handlers.
  2200. dict_[key] = _instance(row)
  2201. def load_scalar_from_joined_existing_row(state, dict_, row):
  2202. # call _instance on the row, even though the object has
  2203. # been created, so that we further descend into properties
  2204. existing = _instance(row)
  2205. # conflicting value already loaded, this shouldn't happen
  2206. if key in dict_:
  2207. if existing is not dict_[key]:
  2208. util.warn(
  2209. "Multiple rows returned with "
  2210. "uselist=False for eagerly-loaded attribute '%s' "
  2211. % self
  2212. )
  2213. else:
  2214. # this case is when one row has multiple loads of the
  2215. # same entity (e.g. via aliasing), one has an attribute
  2216. # that the other doesn't.
  2217. dict_[key] = existing
  2218. def load_scalar_from_joined_exec(state, dict_, row):
  2219. _instance(row)
  2220. populators["new"].append((self.key, load_scalar_from_joined_new_row))
  2221. populators["existing"].append(
  2222. (self.key, load_scalar_from_joined_existing_row)
  2223. )
  2224. if context.invoke_all_eagers:
  2225. populators["eager"].append(
  2226. (self.key, load_scalar_from_joined_exec)
  2227. )
  2228. @log.class_logger
  2229. @relationships.RelationshipProperty.strategy_for(lazy="selectin")
  2230. class SelectInLoader(PostLoader, util.MemoizedSlots):
  2231. __slots__ = (
  2232. "join_depth",
  2233. "omit_join",
  2234. "_parent_alias",
  2235. "_query_info",
  2236. "_fallback_query_info",
  2237. "_lambda_cache",
  2238. )
  2239. query_info = collections.namedtuple(
  2240. "queryinfo",
  2241. [
  2242. "load_only_child",
  2243. "load_with_join",
  2244. "in_expr",
  2245. "pk_cols",
  2246. "zero_idx",
  2247. "child_lookup_cols",
  2248. ],
  2249. )
  2250. _chunksize = 500
  2251. def __init__(self, parent, strategy_key):
  2252. super(SelectInLoader, self).__init__(parent, strategy_key)
  2253. self.join_depth = self.parent_property.join_depth
  2254. is_m2o = self.parent_property.direction is interfaces.MANYTOONE
  2255. if self.parent_property.omit_join is not None:
  2256. self.omit_join = self.parent_property.omit_join
  2257. else:
  2258. lazyloader = self.parent_property._get_strategy(
  2259. (("lazy", "select"),)
  2260. )
  2261. if is_m2o:
  2262. self.omit_join = lazyloader.use_get
  2263. else:
  2264. self.omit_join = self.parent._get_clause[0].compare(
  2265. lazyloader._rev_lazywhere,
  2266. use_proxies=True,
  2267. compare_keys=False,
  2268. equivalents=self.parent._equivalent_columns,
  2269. )
  2270. if self.omit_join:
  2271. if is_m2o:
  2272. self._query_info = self._init_for_omit_join_m2o()
  2273. self._fallback_query_info = self._init_for_join()
  2274. else:
  2275. self._query_info = self._init_for_omit_join()
  2276. else:
  2277. self._query_info = self._init_for_join()
  2278. def _init_for_omit_join(self):
  2279. pk_to_fk = dict(
  2280. self.parent_property._join_condition.local_remote_pairs
  2281. )
  2282. pk_to_fk.update(
  2283. (equiv, pk_to_fk[k])
  2284. for k in list(pk_to_fk)
  2285. for equiv in self.parent._equivalent_columns.get(k, ())
  2286. )
  2287. pk_cols = fk_cols = [
  2288. pk_to_fk[col] for col in self.parent.primary_key if col in pk_to_fk
  2289. ]
  2290. if len(fk_cols) > 1:
  2291. in_expr = sql.tuple_(*fk_cols)
  2292. zero_idx = False
  2293. else:
  2294. in_expr = fk_cols[0]
  2295. zero_idx = True
  2296. return self.query_info(False, False, in_expr, pk_cols, zero_idx, None)
  2297. def _init_for_omit_join_m2o(self):
  2298. pk_cols = self.mapper.primary_key
  2299. if len(pk_cols) > 1:
  2300. in_expr = sql.tuple_(*pk_cols)
  2301. zero_idx = False
  2302. else:
  2303. in_expr = pk_cols[0]
  2304. zero_idx = True
  2305. lazyloader = self.parent_property._get_strategy((("lazy", "select"),))
  2306. lookup_cols = [lazyloader._equated_columns[pk] for pk in pk_cols]
  2307. return self.query_info(
  2308. True, False, in_expr, pk_cols, zero_idx, lookup_cols
  2309. )
  2310. def _init_for_join(self):
  2311. self._parent_alias = aliased(self.parent.class_)
  2312. pa_insp = inspect(self._parent_alias)
  2313. pk_cols = [
  2314. pa_insp._adapt_element(col) for col in self.parent.primary_key
  2315. ]
  2316. if len(pk_cols) > 1:
  2317. in_expr = sql.tuple_(*pk_cols)
  2318. zero_idx = False
  2319. else:
  2320. in_expr = pk_cols[0]
  2321. zero_idx = True
  2322. return self.query_info(False, True, in_expr, pk_cols, zero_idx, None)
  2323. def init_class_attribute(self, mapper):
  2324. self.parent_property._get_strategy(
  2325. (("lazy", "select"),)
  2326. ).init_class_attribute(mapper)
  2327. def _memoized_attr__lambda_cache(self):
  2328. # cache is per lazy loader, and is used for caching of
  2329. # sqlalchemy.sql.lambdas.AnalyzedCode and
  2330. # sqlalchemy.sql.lambdas.AnalyzedFunction objects which are generated
  2331. # from the StatementLambda used.
  2332. return util.LRUCache(30)
  2333. def create_row_processor(
  2334. self,
  2335. context,
  2336. query_entity,
  2337. path,
  2338. loadopt,
  2339. mapper,
  2340. result,
  2341. adapter,
  2342. populators,
  2343. ):
  2344. if context.refresh_state:
  2345. return self._immediateload_create_row_processor(
  2346. context,
  2347. query_entity,
  2348. path,
  2349. loadopt,
  2350. mapper,
  2351. result,
  2352. adapter,
  2353. populators,
  2354. )
  2355. elif self._check_recursive_postload(context, path, self.join_depth):
  2356. return
  2357. if not self.parent.class_manager[self.key].impl.supports_population:
  2358. raise sa_exc.InvalidRequestError(
  2359. "'%s' does not support object "
  2360. "population - eager loading cannot be applied." % self
  2361. )
  2362. # a little dance here as the "path" is still something that only
  2363. # semi-tracks the exact series of things we are loading, still not
  2364. # telling us about with_polymorphic() and stuff like that when it's at
  2365. # the root.. the initial MapperEntity is more accurate for this case.
  2366. if len(path) == 1:
  2367. if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
  2368. return
  2369. elif not orm_util._entity_isa(path[-1], self.parent):
  2370. return
  2371. selectin_path = (
  2372. context.compile_state.current_path or orm_util.PathRegistry.root
  2373. ) + path
  2374. path_w_prop = path[self.parent_property]
  2375. # build up a path indicating the path from the leftmost
  2376. # entity to the thing we're subquery loading.
  2377. with_poly_entity = path_w_prop.get(
  2378. context.attributes, "path_with_polymorphic", None
  2379. )
  2380. if with_poly_entity is not None:
  2381. effective_entity = inspect(with_poly_entity)
  2382. else:
  2383. effective_entity = self.entity
  2384. loading.PostLoad.callable_for_path(
  2385. context,
  2386. selectin_path,
  2387. self.parent,
  2388. self.parent_property,
  2389. self._load_for_path,
  2390. effective_entity,
  2391. loadopt,
  2392. )
  2393. def _load_for_path(
  2394. self, context, path, states, load_only, effective_entity, loadopt
  2395. ):
  2396. if load_only and self.key not in load_only:
  2397. return
  2398. query_info = self._query_info
  2399. if query_info.load_only_child:
  2400. our_states = collections.defaultdict(list)
  2401. none_states = []
  2402. mapper = self.parent
  2403. for state, overwrite in states:
  2404. state_dict = state.dict
  2405. related_ident = tuple(
  2406. mapper._get_state_attr_by_column(
  2407. state,
  2408. state_dict,
  2409. lk,
  2410. passive=attributes.PASSIVE_NO_FETCH,
  2411. )
  2412. for lk in query_info.child_lookup_cols
  2413. )
  2414. # if the loaded parent objects do not have the foreign key
  2415. # to the related item loaded, then degrade into the joined
  2416. # version of selectinload
  2417. if attributes.PASSIVE_NO_RESULT in related_ident:
  2418. query_info = self._fallback_query_info
  2419. break
  2420. # organize states into lists keyed to particular foreign
  2421. # key values.
  2422. if None not in related_ident:
  2423. our_states[related_ident].append(
  2424. (state, state_dict, overwrite)
  2425. )
  2426. else:
  2427. # For FK values that have None, add them to a
  2428. # separate collection that will be populated separately
  2429. none_states.append((state, state_dict, overwrite))
  2430. # note the above conditional may have changed query_info
  2431. if not query_info.load_only_child:
  2432. our_states = [
  2433. (state.key[1], state, state.dict, overwrite)
  2434. for state, overwrite in states
  2435. ]
  2436. pk_cols = query_info.pk_cols
  2437. in_expr = query_info.in_expr
  2438. if not query_info.load_with_join:
  2439. # in "omit join" mode, the primary key column and the
  2440. # "in" expression are in terms of the related entity. So
  2441. # if the related entity is polymorphic or otherwise aliased,
  2442. # we need to adapt our "pk_cols" and "in_expr" to that
  2443. # entity. in non-"omit join" mode, these are against the
  2444. # parent entity and do not need adaption.
  2445. if effective_entity.is_aliased_class:
  2446. pk_cols = [
  2447. effective_entity._adapt_element(col) for col in pk_cols
  2448. ]
  2449. in_expr = effective_entity._adapt_element(in_expr)
  2450. q = sql.lambda_stmt(
  2451. lambda: sql.select(
  2452. orm_util.Bundle("pk", *pk_cols), effective_entity
  2453. )
  2454. .set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL)
  2455. ._set_compile_options(ORMCompileState.default_compile_options)
  2456. ._set_propagate_attrs(
  2457. {
  2458. "compile_state_plugin": "orm",
  2459. "plugin_subject": effective_entity,
  2460. }
  2461. ),
  2462. lambda_cache=self._lambda_cache,
  2463. global_track_bound_values=False,
  2464. track_on=(self, effective_entity) + (tuple(pk_cols),),
  2465. )
  2466. if not self.parent_property.bake_queries:
  2467. q = q.spoil()
  2468. if not query_info.load_with_join:
  2469. # the Bundle we have in the "omit_join" case is against raw, non
  2470. # annotated columns, so to ensure the Query knows its primary
  2471. # entity, we add it explicitly. If we made the Bundle against
  2472. # annotated columns, we hit a performance issue in this specific
  2473. # case, which is detailed in issue #4347.
  2474. q = q.add_criteria(lambda q: q.select_from(effective_entity))
  2475. else:
  2476. # in the non-omit_join case, the Bundle is against the annotated/
  2477. # mapped column of the parent entity, but the #4347 issue does not
  2478. # occur in this case.
  2479. q = q.add_criteria(
  2480. lambda q: q.select_from(self._parent_alias).join(
  2481. getattr(
  2482. self._parent_alias, self.parent_property.key
  2483. ).of_type(effective_entity)
  2484. ),
  2485. track_on=[self],
  2486. )
  2487. q = q.add_criteria(
  2488. lambda q: q.filter(in_expr.in_(sql.bindparam("primary_keys")))
  2489. )
  2490. # a test which exercises what these comments talk about is
  2491. # test_selectin_relations.py -> test_twolevel_selectin_w_polymorphic
  2492. #
  2493. # effective_entity above is given to us in terms of the cached
  2494. # statement, namely this one:
  2495. orig_query = context.compile_state.select_statement
  2496. # the actual statement that was requested is this one:
  2497. # context_query = context.query
  2498. #
  2499. # that's not the cached one, however. So while it is of the identical
  2500. # structure, if it has entities like AliasedInsp, which we get from
  2501. # aliased() or with_polymorphic(), the AliasedInsp will likely be a
  2502. # different object identity each time, and will not match up
  2503. # hashing-wise to the corresponding AliasedInsp that's in the
  2504. # cached query, meaning it won't match on paths and loader lookups
  2505. # and loaders like this one will be skipped if it is used in options.
  2506. #
  2507. # Now we want to transfer loader options from the parent query to the
  2508. # "selectinload" query we're about to run. Which query do we transfer
  2509. # the options from? We use the cached query, because the options in
  2510. # that query will be in terms of the effective entity we were just
  2511. # handed.
  2512. #
  2513. # But now the selectinload query we are running is *also*
  2514. # cached. What if it's cached and running from some previous iteration
  2515. # of that AliasedInsp? Well in that case it will also use the previous
  2516. # iteration of the loader options. If the query expires and
  2517. # gets generated again, it will be handed the current effective_entity
  2518. # and the current _with_options, again in terms of whatever
  2519. # compile_state.select_statement happens to be right now, so the
  2520. # query will still be internally consistent and loader callables
  2521. # will be correctly invoked.
  2522. effective_path = path[self.parent_property]
  2523. options = orig_query._with_options
  2524. if loadopt and loadopt._extra_criteria:
  2525. options += (
  2526. orm_util.LoaderCriteriaOption(
  2527. effective_entity,
  2528. loadopt._generate_extra_criteria(context),
  2529. ),
  2530. )
  2531. q = q.add_criteria(
  2532. lambda q: q.options(*options)._update_compile_options(
  2533. {"_current_path": effective_path}
  2534. )
  2535. )
  2536. if context.populate_existing:
  2537. q = q.add_criteria(
  2538. lambda q: q.execution_options(populate_existing=True)
  2539. )
  2540. if self.parent_property.order_by:
  2541. if not query_info.load_with_join:
  2542. eager_order_by = self.parent_property.order_by
  2543. if effective_entity.is_aliased_class:
  2544. eager_order_by = [
  2545. effective_entity._adapt_element(elem)
  2546. for elem in eager_order_by
  2547. ]
  2548. q = q.add_criteria(lambda q: q.order_by(*eager_order_by))
  2549. else:
  2550. def _setup_outermost_orderby(compile_context):
  2551. compile_context.eager_order_by += tuple(
  2552. util.to_list(self.parent_property.order_by)
  2553. )
  2554. q = q.add_criteria(
  2555. lambda q: q._add_context_option(
  2556. _setup_outermost_orderby, self.parent_property
  2557. ),
  2558. track_on=[self],
  2559. )
  2560. if query_info.load_only_child:
  2561. self._load_via_child(
  2562. our_states, none_states, query_info, q, context
  2563. )
  2564. else:
  2565. self._load_via_parent(our_states, query_info, q, context)
  2566. def _load_via_child(self, our_states, none_states, query_info, q, context):
  2567. uselist = self.uselist
  2568. # this sort is really for the benefit of the unit tests
  2569. our_keys = sorted(our_states)
  2570. while our_keys:
  2571. chunk = our_keys[0 : self._chunksize]
  2572. our_keys = our_keys[self._chunksize :]
  2573. data = {
  2574. k: v
  2575. for k, v in context.session.execute(
  2576. q,
  2577. params={
  2578. "primary_keys": [
  2579. key[0] if query_info.zero_idx else key
  2580. for key in chunk
  2581. ]
  2582. },
  2583. ).unique()
  2584. }
  2585. for key in chunk:
  2586. # for a real foreign key and no concurrent changes to the
  2587. # DB while running this method, "key" is always present in
  2588. # data. However, for primaryjoins without real foreign keys
  2589. # a non-None primaryjoin condition may still refer to no
  2590. # related object.
  2591. related_obj = data.get(key, None)
  2592. for state, dict_, overwrite in our_states[key]:
  2593. if not overwrite and self.key in dict_:
  2594. continue
  2595. state.get_impl(self.key).set_committed_value(
  2596. state,
  2597. dict_,
  2598. related_obj if not uselist else [related_obj],
  2599. )
  2600. # populate none states with empty value / collection
  2601. for state, dict_, overwrite in none_states:
  2602. if not overwrite and self.key in dict_:
  2603. continue
  2604. # note it's OK if this is a uselist=True attribute, the empty
  2605. # collection will be populated
  2606. state.get_impl(self.key).set_committed_value(state, dict_, None)
  2607. def _load_via_parent(self, our_states, query_info, q, context):
  2608. uselist = self.uselist
  2609. _empty_result = () if uselist else None
  2610. while our_states:
  2611. chunk = our_states[0 : self._chunksize]
  2612. our_states = our_states[self._chunksize :]
  2613. primary_keys = [
  2614. key[0] if query_info.zero_idx else key
  2615. for key, state, state_dict, overwrite in chunk
  2616. ]
  2617. data = collections.defaultdict(list)
  2618. for k, v in itertools.groupby(
  2619. context.session.execute(
  2620. q, params={"primary_keys": primary_keys}
  2621. ).unique(),
  2622. lambda x: x[0],
  2623. ):
  2624. data[k].extend(vv[1] for vv in v)
  2625. for key, state, state_dict, overwrite in chunk:
  2626. if not overwrite and self.key in state_dict:
  2627. continue
  2628. collection = data.get(key, _empty_result)
  2629. if not uselist and collection:
  2630. if len(collection) > 1:
  2631. util.warn(
  2632. "Multiple rows returned with "
  2633. "uselist=False for eagerly-loaded "
  2634. "attribute '%s' " % self
  2635. )
  2636. state.get_impl(self.key).set_committed_value(
  2637. state, state_dict, collection[0]
  2638. )
  2639. else:
  2640. # note that empty tuple set on uselist=False sets the
  2641. # value to None
  2642. state.get_impl(self.key).set_committed_value(
  2643. state, state_dict, collection
  2644. )
  2645. def single_parent_validator(desc, prop):
  2646. def _do_check(state, value, oldvalue, initiator):
  2647. if value is not None and initiator.key == prop.key:
  2648. hasparent = initiator.hasparent(attributes.instance_state(value))
  2649. if hasparent and oldvalue is not value:
  2650. raise sa_exc.InvalidRequestError(
  2651. "Instance %s is already associated with an instance "
  2652. "of %s via its %s attribute, and is only allowed a "
  2653. "single parent."
  2654. % (orm_util.instance_str(value), state.class_, prop),
  2655. code="bbf1",
  2656. )
  2657. return value
  2658. def append(state, value, initiator):
  2659. return _do_check(state, value, None, initiator)
  2660. def set_(state, value, oldvalue, initiator):
  2661. return _do_check(state, value, oldvalue, initiator)
  2662. event.listen(
  2663. desc, "append", append, raw=True, retval=True, active_history=True
  2664. )
  2665. event.listen(desc, "set", set_, raw=True, retval=True, active_history=True)