Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# orm/loading.py 

2# Copyright (C) 2005-2020 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 

8"""private module containing functions used to convert database 

9rows into object instances and associated state. 

10 

11the functions here are called primarily by Query, Mapper, 

12as well as some of the attribute loading strategies. 

13 

14""" 

15from __future__ import absolute_import 

16 

17import collections 

18 

19from . import attributes 

20from . import exc as orm_exc 

21from . import path_registry 

22from . import strategy_options 

23from .base import _DEFER_FOR_STATE 

24from .base import _SET_DEFERRED_EXPIRED 

25from .util import _none_set 

26from .util import state_str 

27from .. import exc as sa_exc 

28from .. import util 

29from ..sql import util as sql_util 

30 

31 

32_new_runid = util.counter() 

33 

34 

35def instances(query, cursor, context): 

36 """Return an ORM result as an iterator.""" 

37 

38 context.runid = _new_runid() 

39 context.post_load_paths = {} 

40 

41 filtered = query._has_mapper_entities 

42 

43 single_entity = query.is_single_entity 

44 

45 if filtered: 

46 if single_entity: 

47 filter_fn = id 

48 else: 

49 

50 def filter_fn(row): 

51 return tuple( 

52 id(item) if ent.use_id_for_hash else item 

53 for ent, item in zip(query._entities, row) 

54 ) 

55 

56 try: 

57 (process, labels) = list( 

58 zip( 

59 *[ 

60 query_entity.row_processor(query, context, cursor) 

61 for query_entity in query._entities 

62 ] 

63 ) 

64 ) 

65 

66 if not single_entity: 

67 keyed_tuple = util.lightweight_named_tuple("result", labels) 

68 

69 while True: 

70 context.partials = {} 

71 

72 if query._yield_per: 

73 fetch = cursor.fetchmany(query._yield_per) 

74 if not fetch: 

75 break 

76 else: 

77 fetch = cursor.fetchall() 

78 

79 if single_entity: 

80 proc = process[0] 

81 rows = [proc(row) for row in fetch] 

82 else: 

83 rows = [ 

84 keyed_tuple([proc(row) for proc in process]) 

85 for row in fetch 

86 ] 

87 

88 for path, post_load in context.post_load_paths.items(): 

89 post_load.invoke(context, path) 

90 

91 if filtered: 

92 rows = util.unique_list(rows, filter_fn) 

93 

94 for row in rows: 

95 yield row 

96 

97 if not query._yield_per: 

98 break 

99 except Exception: 

100 with util.safe_reraise(): 

101 cursor.close() 

102 

103 

104@util.dependencies("sqlalchemy.orm.query") 

105def merge_result(querylib, query, iterator, load=True): 

106 """Merge a result into this :class:`_query.Query` object's Session.""" 

107 

108 session = query.session 

109 if load: 

110 # flush current contents if we expect to load data 

111 session._autoflush() 

112 

113 autoflush = session.autoflush 

114 try: 

115 session.autoflush = False 

116 single_entity = len(query._entities) == 1 

117 if single_entity: 

118 if isinstance(query._entities[0], querylib._MapperEntity): 

119 result = [ 

120 session._merge( 

121 attributes.instance_state(instance), 

122 attributes.instance_dict(instance), 

123 load=load, 

124 _recursive={}, 

125 _resolve_conflict_map={}, 

126 ) 

127 for instance in iterator 

128 ] 

129 else: 

130 result = list(iterator) 

131 else: 

132 mapped_entities = [ 

133 i 

134 for i, e in enumerate(query._entities) 

135 if isinstance(e, querylib._MapperEntity) 

136 ] 

137 result = [] 

138 keys = [ent._label_name for ent in query._entities] 

139 keyed_tuple = util.lightweight_named_tuple("result", keys) 

140 for row in iterator: 

141 newrow = list(row) 

142 for i in mapped_entities: 

143 if newrow[i] is not None: 

