1# orm/loading.py 2# Copyright (C) 2005-2019 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 = ( 44 not query._only_return_tuples 45 and len(query._entities) == 1 46 and query._entities[0].supports_single_entity 47 ) 48 49 if filtered: 50 if single_entity: 51 filter_fn = id 52 else: 53 54 def filter_fn(row): 55 return tuple( 56 id(item) if ent.use_id_for_hash else item 57 for ent, item in zip(query._entities, row) 58 ) 59 60 try: 61 (process, labels) = list( 62 zip( 63 *[ 64 query_entity.row_processor(query, context, cursor) 65 for query_entity in query._entities 66 ] 67 ) 68 ) 69 70 if not single_entity: 71 keyed_tuple = util.lightweight_named_tuple("result", labels) 72 73 while True: 74 context.partials = {} 75 76 if query._yield_per: 77 fetch = cursor.fetchmany(query._yield_per) 78 if not fetch: 79 break 80 else: 81 fetch = cursor.fetchall() 82 83 if single_entity: 84 proc = process[0] 85 rows = [proc(row) for row in fetch] 86 else: 87 rows = [ 88 keyed_tuple([proc(row) for proc in process]) 89 for row in fetch 90 ] 91 92 for path, post_load in context.post_load_paths.items(): 93 post_load.invoke(context, path) 94 95 if filtered: 96 rows = util.unique_list(rows, filter_fn) 97 98 for row in rows: 99 yield row 100 101 if not query._yield_per: 102 break 103 except Exception as err: 104 cursor.close() 105 util.raise_from_cause(err) 106 107 108@util.dependencies("sqlalchemy.orm.query") 109def merge_result(querylib, query, iterator, load=True): 110 """Merge a result into this :class:`.Query` object's Session.""" 111 112 session = query.session 113 if load: 114 # flush current contents if we expect to load data 115 session._autoflush() 116 117 autoflush = session.autoflush 118 try: 119 session.autoflush = False 120 single_entity = len(query._entities) == 1 121 if single_entity: 122 if isinstance(query._entities[0], querylib._MapperEntity): 123 result = [ 124 session._merge( 125 attributes.instance_state(instance), 126 attributes.instance_dict(instance), 127 load=load, 128 _recursive={}, 129 _resolve_conflict_map={}, 130 ) 131 for instance in iterator 132 ] 133 else: 134 result = list(iterator) 135 else: 136 mapped_entities = [ 137 i 138 for i, e in enumerate(query._entities) 139 if isinstance(e, querylib._MapperEntity) 140 ] 141 result = [] 142 keys = [ent._label_name for ent in query._entities] 143 keyed_tuple = util.lightweight_named_tuple("result", keys) 144 for row in iterator: 145 newrow = list(row) 146 for i in mapped_entities: 147 if newrow[i] is not None: 148 newrow[i] = session._merge( 149 attributes.instance_state(newrow[i]), 150 attributes.instance_dict(newrow[i]), 151 load=load, 152 _recursive={}, 153 _resolve_conflict_map={}, 154 ) 155 result.append(keyed_tuple(newrow)) 156 157 return iter(result) 158 finally: 159 session.autoflush = autoflush 160 161 162def get_from_identity(session, key, passive): 163 """Look up the given key in the given session's identity map, 164 check the object for expired state if found. 165 166 """ 167 instance = session.identity_map.get(key) 168 if instance is not None: 169 170 state = attributes.instance_state(instance) 171 172 # expired - ensure it still exists 173 if state.expired: 174 if not passive & attributes.SQL_OK: 175 # TODO: no coverage here 176 return attributes.PASSIVE_NO_RESULT 177 elif not passive & attributes.RELATED_OBJECT_OK: 178 # this mode is used within a flush and the instance's 179 # expired state will be checked soon enough, if necessary 180 return instance 181 try: 182 state._load_expired(state, passive) 183 except orm_exc.ObjectDeletedError: 184 session._remove_newly_deleted([state]) 185 return None 186 return instance 187 else: 188 return None 189 190 191def load_on_ident( 192 query, key, refresh_state=None, with_for_update=None, only_load_props=None 193): 194 """Load the given identity key from the database.""" 195 196 if key is not None: 197 ident = key[1] 198 identity_token = key[2] 199 else: 200 ident = identity_token = None 201 202 return load_on_pk_identity( 203 query, 204 ident, 205 refresh_state=refresh_state, 206 with_for_update=with_for_update, 207 only_load_props=only_load_props, 208 identity_token=identity_token, 209 ) 210 211 212def load_on_pk_identity( 213 query, 214 primary_key_identity, 215 refresh_state=None, 216 with_for_update=None, 217 only_load_props=None, 218 identity_token=None, 219): 220 221 """Load the given primary key identity from the database.""" 222 223 if refresh_state is None: 224 q = query._clone() 225 q._get_condition() 226 else: 227 q = query._clone() 228 229 if primary_key_identity is not None: 230 mapper = query._mapper_zero() 231 232 (_get_clause, _get_params) = mapper._get_clause 233 234 # None present in ident - turn those comparisons 235 # into "IS NULL" 236 if None in primary_key_identity: 237 nones = set( 238 [ 239 _get_params[col].key 240 for col, value in zip( 241 mapper.primary_key, primary_key_identity 242 ) 243 if value is None 244 ] 245 ) 246 _get_clause = sql_util.adapt_criterion_to_null(_get_clause, nones) 247 248 _get_clause = q._adapt_clause(_get_clause, True, False) 249 q._criterion = _get_clause 250 251 params = dict( 252 [ 253 (_get_params[primary_key].key, id_val) 254 for id_val, primary_key in zip( 255 primary_key_identity, mapper.primary_key 256 ) 257 ] 258 ) 259 260 q._params = params 261 262 # with_for_update needs to be query.LockmodeArg() 263 if with_for_update is not None: 264 version_check = True 265 q._for_update_arg = with_for_update 266 elif query._for_update_arg is not None: 267 version_check = True 268 q._for_update_arg = query._for_update_arg 269 else: 270 version_check = False 271 272 q._get_options( 273 populate_existing=bool(refresh_state), 274 version_check=version_check, 275 only_load_props=only_load_props, 276 refresh_state=refresh_state, 277 identity_token=identity_token, 278 ) 279 q._order_by = None 280 281 try: 282 return q.one() 283 except orm_exc.NoResultFound: 284 return None 285 286 287def _setup_entity_query( 288 context, 289 mapper, 290 query_entity, 291 path, 292 adapter, 293 column_collection, 294 with_polymorphic=None, 295 only_load_props=None, 296 polymorphic_discriminator=None, 297 **kw 298): 299 300 if with_polymorphic: 301 poly_properties = mapper._iterate_polymorphic_properties( 302 with_polymorphic 303 ) 304 else: 305 poly_properties = mapper._polymorphic_properties 306 307 quick_populators = {} 308 309 path.set(context.attributes, "memoized_setups", quick_populators) 310 311 for value in poly_properties: 312 if only_load_props and value.key not in only_load_props: 313 continue 314 value.setup( 315 context, 316 query_entity, 317 path, 318 adapter, 319 only_load_props=only_load_props, 320 column_collection=column_collection, 321 memoized_populators=quick_populators, 322 **kw 323 ) 324 325 if ( 326 polymorphic_discriminator is not None 327 and polymorphic_discriminator is not mapper.polymorphic_on 328 ): 329 330 if adapter: 331 pd = adapter.columns[polymorphic_discriminator] 332 else: 333 pd = polymorphic_discriminator 334 column_collection.append(pd) 335 336 337def _instance_processor( 338 mapper, 339 context, 340 result, 341 path, 342 adapter, 343 only_load_props=None, 344 refresh_state=None, 345 polymorphic_discriminator=None, 346 _polymorphic_from=None, 347): 348 """Produce a mapper level row processor callable 349 which processes rows into mapped instances.""" 350 351 # note that this method, most of which exists in a closure 352 # called _instance(), resists being broken out, as 353 # attempts to do so tend to add significant function 354 # call overhead. _instance() is the most 355 # performance-critical section in the whole ORM. 356 357 pk_cols = mapper.primary_key 358 359 if adapter: 360 pk_cols = [adapter.columns[c] for c in pk_cols] 361 362 identity_class = mapper._identity_class 363 364 populators = collections.defaultdict(list) 365 366 props = mapper._prop_set 367 if only_load_props is not None: 368 props = props.intersection(mapper._props[k] for k in only_load_props) 369 370 quick_populators = path.get( 371 context.attributes, "memoized_setups", _none_set 372 ) 373 374 for prop in props: 375 if prop in quick_populators: 376 # this is an inlined path just for column-based attributes. 377 col = quick_populators[prop] 378 if col is _DEFER_FOR_STATE: 379 populators["new"].append( 380 (prop.key, prop._deferred_column_loader) 381 ) 382 elif col is _SET_DEFERRED_EXPIRED: 383 # note that in this path, we are no longer 384 # searching in the result to see if the column might 385 # be present in some unexpected way. 386 populators["expire"].append((prop.key, False)) 387 else: 388 getter = None 389 # the "adapter" can be here via different paths, 390 # e.g. via adapter present at setup_query or adapter 391 # applied to the query afterwards via eager load subquery. 392 # If the column here 393 # were already a product of this adapter, sending it through 394 # the adapter again can return a totally new expression that 395 # won't be recognized in the result, and the ColumnAdapter 396 # currently does not accommodate for this. OTOH, if the 397 # column were never applied through this adapter, we may get 398 # None back, in which case we still won't get our "getter". 399 # so try both against result._getter(). See issue #4048 400 if adapter: 401 adapted_col = adapter.columns[col] 402 if adapted_col is not None: 403 getter = result._getter(adapted_col, False) 404 if not getter: 405 getter = result._getter(col, False) 406 if getter: 407 populators["quick"].append((prop.key, getter)) 408 else: 409 # fall back to the ColumnProperty itself, which 410 # will iterate through all of its columns 411 # to see if one fits 412 prop.create_row_processor( 413 context, path, mapper, result, adapter, populators 414 ) 415 else: 416 prop.create_row_processor( 417 context, path, mapper, result, adapter, populators 418 ) 419 420 propagate_options = context.propagate_options 421 load_path = ( 422 context.query._current_path + path 423 if context.query._current_path.path 424 else path 425 ) 426 427 session_identity_map = context.session.identity_map 428 429 populate_existing = context.populate_existing or mapper.always_refresh 430 load_evt = bool(mapper.class_manager.dispatch.load) 431 refresh_evt = bool(mapper.class_manager.dispatch.refresh) 432 persistent_evt = bool(context.session.dispatch.loaded_as_persistent) 433 if persistent_evt: 434 loaded_as_persistent = context.session.dispatch.loaded_as_persistent 435 instance_state = attributes.instance_state 436 instance_dict = attributes.instance_dict 437 session_id = context.session.hash_key 438 version_check = context.version_check 439 runid = context.runid 440 identity_token = context.identity_token 441 442 if not refresh_state and _polymorphic_from is not None: 443 key = ("loader", path.path) 444 if key in context.attributes and context.attributes[key].strategy == ( 445 ("selectinload_polymorphic", True), 446 ): 447 selectin_load_via = mapper._should_selectin_load( 448 context.attributes[key].local_opts["entities"], 449 _polymorphic_from, 450 ) 451 else: 452 selectin_load_via = mapper._should_selectin_load( 453 None, _polymorphic_from 454 ) 455 456 if selectin_load_via and selectin_load_via is not _polymorphic_from: 457 # only_load_props goes w/ refresh_state only, and in a refresh 458 # we are a single row query for the exact entity; polymorphic 459 # loading does not apply 460 assert only_load_props is None 461 462 callable_ = _load_subclass_via_in(context, path, selectin_load_via) 463 464 PostLoad.callable_for_path( 465 context, 466 load_path, 467 selectin_load_via.mapper, 468 selectin_load_via, 469 callable_, 470 selectin_load_via, 471 ) 472 473 post_load = PostLoad.for_context(context, load_path, only_load_props) 474 475 if refresh_state: 476 refresh_identity_key = refresh_state.key 477 if refresh_identity_key is None: 478 # super-rare condition; a refresh is being called 479 # on a non-instance-key instance; this is meant to only 480 # occur within a flush() 481 refresh_identity_key = mapper._identity_key_from_state( 482 refresh_state 483 ) 484 else: 485 refresh_identity_key = None 486 487 if mapper.allow_partial_pks: 488 is_not_primary_key = _none_set.issuperset 489 else: 490 is_not_primary_key = _none_set.intersection 491 492 def _instance(row): 493 494 # determine the state that we'll be populating 495 if refresh_identity_key: 496 # fixed state that we're refreshing 497 state = refresh_state 498 instance = state.obj() 499 dict_ = instance_dict(instance) 500 isnew = state.runid != runid 501 currentload = True 502 loaded_instance = False 503 else: 504 # look at the row, see if that identity is in the 505 # session, or we have to create a new one 506 identitykey = ( 507 identity_class, 508 tuple([row[column] for column in pk_cols]), 509 identity_token, 510 ) 511 512 instance = session_identity_map.get(identitykey) 513 514 if instance is not None: 515 # existing instance 516 state = instance_state(instance) 517 dict_ = instance_dict(instance) 518 519 isnew = state.runid != runid 520 currentload = not isnew 521 loaded_instance = False 522 523 if version_check and not currentload: 524 _validate_version_id(mapper, state, dict_, row, adapter) 525 526 else: 527 # create a new instance 528 529 # check for non-NULL values in the primary key columns, 530 # else no entity is returned for the row 531 if is_not_primary_key(identitykey[1]): 532 return None 533 534 isnew = True 535 currentload = True 536 loaded_instance = True 537 538 instance = mapper.class_manager.new_instance() 539 540 dict_ = instance_dict(instance) 541 state = instance_state(instance) 542 state.key = identitykey 543 state.identity_token = identity_token 544 545 # attach instance to session. 546 state.session_id = session_id 547 session_identity_map._add_unpresent(state, identitykey) 548 549 # populate. this looks at whether this state is new 550 # for this load or was existing, and whether or not this 551 # row is the first row with this identity. 552 if currentload or populate_existing: 553 # full population routines. Objects here are either 554 # just created, or we are doing a populate_existing 555 556 # be conservative about setting load_path when populate_existing 557 # is in effect; want to maintain options from the original 558 # load. see test_expire->test_refresh_maintains_deferred_options 559 if isnew and (propagate_options or not populate_existing): 560 state.load_options = propagate_options 561 state.load_path = load_path 562 563 _populate_full( 564 context, 565 row, 566 state, 567 dict_, 568 isnew, 569 load_path, 570 loaded_instance, 571 populate_existing, 572 populators, 573 ) 574 575 if isnew: 576 if loaded_instance: 577 if load_evt: 578 state.manager.dispatch.load(state, context) 579 if persistent_evt: 580 loaded_as_persistent(context.session, state.obj()) 581 elif refresh_evt: 582 state.manager.dispatch.refresh( 583 state, context, only_load_props 584 ) 585 586 if populate_existing or state.modified: 587 if refresh_state and only_load_props: 588 state._commit(dict_, only_load_props) 589 else: 590 state._commit_all(dict_, session_identity_map) 591 592 if post_load: 593 post_load.add_state(state, True) 594 595 else: 596 # partial population routines, for objects that were already 597 # in the Session, but a row matches them; apply eager loaders 598 # on existing objects, etc. 599 unloaded = state.unloaded 600 isnew = state not in context.partials 601 602 if not isnew or unloaded or populators["eager"]: 603 # state is having a partial set of its attributes 604 # refreshed. Populate those attributes, 605 # and add to the "context.partials" collection. 606 607 to_load = _populate_partial( 608 context, 609 row, 610 state, 611 dict_, 612 isnew, 613 load_path, 614 unloaded, 615 populators, 616 ) 617 618 if isnew: 619 if refresh_evt: 620 state.manager.dispatch.refresh(state, context, to_load) 621 622 state._commit(dict_, to_load) 623 624 if post_load and context.invoke_all_eagers: 625 post_load.add_state(state, False) 626 627 return instance 628 629 if mapper.polymorphic_map and not _polymorphic_from and not refresh_state: 630 # if we are doing polymorphic, dispatch to a different _instance() 631 # method specific to the subclass mapper 632 _instance = _decorate_polymorphic_switch( 633 _instance, 634 context, 635 mapper, 636 result, 637 path, 638 polymorphic_discriminator, 639 adapter, 640 ) 641 642 return _instance 643 644 645def _load_subclass_via_in(context, path, entity): 646 mapper = entity.mapper 647 648 zero_idx = len(mapper.base_mapper.primary_key) == 1 649 650 if entity.is_aliased_class: 651 q, enable_opt, disable_opt = mapper._subclass_load_via_in(entity) 652 else: 653 q, enable_opt, disable_opt = mapper._subclass_load_via_in_mapper 654 655 def do_load(context, path, states, load_only, effective_entity): 656 orig_query = context.query 657 658 q2 = q._with_lazyload_options( 659 (enable_opt,) + orig_query._with_options + (disable_opt,), 660 path.parent, 661 cache_path=path, 662 ) 663 664 if orig_query._populate_existing: 665 q2.add_criteria(lambda q: q.populate_existing()) 666 667 q2(context.session).params( 668 primary_keys=[ 669 state.key[1][0] if zero_idx else state.key[1] 670 for state, load_attrs in states 671 ] 672 ).all() 673 674 return do_load 675 676 677def _populate_full( 678 context, 679 row, 680 state, 681 dict_, 682 isnew, 683 load_path, 684 loaded_instance, 685 populate_existing, 686 populators, 687): 688 if isnew: 689 # first time we are seeing a row with this identity. 690 state.runid = context.runid 691 692 for key, getter in populators["quick"]: 693 dict_[key] = getter(row) 694 if populate_existing: 695 for key, set_callable in populators["expire"]: 696 dict_.pop(key, None) 697 if set_callable: 698 state.expired_attributes.add(key) 699 else: 700 for key, set_callable in populators["expire"]: 701 if set_callable: 702 state.expired_attributes.add(key) 703 for key, populator in populators["new"]: 704 populator(state, dict_, row) 705 for key, populator in populators["delayed"]: 706 populator(state, dict_, row) 707 elif load_path != state.load_path: 708 # new load path, e.g. object is present in more than one 709 # column position in a series of rows 710 state.load_path = load_path 711 712 # if we have data, and the data isn't in the dict, OK, let's put 713 # it in. 714 for key, getter in populators["quick"]: 715 if key not in dict_: 716 dict_[key] = getter(row) 717 718 # otherwise treat like an "already seen" row 719 for key, populator in populators["existing"]: 720 populator(state, dict_, row) 721 # TODO: allow "existing" populator to know this is 722 # a new path for the state: 723 # populator(state, dict_, row, new_path=True) 724 725 else: 726 # have already seen rows with this identity in this same path. 727 for key, populator in populators["existing"]: 728 populator(state, dict_, row) 729 730 # TODO: same path 731 # populator(state, dict_, row, new_path=False) 732 733 734def _populate_partial( 735 context, row, state, dict_, isnew, load_path, unloaded, populators 736): 737 738 if not isnew: 739 to_load = context.partials[state] 740 for key, populator in populators["existing"]: 741 if key in to_load: 742 populator(state, dict_, row) 743 else: 744 to_load = unloaded 745 context.partials[state] = to_load 746 747 for key, getter in populators["quick"]: 748 if key in to_load: 749 dict_[key] = getter(row) 750 for key, set_callable in populators["expire"]: 751 if key in to_load: 752 dict_.pop(key, None) 753 if set_callable: 754 state.expired_attributes.add(key) 755 for key, populator in populators["new"]: 756 if key in to_load: 757 populator(state, dict_, row) 758 for key, populator in populators["delayed"]: 759 if key in to_load: 760 populator(state, dict_, row) 761 for key, populator in populators["eager"]: 762 if key not in unloaded: 763 populator(state, dict_, row) 764 765 return to_load 766 767 768def _validate_version_id(mapper, state, dict_, row, adapter): 769 770 version_id_col = mapper.version_id_col 771 772 if version_id_col is None: 773 return 774 775 if adapter: 776 version_id_col = adapter.columns[version_id_col] 777 778 if ( 779 mapper._get_state_attr_by_column(state, dict_, mapper.version_id_col) 780 != row[version_id_col] 781 ): 782 raise orm_exc.StaleDataError( 783 "Instance '%s' has version id '%s' which " 784 "does not match database-loaded version id '%s'." 785 % ( 786 state_str(state), 787 mapper._get_state_attr_by_column( 788 state, dict_, mapper.version_id_col 789 ), 790 row[version_id_col], 791 ) 792 ) 793 794 795def _decorate_polymorphic_switch( 796 instance_fn, 797 context, 798 mapper, 799 result, 800 path, 801 polymorphic_discriminator, 802 adapter, 803): 804 if polymorphic_discriminator is not None: 805 polymorphic_on = polymorphic_discriminator 806 else: 807 polymorphic_on = mapper.polymorphic_on 808 if polymorphic_on is None: 809 return instance_fn 810 811 if adapter: 812 polymorphic_on = adapter.columns[polymorphic_on] 813 814 def configure_subclass_mapper(discriminator): 815 try: 816 sub_mapper = mapper.polymorphic_map[discriminator] 817 except KeyError: 818 raise AssertionError( 819 "No such polymorphic_identity %r is defined" % discriminator 820 ) 821 else: 822 if sub_mapper is mapper: 823 return None 824 825 return _instance_processor( 826 sub_mapper, 827 context, 828 result, 829 path, 830 adapter, 831 _polymorphic_from=mapper, 832 ) 833 834 polymorphic_instances = util.PopulateDict(configure_subclass_mapper) 835 836 def polymorphic_instance(row): 837 discriminator = row[polymorphic_on] 838 if discriminator is not None: 839 _instance = polymorphic_instances[discriminator] 840 if _instance: 841 return _instance(row) 842 return instance_fn(row) 843 844 return polymorphic_instance 845 846 847class PostLoad(object): 848 """Track loaders and states for "post load" operations. 849 850 """ 851 852 __slots__ = "loaders", "states", "load_keys" 853 854 def __init__(self): 855 self.loaders = {} 856 self.states = util.OrderedDict() 857 self.load_keys = None 858 859 def add_state(self, state, overwrite): 860 # the states for a polymorphic load here are all shared 861 # within a single PostLoad object among multiple subtypes. 862 # Filtering of callables on a per-subclass basis needs to be done at 863 # the invocation level 864 self.states[state] = overwrite 865 866 def invoke(self, context, path): 867 if not self.states: 868 return 869 path = path_registry.PathRegistry.coerce(path) 870 for token, limit_to_mapper, loader, arg, kw in self.loaders.values(): 871 states = [ 872 (state, overwrite) 873 for state, overwrite in self.states.items() 874 if state.manager.mapper.isa(limit_to_mapper) 875 ] 876 if states: 877 loader(context, path, states, self.load_keys, *arg, **kw) 878 self.states.clear() 879 880 @classmethod 881 def for_context(cls, context, path, only_load_props): 882 pl = context.post_load_paths.get(path.path) 883 if pl is not None and only_load_props: 884 pl.load_keys = only_load_props 885 return pl 886 887 @classmethod 888 def path_exists(self, context, path, key): 889 return ( 890 path.path in context.post_load_paths 891 and key in context.post_load_paths[path.path].loaders 892 ) 893 894 @classmethod 895 def callable_for_path( 896 cls, context, path, limit_to_mapper, token, loader_callable, *arg, **kw 897 ): 898 if path.path in context.post_load_paths: 899 pl = context.post_load_paths[path.path] 900 else: 901 pl = context.post_load_paths[path.path] = PostLoad() 902 pl.loaders[token] = (token, limit_to_mapper, loader_callable, arg, kw) 903 904 905def load_scalar_attributes(mapper, state, attribute_names): 906 """initiate a column-based attribute refresh operation.""" 907 908 # assert mapper is _state_mapper(state) 909 session = state.session 910 if not session: 911 raise orm_exc.DetachedInstanceError( 912 "Instance %s is not bound to a Session; " 913 "attribute refresh operation cannot proceed" % (state_str(state)) 914 ) 915 916 has_key = bool(state.key) 917 918 result = False 919 920 # in the case of inheritance, particularly concrete and abstract 921 # concrete inheritance, the class manager might have some keys 922 # of attributes on the superclass that we didn't actually map. 923 # These could be mapped as "concrete, dont load" or could be completely 924 # exluded from the mapping and we know nothing about them. Filter them 925 # here to prevent them from coming through. 926 if attribute_names: 927 attribute_names = attribute_names.intersection(mapper.attrs.keys()) 928 929 if mapper.inherits and not mapper.concrete: 930 # because we are using Core to produce a select() that we 931 # pass to the Query, we aren't calling setup() for mapped 932 # attributes; in 1.0 this means deferred attrs won't get loaded 933 # by default 934 statement = mapper._optimized_get_statement(state, attribute_names) 935 if statement is not None: 936 result = load_on_ident( 937 session.query(mapper) 938 .options(strategy_options.Load(mapper).undefer("*")) 939 .from_statement(statement), 940 None, 941 only_load_props=attribute_names, 942 refresh_state=state, 943 ) 944 945 if result is False: 946 if has_key: 947 identity_key = state.key 948 else: 949 # this codepath is rare - only valid when inside a flush, and the 950 # object is becoming persistent but hasn't yet been assigned 951 # an identity_key. 952 # check here to ensure we have the attrs we need. 953 pk_attrs = [ 954 mapper._columntoproperty[col].key for col in mapper.primary_key 955 ] 956 if state.expired_attributes.intersection(pk_attrs): 957 raise sa_exc.InvalidRequestError( 958 "Instance %s cannot be refreshed - it's not " 959 " persistent and does not " 960 "contain a full primary key." % state_str(state) 961 ) 962 identity_key = mapper._identity_key_from_state(state) 963 964 if ( 965 _none_set.issubset(identity_key) and not mapper.allow_partial_pks 966 ) or _none_set.issuperset(identity_key): 967 util.warn_limited( 968 "Instance %s to be refreshed doesn't " 969 "contain a full primary key - can't be refreshed " 970 "(and shouldn't be expired, either).", 971 state_str(state), 972 ) 973 return 974 975 result = load_on_ident( 976 session.query(mapper), 977 identity_key, 978 refresh_state=state, 979 only_load_props=attribute_names, 980 ) 981 982 # if instance is pending, a refresh operation 983 # may not complete (even if PK attributes are assigned) 984 if has_key and result is None: 985 raise orm_exc.ObjectDeletedError(state) 986