144 newrow[i] = session._merge( 

145 attributes.instance_state(newrow[i]), 

146 attributes.instance_dict(newrow[i]), 

147 load=load, 

148 _recursive={}, 

149 _resolve_conflict_map={}, 

150 ) 

151 result.append(keyed_tuple(newrow)) 

152 

153 return iter(result) 

154 finally: 

155 session.autoflush = autoflush 

156 

157 

158def get_from_identity(session, mapper, key, passive): 

159 """Look up the given key in the given session's identity map, 

160 check the object for expired state if found. 

161 

162 """ 

163 instance = session.identity_map.get(key) 

164 if instance is not None: 

165 

166 state = attributes.instance_state(instance) 

167 

168 if mapper.inherits and not state.mapper.isa(mapper): 

169 return attributes.PASSIVE_CLASS_MISMATCH 

170 

171 # expired - ensure it still exists 

172 if state.expired: 

173 if not passive & attributes.SQL_OK: 

174 # TODO: no coverage here 

175 return attributes.PASSIVE_NO_RESULT 

176 elif not passive & attributes.RELATED_OBJECT_OK: 

177 # this mode is used within a flush and the instance's 

178 # expired state will be checked soon enough, if necessary 

179 return instance 

180 try: 

181 state._load_expired(state, passive) 

182 except orm_exc.ObjectDeletedError: 

183 session._remove_newly_deleted([state]) 

184 return None 

185 return instance 

186 else: 

187 return None 

188 

189 

190def load_on_ident( 

191 query, key, refresh_state=None, with_for_update=None, only_load_props=None 

192): 

193 """Load the given identity key from the database.""" 

194 

195 if key is not None: 

196 ident = key[1] 

197 identity_token = key[2] 

198 else: 

199 ident = identity_token = None 

200 

201 return load_on_pk_identity( 

202 query, 

203 ident, 

204 refresh_state=refresh_state, 

205 with_for_update=with_for_update, 

206 only_load_props=only_load_props, 

207 identity_token=identity_token, 

208 ) 

209 

210 

211def load_on_pk_identity( 

212 query, 

213 primary_key_identity, 

214 refresh_state=None, 

215 with_for_update=None, 

216 only_load_props=None, 

217 identity_token=None, 

218): 

219 

220 """Load the given primary key identity from the database.""" 

221 

222 if refresh_state is None: 

223 q = query._clone() 

224 q._get_condition() 

225 else: 

226 q = query._clone() 

227 

228 if primary_key_identity is not None: 

229 mapper = query._mapper_zero() 

230 

231 (_get_clause, _get_params) = mapper._get_clause 

232 

233 # None present in ident - turn those comparisons 

234 # into "IS NULL" 

235 if None in primary_key_identity: 

236 nones = set( 

237 [ 

238 _get_params[col].key 

239 for col, value in zip( 

240 mapper.primary_key, primary_key_identity 

241 ) 

242 if value is None 

243 ] 

244 ) 

245 _get_clause = sql_util.adapt_criterion_to_null(_get_clause, nones) 

246 

247 if len(nones) == len(primary_key_identity): 

248 util.warn( 

249 "fully NULL primary key identity cannot load any " 

250 "object. This condition may raise an error in a future " 

251 "release." 

252 ) 

253 _get_clause = q._adapt_clause(_get_clause, True, False) 

254 q._criterion = _get_clause 

255 

256 params = dict( 

257 [ 

258 (_get_params[primary_key].key, id_val) 

259 for id_val, primary_key in zip( 

260 primary_key_identity, mapper.primary_key 

261 ) 

262 ] 

263 ) 

264 

265 q._params = params 

266 

267 # with_for_update needs to be query.LockmodeArg() 

268 if with_for_update is not None: 

269 version_check = True 

270 q._for_update_arg = with_for_update 

271 elif query._for_update_arg is not None: 

272 version_check = True 

273 q._for_update_arg = query._for_update_arg 

274 else: 

275 version_check = False 

276 

277 q._get_options( 

278 populate_existing=bool(refresh_state), 

279 version_check=version_check, 

280 only_load_props=only_load_props, 

281 refresh_state=refresh_state, 

282 identity_token=identity_token, 

283 ) 

284 q._order_by = None 

285 

286 try: 

287 return q.one() 

288 except orm_exc.NoResultFound: 

289 return None 

290 

291 

292def _setup_entity_query( 

293 context, 

294 mapper, 

295 query_entity, 

296 path, 

297 adapter, 

298 column_collection, 

299 with_polymorphic=None, 

300 only_load_props=None, 

301 polymorphic_discriminator=None, 

302 **kw 

303): 

304 

305 if with_polymorphic: 

306 poly_properties = mapper._iterate_polymorphic_properties( 

307 with_polymorphic 

308 ) 

309 else: 

310 poly_properties = mapper._polymorphic_properties 

311 

312 quick_populators = {} 

313 

314 path.set(context.attributes, "memoized_setups", quick_populators) 

315 

316 for value in poly_properties: 

317 if only_load_props and value.key not in only_load_props: 

318 continue 

319 value.setup( 

320 context, 

321 query_entity, 

322 path, 

323 adapter, 

324 only_load_props=only_load_props, 

325 column_collection=column_collection, 

326 memoized_populators=quick_populators, 

327 **kw 

328 ) 

329 

330 if ( 

331 polymorphic_discriminator is not None 

332 and polymorphic_discriminator is not mapper.polymorphic_on 

333 ): 

334 

335 if adapter: 

336 pd = adapter.columns[polymorphic_discriminator] 

337 else: 

338 pd = polymorphic_discriminator 

339 column_collection.append(pd) 

340 

341 

342def _warn_for_runid_changed(state): 

343 util.warn( 

344 "Loading context for %s has changed within a load/refresh " 

345 "handler, suggesting a row refresh operation took place. If this " 

346 "event handler is expected to be " 

347 "emitting row refresh operations within an existing load or refresh " 

348 "operation, set restore_load_context=True when establishing the " 

349 "listener to ensure the context remains unchanged when the event " 

350 "handler completes." % (state_str(state),) 

351 ) 

352 

353 

354def _instance_processor( 

355 mapper, 

356 context, 

357 result, 

358 path, 

359 adapter, 

360 only_load_props=None, 

361 refresh_state=None, 

362 polymorphic_discriminator=None, 

363 _polymorphic_from=None, 

364): 

365 """Produce a mapper level row processor callable 

366 which processes rows into mapped instances.""" 

367 

368 # note that this method, most of which exists in a closure 

369 # called _instance(), resists being broken out, as 

370 # attempts to do so tend to add significant function 

371 # call overhead. _instance() is the most 

372 # performance-critical section in the whole ORM. 

373 

374 pk_cols = mapper.primary_key 

375 

376 if adapter: 

377 pk_cols = [adapter.columns[c] for c in pk_cols] 

378 

379 identity_class = mapper._identity_class 

380 

381 populators = collections.defaultdict(list) 

382 

383 props = mapper._prop_set 

384 if only_load_props is not None: 

385 props = props.intersection(mapper._props[k] for k in only_load_props) 

386 

387 quick_populators = path.get( 

388 context.attributes, "memoized_setups", _none_set 

389 ) 

390 

391 for prop in props: 

392 if prop in quick_populators: 

393 # this is an inlined path just for column-based attributes. 

394 col = quick_populators[prop] 

395 if col is _DEFER_FOR_STATE: 

396 populators["new"].append( 

397 (prop.key, prop._deferred_column_loader) 

398 ) 

399 elif col is _SET_DEFERRED_EXPIRED: 

400 # note that in this path, we are no longer 

401 # searching in the result to see if the column might 

402 # be present in some unexpected way. 

403 populators["expire"].append((prop.key, False)) 

404 else: 

405 getter = None 

406 # the "adapter" can be here via different paths, 

407 # e.g. via adapter present at setup_query or adapter 

408 # applied to the query afterwards via eager load subquery. 

409 # If the column here 

410 # were already a product of this adapter, sending it through 

411 # the adapter again can return a totally new expression that 

412 # won't be recognized in the result, and the ColumnAdapter 

413 # currently does not accommodate for this. OTOH, if the 

414 # column were never applied through this adapter, we may get 

415 # None back, in which case we still won't get our "getter". 

416 # so try both against result._getter(). See issue #4048 

417 if adapter: 

418 adapted_col = adapter.columns[col] 

419 if adapted_col is not None: 

420 getter = result._getter(adapted_col, False) 

421 if not getter: 

422 getter = result._getter(col, False) 

423 if getter: 

424 populators["quick"].append((prop.key, getter)) 

425 else: 

426 # fall back to the ColumnProperty itself, which 

427 # will iterate through all of its columns 

428 # to see if one fits 

429 prop.create_row_processor( 

430 context, path, mapper, result, adapter, populators 

431 ) 

432 else: 

433 prop.create_row_processor( 

434 context, path, mapper, result, adapter, populators 

435 ) 

436 

437 propagate_options = context.propagate_options 

438 load_path = ( 

439 context.query._current_path + path 

440 if context.query._current_path.path 

441 else path 

442 ) 

443 

444 session_identity_map = context.session.identity_map 

445 

446 populate_existing = context.populate_existing or mapper.always_refresh 

447 load_evt = bool(mapper.class_manager.dispatch.load) 

448 refresh_evt = bool(mapper.class_manager.dispatch.refresh) 

449 persistent_evt = bool(context.session.dispatch.loaded_as_persistent) 

450 if persistent_evt: 

451 loaded_as_persistent = context.session.dispatch.loaded_as_persistent 

452 instance_state = attributes.instance_state 

453 instance_dict = attributes.instance_dict 

454 session_id = context.session.hash_key 

455 version_check = context.version_check 

456 runid = context.runid 

457 identity_token = context.identity_token 

458 

459 if not refresh_state and _polymorphic_from is not None: 

460 key = ("loader", path.path) 

461 if key in context.attributes and context.attributes[key].strategy == ( 

462 ("selectinload_polymorphic", True), 

463 ): 

464 selectin_load_via = mapper._should_selectin_load( 

465 context.attributes[key].local_opts["entities"], 

466 _polymorphic_from, 

467 ) 

468 else: 

469 selectin_load_via = mapper._should_selectin_load( 

470 None, _polymorphic_from 

471 ) 

472 

473 if selectin_load_via and selectin_load_via is not _polymorphic_from: 

474 # only_load_props goes w/ refresh_state only, and in a refresh 

475 # we are a single row query for the exact entity; polymorphic 

476 # loading does not apply 

477 assert only_load_props is None 

478 

479 callable_ = _load_subclass_via_in(context, path, selectin_load_via) 

480 

481 PostLoad.callable_for_path( 

482 context, 

483 load_path, 

484 selectin_load_via.mapper, 

485 selectin_load_via, 

486 callable_, 

487 selectin_load_via, 

488 ) 

489 

490 post_load = PostLoad.for_context(context, load_path, only_load_props) 

491 

492 if refresh_state: 

493 refresh_identity_key = refresh_state.key 

494 if refresh_identity_key is None: 

495 # super-rare condition; a refresh is being called 

496 # on a non-instance-key instance; this is meant to only 

497 # occur within a flush() 

498 refresh_identity_key = mapper._identity_key_from_state( 

499 refresh_state 

500 ) 

501 else: 

502 refresh_identity_key = None 

503 

504 if mapper.allow_partial_pks: 

505 is_not_primary_key = _none_set.issuperset 

506 else: 

507 is_not_primary_key = _none_set.intersection 

508 

509 def _instance(row): 

510 

511 # determine the state that we'll be populating 

512 if refresh_identity_key: 

513 # fixed state that we're refreshing 

514 state = refresh_state 

515 instance = state.obj() 

516 dict_ = instance_dict(instance) 

517 isnew = state.runid != runid 

518 currentload = True 

519 loaded_instance = False 

520 else: 

521 # look at the row, see if that identity is in the 

522 # session, or we have to create a new one 

523 identitykey = ( 

524 identity_class, 

525 tuple([row[column] for column in pk_cols]), 

526 identity_token, 

527 ) 

528 

529 instance = session_identity_map.get(identitykey) 

530 

531 if instance is not None: 

532 # existing instance 

533 state = instance_state(instance) 

534 dict_ = instance_dict(instance) 

535 

536 isnew = state.runid != runid 

537 currentload = not isnew 

538 loaded_instance = False 

539 

540 if version_check and not currentload: 

541 _validate_version_id(mapper, state, dict_, row, adapter) 

542 

543 else: 

544 # create a new instance 

545 

546 # check for non-NULL values in the primary key columns, 

547 # else no entity is returned for the row 

548 if is_not_primary_key(identitykey[1]): 

549 return None 

550 

551 isnew = True 

552 currentload = True 

553 loaded_instance = True 

554 

555 instance = mapper.class_manager.new_instance() 

556 

557 dict_ = instance_dict(instance) 

558 state = instance_state(instance) 

559 state.key = identitykey 

560 state.identity_token = identity_token 

561 

562 # attach instance to session. 

563 state.session_id = session_id 

564 session_identity_map._add_unpresent(state, identitykey) 

565 

566 # populate. this looks at whether this state is new 

567 # for this load or was existing, and whether or not this 

568 # row is the first row with this identity. 

569 if currentload or populate_existing: 

570 # full population routines. Objects here are either 

571 # just created, or we are doing a populate_existing 

572 

573 # be conservative about setting load_path when populate_existing 

574 # is in effect; want to maintain options from the original 

575 # load. see test_expire->test_refresh_maintains_deferred_options 

576 if isnew and (propagate_options or not populate_existing): 

577 state.load_options = propagate_options 

578 state.load_path = load_path 

579 

580 _populate_full( 

581 context, 

582 row, 

583 state, 

584 dict_, 

585 isnew, 

586 load_path, 

587 loaded_instance, 

588 populate_existing, 

589 populators, 

590 ) 

591 

592 if isnew: 

593 # state.runid should be equal to context.runid / runid 

594 # here, however for event checks we are being more conservative 

595 # and checking against existing run id 

596 # assert state.runid == runid 

597 

598 existing_runid = state.runid 

599 

600 if loaded_instance: 

601 if load_evt: 

602 state.manager.dispatch.load(state, context) 

603 if state.runid != existing_runid: 

604 _warn_for_runid_changed(state) 

605 if persistent_evt: 

606 loaded_as_persistent(context.session, state) 

607 if state.runid != existing_runid: 

608 _warn_for_runid_changed(state) 

609 elif refresh_evt: 

610 state.manager.dispatch.refresh( 

611 state, context, only_load_props 

612 ) 

613 if state.runid != runid: 

614 _warn_for_runid_changed(state) 

615 

616 if populate_existing or state.modified: 

617 if refresh_state and only_load_props: 

618 state._commit(dict_, only_load_props) 

619 else: 

620 state._commit_all(dict_, session_identity_map) 

621 

622 if post_load: 

623 post_load.add_state(state, True) 

624 

625 else: 

626 # partial population routines, for objects that were already 

627 # in the Session, but a row matches them; apply eager loaders 

628 # on existing objects, etc. 

629 unloaded = state.unloaded 

630 isnew = state not in context.partials 

631 

632 if not isnew or unloaded or populators["eager"]: 

633 # state is having a partial set of its attributes 

634 # refreshed. Populate those attributes, 

635 # and add to the "context.partials" collection. 

636 

637 to_load = _populate_partial( 

638 context, 

639 row, 

640 state, 

641 dict_, 

642 isnew, 

643 load_path, 

644 unloaded, 

645 populators, 

646 ) 

647 

648 if isnew: 

649 if refresh_evt: 

650 existing_runid = state.runid 

651 state.manager.dispatch.refresh(state, context, to_load) 

652 if state.runid != existing_runid: 

653 _warn_for_runid_changed(state) 

654 

655 state._commit(dict_, to_load) 

656 

657 if post_load and context.invoke_all_eagers: 

658 post_load.add_state(state, False) 

659 

660 return instance 

661 

662 if mapper.polymorphic_map and not _polymorphic_from and not refresh_state: 

663 # if we are doing polymorphic, dispatch to a different _instance() 

664 # method specific to the subclass mapper 

665 _instance = _decorate_polymorphic_switch( 

666 _instance, 

667 context, 

668 mapper, 

669 result, 

670 path, 

671 polymorphic_discriminator, 

672 adapter, 

673 ) 

674 

675 return _instance 

676 

677 

678def _load_subclass_via_in(context, path, entity): 

679 mapper = entity.mapper 

680 

681 zero_idx = len(mapper.base_mapper.primary_key) == 1 

682 

683 if entity.is_aliased_class: 

684 q, enable_opt, disable_opt = mapper._subclass_load_via_in(entity) 

685 else: 

686 q, enable_opt, disable_opt = mapper._subclass_load_via_in_mapper 

687 

688 def do_load(context, path, states, load_only, effective_entity): 

689 orig_query = context.query 

690 

691 q2 = q._with_lazyload_options( 

692 (enable_opt,) + orig_query._with_options + (disable_opt,), 

693 path.parent, 

694 cache_path=path, 

695 ) 

696 

697 if orig_query._populate_existing: 

698 q2.add_criteria(lambda q: q.populate_existing()) 

699 

700 q2(context.session).params( 

701 primary_keys=[ 

702 state.key[1][0] if zero_idx else state.key[1] 

703 for state, load_attrs in states 

704 ] 

705 ).all() 

706 

707 return do_load 

708 

709 

710def _populate_full( 

711 context, 

712 row, 

713 state, 

714 dict_, 

715 isnew, 

716 load_path, 

717 loaded_instance, 

718 populate_existing, 

719 populators, 

720): 

721 if isnew: 

722 # first time we are seeing a row with this identity. 

723 state.runid = context.runid 

724 

725 for key, getter in populators["quick"]: 

726 dict_[key] = getter(row) 

727 if populate_existing: 

728 for key, set_callable in populators["expire"]: 

729 dict_.pop(key, None) 

730 if set_callable: 

731 state.expired_attributes.add(key) 

732 else: 

733 for key, set_callable in populators["expire"]: 

734 if set_callable: 

735 state.expired_attributes.add(key) 

736 for key, populator in populators["new"]: 

737 populator(state, dict_, row) 

738 for key, populator in populators["delayed"]: 

739 populator(state, dict_, row) 

740 elif load_path != state.load_path: 

741 # new load path, e.g. object is present in more than one 

742 # column position in a series of rows 

743 state.load_path = load_path 

744 

745 # if we have data, and the data isn't in the dict, OK, let's put 

746 # it in. 

747 for key, getter in populators["quick"]: 

748 if key not in dict_: 

749 dict_[key] = getter(row) 

750 

751 # otherwise treat like an "already seen" row 

752 for key, populator in populators["existing"]: 

753 populator(state, dict_, row) 

754 # TODO: allow "existing" populator to know this is 

755 # a new path for the state: 

756 # populator(state, dict_, row, new_path=True) 

757 

758 else: 

759 # have already seen rows with this identity in this same path. 

760 for key, populator in populators["existing"]: 

761 populator(state, dict_, row) 

762 

763 # TODO: same path 

764 # populator(state, dict_, row, new_path=False) 

765 

766 

767def _populate_partial( 

768 context, row, state, dict_, isnew, load_path, unloaded, populators 

769): 

770 

771 if not isnew: 

772 to_load = context.partials[state] 

773 for key, populator in populators["existing"]: 

774 if key in to_load: 

775 populator(state, dict_, row) 

776 else: 

777 to_load = unloaded 

778 context.partials[state] = to_load 

779 

780 for key, getter in populators["quick"]: 

781 if key in to_load: 

782 dict_[key] = getter(row) 

783 for key, set_callable in populators["expire"]: 

784 if key in to_load: 

785 dict_.pop(key, None) 

786 if set_callable: 

787 state.expired_attributes.add(key) 

788 for key, populator in populators["new"]: 

789 if key in to_load: 

790 populator(state, dict_, row) 

791 for key, populator in populators["delayed"]: 

792 if key in to_load: 

793 populator(state, dict_, row) 

794 for key, populator in populators["eager"]: 

795 if key not in unloaded: 

796 populator(state, dict_, row) 

797 

798 return to_load 

799 

800 

801def _validate_version_id(mapper, state, dict_, row, adapter): 

802 

803 version_id_col = mapper.version_id_col 

804 

805 if version_id_col is None: 

806 return 

807 

808 if adapter: 

809 version_id_col = adapter.columns[version_id_col] 

810 

811 if ( 

812 mapper._get_state_attr_by_column(state, dict_, mapper.version_id_col) 

813 != row[version_id_col] 

814 ): 

815 raise orm_exc.StaleDataError( 

816 "Instance '%s' has version id '%s' which " 

817 "does not match database-loaded version id '%s'." 

818 % ( 

819 state_str(state), 

820 mapper._get_state_attr_by_column( 

821 state, dict_, mapper.version_id_col 

822 ), 

823 row[version_id_col], 

824 ) 

825 ) 

826 

827 

828def _decorate_polymorphic_switch( 

829 instance_fn, 

830 context, 

831 mapper, 

832 result, 

833 path, 

834 polymorphic_discriminator, 

835 adapter, 

836): 

837 if polymorphic_discriminator is not None: 

838 polymorphic_on = polymorphic_discriminator 

839 else: 

840 polymorphic_on = mapper.polymorphic_on 

841 if polymorphic_on is None: 

842 return instance_fn 

843 

844 if adapter: 

845 polymorphic_on = adapter.columns[polymorphic_on] 

846 

847 def configure_subclass_mapper(discriminator): 

848 try: 

849 sub_mapper = mapper.polymorphic_map[discriminator] 

850 except KeyError: 

851 raise AssertionError( 

852 "No such polymorphic_identity %r is defined" % discriminator 

853 ) 

854 else: 

855 if sub_mapper is mapper: 

856 return None 

857 

858 return _instance_processor( 

859 sub_mapper, 

860 context, 

861 result, 

862 path, 

863 adapter, 

864 _polymorphic_from=mapper, 

865 ) 

866 

867 polymorphic_instances = util.PopulateDict(configure_subclass_mapper) 

868 

869 def polymorphic_instance(row): 

870 discriminator = row[polymorphic_on] 

871 if discriminator is not None: 

872 _instance = polymorphic_instances[discriminator] 

873 if _instance: 

874 return _instance(row) 

875 return instance_fn(row) 

876 

877 return polymorphic_instance 

878 

879 

880class PostLoad(object): 

881 """Track loaders and states for "post load" operations. 

882 

883 """ 

884 

885 __slots__ = "loaders", "states", "load_keys" 

886 

887 def __init__(self): 

888 self.loaders = {} 

889 self.states = util.OrderedDict() 

890 self.load_keys = None 

891 

892 def add_state(self, state, overwrite): 

893 # the states for a polymorphic load here are all shared 

894 # within a single PostLoad object among multiple subtypes. 

895 # Filtering of callables on a per-subclass basis needs to be done at 

896 # the invocation level 

897 self.states[state] = overwrite 

898 

899 def invoke(self, context, path): 

900 if not self.states: 

901 return 

902 path = path_registry.PathRegistry.coerce(path) 

903 for token, limit_to_mapper, loader, arg, kw in self.loaders.values(): 

904 states = [ 

905 (state, overwrite) 

906 for state, overwrite in self.states.items() 

907 if state.manager.mapper.isa(limit_to_mapper) 

908 ] 

909 if states: 

910 loader(context, path, states, self.load_keys, *arg, **kw) 

911 self.states.clear() 

912 

913 @classmethod 

914 def for_context(cls, context, path, only_load_props): 

915 pl = context.post_load_paths.get(path.path) 

916 if pl is not None and only_load_props: 

917 pl.load_keys = only_load_props 

918 return pl 

919 

920 @classmethod 

921 def path_exists(self, context, path, key): 

922 return ( 

923 path.path in context.post_load_paths 

924 and key in context.post_load_paths[path.path].loaders 

925 ) 

926 

927 @classmethod 

928 def callable_for_path( 

929 cls, context, path, limit_to_mapper, token, loader_callable, *arg, **kw 

930 ): 

931 if path.path in context.post_load_paths: 

932 pl = context.post_load_paths[path.path] 

933 else: 

934 pl = context.post_load_paths[path.path] = PostLoad() 

935 pl.loaders[token] = (token, limit_to_mapper, loader_callable, arg, kw) 

936 

937 

938def load_scalar_attributes(mapper, state, attribute_names): 

939 """initiate a column-based attribute refresh operation.""" 

940 

941 # assert mapper is _state_mapper(state) 

942 session = state.session 

943 if not session: 

944 raise orm_exc.DetachedInstanceError( 

945 "Instance %s is not bound to a Session; " 

946 "attribute refresh operation cannot proceed" % (state_str(state)) 

947 ) 

948 

949 has_key = bool(state.key) 

950 

951 result = False 

952 

953 # in the case of inheritance, particularly concrete and abstract 

954 # concrete inheritance, the class manager might have some keys 

955 # of attributes on the superclass that we didn't actually map. 

956 # These could be mapped as "concrete, dont load" or could be completely 

957 # exluded from the mapping and we know nothing about them. Filter them 

958 # here to prevent them from coming through. 

959 if attribute_names: 

960 attribute_names = attribute_names.intersection(mapper.attrs.keys()) 

961 

962 if mapper.inherits and not mapper.concrete: 

963 # because we are using Core to produce a select() that we 

964 # pass to the Query, we aren't calling setup() for mapped 

965 # attributes; in 1.0 this means deferred attrs won't get loaded 

966 # by default 

967 statement = mapper._optimized_get_statement(state, attribute_names) 

968 if statement is not None: 

969 result = load_on_ident( 

970 session.query(mapper) 

971 .options(strategy_options.Load(mapper).undefer("*")) 

972 .from_statement(statement), 

973 None, 

974 only_load_props=attribute_names, 

975 refresh_state=state, 

976 ) 

977 

978 if result is False: 

979 if has_key: 

980 identity_key = state.key 

981 else: 

982 # this codepath is rare - only valid when inside a flush, and the 

983 # object is becoming persistent but hasn't yet been assigned 

984 # an identity_key. 

985 # check here to ensure we have the attrs we need. 

986 pk_attrs = [ 

987 mapper._columntoproperty[col].key for col in mapper.primary_key 

988 ] 

989 if state.expired_attributes.intersection(pk_attrs): 

990 raise sa_exc.InvalidRequestError( 

991 "Instance %s cannot be refreshed - it's not " 

992 " persistent and does not " 

993 "contain a full primary key." % state_str(state) 

994 ) 

995 identity_key = mapper._identity_key_from_state(state) 

996 

997 if ( 

998 _none_set.issubset(identity_key) and not mapper.allow_partial_pks 

999 ) or _none_set.issuperset(identity_key): 

1000 util.warn_limited( 

1001 "Instance %s to be refreshed doesn't " 

1002 "contain a full primary key - can't be refreshed " 

1003 "(and shouldn't be expired, either).", 

1004 state_str(state), 

1005 ) 

1006 return 

1007 

1008 result = load_on_ident( 

1009 session.query(mapper), 

1010 identity_key, 

1011 refresh_state=state, 

1012 only_load_props=attribute_names, 

1013 ) 

1014 

1015 # if instance is pending, a refresh operation 

1016 # may not complete (even if PK attributes are assigned) 

1017 if has_key and result is None: 

1018 raise orm_exc.ObjectDeletedError(state)