[3] | 1 | # strategies.py |
---|
| 2 | # Copyright (C) 2005, 2006, 2007, 2008, 2009 Michael Bayer mike_mp@zzzcomputing.com |
---|
| 3 | # |
---|
| 4 | # This module is part of SQLAlchemy and is released under |
---|
| 5 | # the MIT License: http://www.opensource.org/licenses/mit-license.php |
---|
| 6 | |
---|
| 7 | """sqlalchemy.orm.interfaces.LoaderStrategy implementations, and related MapperOptions.""" |
---|
| 8 | |
---|
| 9 | import sqlalchemy.exceptions as sa_exc |
---|
| 10 | from sqlalchemy import sql, util, log |
---|
| 11 | from sqlalchemy.sql import util as sql_util |
---|
| 12 | from sqlalchemy.sql import visitors, expression, operators |
---|
| 13 | from sqlalchemy.orm import mapper, attributes, interfaces |
---|
| 14 | from sqlalchemy.orm.interfaces import ( |
---|
| 15 | LoaderStrategy, StrategizedOption, MapperOption, PropertyOption, |
---|
| 16 | serialize_path, deserialize_path, StrategizedProperty |
---|
| 17 | ) |
---|
| 18 | from sqlalchemy.orm import session as sessionlib |
---|
| 19 | from sqlalchemy.orm import util as mapperutil |
---|
| 20 | |
---|
| 21 | def _register_attribute(strategy, mapper, useobject, |
---|
| 22 | compare_function=None, |
---|
| 23 | typecallable=None, |
---|
| 24 | copy_function=None, |
---|
| 25 | mutable_scalars=False, |
---|
| 26 | uselist=False, |
---|
| 27 | callable_=None, |
---|
| 28 | proxy_property=None, |
---|
| 29 | active_history=False, |
---|
| 30 | impl_class=None, |
---|
| 31 | **kw |
---|
| 32 | ): |
---|
| 33 | |
---|
| 34 | prop = strategy.parent_property |
---|
| 35 | attribute_ext = list(util.to_list(prop.extension, default=[])) |
---|
| 36 | |
---|
| 37 | if useobject and prop.single_parent: |
---|
| 38 | attribute_ext.append(_SingleParentValidator(prop)) |
---|
| 39 | |
---|
| 40 | if getattr(prop, 'backref', None): |
---|
| 41 | attribute_ext.append(prop.backref.extension) |
---|
| 42 | |
---|
| 43 | if prop.key in prop.parent._validators: |
---|
| 44 | attribute_ext.append(mapperutil.Validator(prop.key, prop.parent._validators[prop.key])) |
---|
| 45 | |
---|
| 46 | if useobject: |
---|
| 47 | attribute_ext.append(sessionlib.UOWEventHandler(prop.key)) |
---|
| 48 | |
---|
| 49 | for m in mapper.polymorphic_iterator(): |
---|
| 50 | if prop is m._props.get(prop.key): |
---|
| 51 | |
---|
| 52 | attributes.register_attribute_impl( |
---|
| 53 | m.class_, |
---|
| 54 | prop.key, |
---|
| 55 | parent_token=prop, |
---|
| 56 | mutable_scalars=mutable_scalars, |
---|
| 57 | uselist=uselist, |
---|
| 58 | copy_function=copy_function, |
---|
| 59 | compare_function=compare_function, |
---|
| 60 | useobject=useobject, |
---|
| 61 | extension=attribute_ext, |
---|
| 62 | trackparent=useobject, |
---|
| 63 | typecallable=typecallable, |
---|
| 64 | callable_=callable_, |
---|
| 65 | active_history=active_history, |
---|
| 66 | impl_class=impl_class, |
---|
| 67 | **kw |
---|
| 68 | ) |
---|
| 69 | |
---|
| 70 | class UninstrumentedColumnLoader(LoaderStrategy): |
---|
| 71 | """Represent the strategy for a MapperProperty that doesn't instrument the class. |
---|
| 72 | |
---|
| 73 | The polymorphic_on argument of mapper() often results in this, |
---|
| 74 | if the argument is against the with_polymorphic selectable. |
---|
| 75 | |
---|
| 76 | """ |
---|
| 77 | def init(self): |
---|
| 78 | self.columns = self.parent_property.columns |
---|
| 79 | |
---|
| 80 | def setup_query(self, context, entity, path, adapter, column_collection=None, **kwargs): |
---|
| 81 | for c in self.columns: |
---|
| 82 | if adapter: |
---|
| 83 | c = adapter.columns[c] |
---|
| 84 | column_collection.append(c) |
---|
| 85 | |
---|
| 86 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
| 87 | return (None, None) |
---|
| 88 | |
---|
| 89 | class ColumnLoader(LoaderStrategy): |
---|
| 90 | """Strategize the loading of a plain column-based MapperProperty.""" |
---|
| 91 | |
---|
| 92 | def init(self): |
---|
| 93 | self.columns = self.parent_property.columns |
---|
| 94 | self.is_composite = hasattr(self.parent_property, 'composite_class') |
---|
| 95 | |
---|
| 96 | def setup_query(self, context, entity, path, adapter, column_collection=None, **kwargs): |
---|
| 97 | for c in self.columns: |
---|
| 98 | if adapter: |
---|
| 99 | c = adapter.columns[c] |
---|
| 100 | column_collection.append(c) |
---|
| 101 | |
---|
| 102 | def init_class_attribute(self, mapper): |
---|
| 103 | self.is_class_level = True |
---|
| 104 | coltype = self.columns[0].type |
---|
| 105 | active_history = self.columns[0].primary_key # TODO: check all columns ? check for foreign Key as well? |
---|
| 106 | |
---|
| 107 | _register_attribute(self, mapper, useobject=False, |
---|
| 108 | compare_function=coltype.compare_values, |
---|
| 109 | copy_function=coltype.copy_value, |
---|
| 110 | mutable_scalars=self.columns[0].type.is_mutable(), |
---|
| 111 | active_history = active_history |
---|
| 112 | ) |
---|
| 113 | |
---|
| 114 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
| 115 | key, col = self.key, self.columns[0] |
---|
| 116 | if adapter: |
---|
| 117 | col = adapter.columns[col] |
---|
| 118 | if col is not None and col in row: |
---|
| 119 | def new_execute(state, dict_, row, **flags): |
---|
| 120 | dict_[key] = row[col] |
---|
| 121 | |
---|
| 122 | if self._should_log_debug: |
---|
| 123 | new_execute = self.debug_callable(new_execute, self.logger, |
---|
| 124 | "%s returning active column fetcher" % self, |
---|
| 125 | lambda state, dict_, row, **flags: "%s populating %s" % \ |
---|
| 126 | (self, |
---|
| 127 | mapperutil.state_attribute_str(state, key)) |
---|
| 128 | ) |
---|
| 129 | return (new_execute, None) |
---|
| 130 | else: |
---|
| 131 | def new_execute(state, dict_, row, isnew, **flags): |
---|
| 132 | if isnew: |
---|
| 133 | state.expire_attributes([key]) |
---|
| 134 | if self._should_log_debug: |
---|
| 135 | self.logger.debug("%s deferring load" % self) |
---|
| 136 | return (new_execute, None) |
---|
| 137 | |
---|
| 138 | log.class_logger(ColumnLoader) |
---|
| 139 | |
---|
| 140 | class CompositeColumnLoader(ColumnLoader): |
---|
| 141 | """Strategize the loading of a composite column-based MapperProperty.""" |
---|
| 142 | |
---|
| 143 | def init_class_attribute(self, mapper): |
---|
| 144 | self.is_class_level = True |
---|
| 145 | self.logger.info("%s register managed composite attribute" % self) |
---|
| 146 | |
---|
| 147 | def copy(obj): |
---|
| 148 | if obj is None: |
---|
| 149 | return None |
---|
| 150 | return self.parent_property.composite_class(*obj.__composite_values__()) |
---|
| 151 | |
---|
| 152 | def compare(a, b): |
---|
| 153 | if a is None or b is None: |
---|
| 154 | return a is b |
---|
| 155 | |
---|
| 156 | for col, aprop, bprop in zip(self.columns, |
---|
| 157 | a.__composite_values__(), |
---|
| 158 | b.__composite_values__()): |
---|
| 159 | if not col.type.compare_values(aprop, bprop): |
---|
| 160 | return False |
---|
| 161 | else: |
---|
| 162 | return True |
---|
| 163 | |
---|
| 164 | _register_attribute(self, mapper, useobject=False, |
---|
| 165 | compare_function=compare, |
---|
| 166 | copy_function=copy, |
---|
| 167 | mutable_scalars=True |
---|
| 168 | #active_history ? |
---|
| 169 | ) |
---|
| 170 | |
---|
| 171 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
| 172 | key, columns, composite_class = self.key, self.columns, self.parent_property.composite_class |
---|
| 173 | if adapter: |
---|
| 174 | columns = [adapter.columns[c] for c in columns] |
---|
| 175 | for c in columns: |
---|
| 176 | if c not in row: |
---|
| 177 | def new_execute(state, dict_, row, isnew, **flags): |
---|
| 178 | if isnew: |
---|
| 179 | state.expire_attributes([key]) |
---|
| 180 | if self._should_log_debug: |
---|
| 181 | self.logger.debug("%s deferring load" % self) |
---|
| 182 | return (new_execute, None) |
---|
| 183 | else: |
---|
| 184 | def new_execute(state, dict_, row, **flags): |
---|
| 185 | dict_[key] = composite_class(*[row[c] for c in columns]) |
---|
| 186 | |
---|
| 187 | if self._should_log_debug: |
---|
| 188 | new_execute = self.debug_callable(new_execute, self.logger, |
---|
| 189 | "%s returning active composite column fetcher" % self, |
---|
| 190 | lambda state, dict_, row, **flags: "populating %s" % \ |
---|
| 191 | (mapperutil.state_attribute_str(state, key)) |
---|
| 192 | ) |
---|
| 193 | |
---|
| 194 | return (new_execute, None) |
---|
| 195 | |
---|
| 196 | log.class_logger(CompositeColumnLoader) |
---|
| 197 | |
---|
| 198 | class DeferredColumnLoader(LoaderStrategy): |
---|
| 199 | """Strategize the loading of a deferred column-based MapperProperty.""" |
---|
| 200 | |
---|
| 201 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
| 202 | col = self.columns[0] |
---|
| 203 | if adapter: |
---|
| 204 | col = adapter.columns[col] |
---|
| 205 | if col in row: |
---|
| 206 | return self.parent_property._get_strategy(ColumnLoader).create_row_processor(selectcontext, path, mapper, row, adapter) |
---|
| 207 | |
---|
| 208 | elif not self.is_class_level: |
---|
| 209 | def new_execute(state, dict_, row, **flags): |
---|
| 210 | state.set_callable(self.key, LoadDeferredColumns(state, self.key)) |
---|
| 211 | else: |
---|
| 212 | def new_execute(state, dict_, row, **flags): |
---|
| 213 | # reset state on the key so that deferred callables |
---|
| 214 | # fire off on next access. |
---|
| 215 | state.reset(self.key, dict_) |
---|
| 216 | |
---|
| 217 | if self._should_log_debug: |
---|
| 218 | new_execute = self.debug_callable(new_execute, self.logger, None, |
---|
| 219 | lambda state, dict_, row, **flags: "set deferred callable on %s" % \ |
---|
| 220 | mapperutil.state_attribute_str(state, self.key) |
---|
| 221 | ) |
---|
| 222 | return (new_execute, None) |
---|
| 223 | |
---|
| 224 | def init(self): |
---|
| 225 | if hasattr(self.parent_property, 'composite_class'): |
---|
| 226 | raise NotImplementedError("Deferred loading for composite types not implemented yet") |
---|
| 227 | self.columns = self.parent_property.columns |
---|
| 228 | self.group = self.parent_property.group |
---|
| 229 | |
---|
| 230 | def init_class_attribute(self, mapper): |
---|
| 231 | self.is_class_level = True |
---|
| 232 | |
---|
| 233 | _register_attribute(self, mapper, useobject=False, |
---|
| 234 | compare_function=self.columns[0].type.compare_values, |
---|
| 235 | copy_function=self.columns[0].type.copy_value, |
---|
| 236 | mutable_scalars=self.columns[0].type.is_mutable(), |
---|
| 237 | callable_=self._class_level_loader, |
---|
| 238 | dont_expire_missing=True |
---|
| 239 | ) |
---|
| 240 | |
---|
| 241 | def setup_query(self, context, entity, path, adapter, only_load_props=None, **kwargs): |
---|
| 242 | if \ |
---|
| 243 | (self.group is not None and context.attributes.get(('undefer', self.group), False)) or \ |
---|
| 244 | (only_load_props and self.key in only_load_props): |
---|
| 245 | |
---|
| 246 | self.parent_property._get_strategy(ColumnLoader).setup_query(context, entity, path, adapter, **kwargs) |
---|
| 247 | |
---|
| 248 | def _class_level_loader(self, state): |
---|
| 249 | if not mapperutil._state_has_identity(state): |
---|
| 250 | return None |
---|
| 251 | |
---|
| 252 | return LoadDeferredColumns(state, self.key) |
---|
| 253 | |
---|
| 254 | |
---|
| 255 | log.class_logger(DeferredColumnLoader) |
---|
| 256 | |
---|
| 257 | class LoadDeferredColumns(object): |
---|
| 258 | """serializable loader object used by DeferredColumnLoader""" |
---|
| 259 | |
---|
| 260 | def __init__(self, state, key): |
---|
| 261 | self.state, self.key = state, key |
---|
| 262 | |
---|
| 263 | def __call__(self): |
---|
| 264 | state = self.state |
---|
| 265 | |
---|
| 266 | |
---|
| 267 | localparent = mapper._state_mapper(state) |
---|
| 268 | |
---|
| 269 | prop = localparent.get_property(self.key) |
---|
| 270 | strategy = prop._get_strategy(DeferredColumnLoader) |
---|
| 271 | |
---|
| 272 | if strategy.group: |
---|
| 273 | toload = [ |
---|
| 274 | p.key for p in |
---|
| 275 | localparent.iterate_properties |
---|
| 276 | if isinstance(p, StrategizedProperty) and |
---|
| 277 | isinstance(p.strategy, DeferredColumnLoader) and |
---|
| 278 | p.group==strategy.group |
---|
| 279 | ] |
---|
| 280 | else: |
---|
| 281 | toload = [self.key] |
---|
| 282 | |
---|
| 283 | # narrow the keys down to just those which have no history |
---|
| 284 | group = [k for k in toload if k in state.unmodified] |
---|
| 285 | |
---|
| 286 | if strategy._should_log_debug: |
---|
| 287 | strategy.logger.debug( |
---|
| 288 | "deferred load %s group %s" % |
---|
| 289 | (mapperutil.state_attribute_str(state, self.key), group and ','.join(group) or 'None') |
---|
| 290 | ) |
---|
| 291 | |
---|
| 292 | session = sessionlib._state_session(state) |
---|
| 293 | if session is None: |
---|
| 294 | raise sa_exc.UnboundExecutionError( |
---|
| 295 | "Parent instance %s is not bound to a Session; " |
---|
| 296 | "deferred load operation of attribute '%s' cannot proceed" % |
---|
| 297 | (mapperutil.state_str(state), self.key) |
---|
| 298 | ) |
---|
| 299 | |
---|
| 300 | query = session.query(localparent) |
---|
| 301 | ident = state.key[1] |
---|
| 302 | query._get(None, ident=ident, only_load_props=group, refresh_state=state) |
---|
| 303 | return attributes.ATTR_WAS_SET |
---|
| 304 | |
---|
| 305 | class DeferredOption(StrategizedOption): |
---|
| 306 | propagate_to_loaders = True |
---|
| 307 | |
---|
| 308 | def __init__(self, key, defer=False): |
---|
| 309 | super(DeferredOption, self).__init__(key) |
---|
| 310 | self.defer = defer |
---|
| 311 | |
---|
| 312 | def get_strategy_class(self): |
---|
| 313 | if self.defer: |
---|
| 314 | return DeferredColumnLoader |
---|
| 315 | else: |
---|
| 316 | return ColumnLoader |
---|
| 317 | |
---|
| 318 | class UndeferGroupOption(MapperOption): |
---|
| 319 | propagate_to_loaders = True |
---|
| 320 | |
---|
| 321 | def __init__(self, group): |
---|
| 322 | self.group = group |
---|
| 323 | def process_query(self, query): |
---|
| 324 | query._attributes[('undefer', self.group)] = True |
---|
| 325 | |
---|
| 326 | class AbstractRelationLoader(LoaderStrategy): |
---|
| 327 | """LoaderStratgies which deal with related objects as opposed to scalars.""" |
---|
| 328 | |
---|
| 329 | def init(self): |
---|
| 330 | for attr in ['mapper', 'target', 'table', 'uselist']: |
---|
| 331 | setattr(self, attr, getattr(self.parent_property, attr)) |
---|
| 332 | |
---|
| 333 | def _init_instance_attribute(self, state, callable_=None): |
---|
| 334 | if callable_: |
---|
| 335 | state.set_callable(self.key, callable_) |
---|
| 336 | else: |
---|
| 337 | state.initialize(self.key) |
---|
| 338 | |
---|
| 339 | class NoLoader(AbstractRelationLoader): |
---|
| 340 | """Strategize a relation() that doesn't load data automatically.""" |
---|
| 341 | |
---|
| 342 | def init_class_attribute(self, mapper): |
---|
| 343 | self.is_class_level = True |
---|
| 344 | |
---|
| 345 | _register_attribute(self, mapper, |
---|
| 346 | useobject=True, |
---|
| 347 | uselist=self.parent_property.uselist, |
---|
| 348 | typecallable = self.parent_property.collection_class, |
---|
| 349 | ) |
---|
| 350 | |
---|
| 351 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
| 352 | def new_execute(state, dict_, row, **flags): |
---|
| 353 | self._init_instance_attribute(state) |
---|
| 354 | |
---|
| 355 | if self._should_log_debug: |
---|
| 356 | new_execute = self.debug_callable(new_execute, self.logger, None, |
---|
| 357 | lambda state, dict_, row, **flags: "initializing blank scalar/collection on %s" % \ |
---|
| 358 | mapperutil.state_attribute_str(state, self.key) |
---|
| 359 | ) |
---|
| 360 | return (new_execute, None) |
---|
| 361 | |
---|
| 362 | log.class_logger(NoLoader) |
---|
| 363 | |
---|
| 364 | class LazyLoader(AbstractRelationLoader): |
---|
| 365 | """Strategize a relation() that loads when first accessed.""" |
---|
| 366 | |
---|
| 367 | def init(self): |
---|
| 368 | super(LazyLoader, self).init() |
---|
| 369 | (self.__lazywhere, self.__bind_to_col, self._equated_columns) = self._create_lazy_clause(self.parent_property) |
---|
| 370 | |
---|
| 371 | self.logger.info("%s lazy loading clause %s" % (self, self.__lazywhere)) |
---|
| 372 | |
---|
| 373 | # determine if our "lazywhere" clause is the same as the mapper's |
---|
| 374 | # get() clause. then we can just use mapper.get() |
---|
| 375 | #from sqlalchemy.orm import query |
---|
| 376 | self.use_get = not self.uselist and self.mapper._get_clause[0].compare(self.__lazywhere) |
---|
| 377 | if self.use_get: |
---|
| 378 | self.logger.info("%s will use query.get() to optimize instance loads" % self) |
---|
| 379 | |
---|
| 380 | def init_class_attribute(self, mapper): |
---|
| 381 | self.is_class_level = True |
---|
| 382 | |
---|
| 383 | # MANYTOONE currently only needs the "old" value for delete-orphan |
---|
| 384 | # cascades. the required _SingleParentValidator will enable active_history |
---|
| 385 | # in that case. otherwise we don't need the "old" value during backref operations. |
---|
| 386 | _register_attribute(self, |
---|
| 387 | mapper, |
---|
| 388 | useobject=True, |
---|
| 389 | callable_=self._class_level_loader, |
---|
| 390 | uselist = self.parent_property.uselist, |
---|
| 391 | typecallable = self.parent_property.collection_class, |
---|
| 392 | active_history = self.parent_property.direction is not interfaces.MANYTOONE, |
---|
| 393 | ) |
---|
| 394 | |
---|
| 395 | def lazy_clause(self, state, reverse_direction=False, alias_secondary=False, adapt_source=None): |
---|
| 396 | if state is None: |
---|
| 397 | return self._lazy_none_clause(reverse_direction, adapt_source=adapt_source) |
---|
| 398 | |
---|
| 399 | if not reverse_direction: |
---|
| 400 | (criterion, bind_to_col, rev) = (self.__lazywhere, self.__bind_to_col, self._equated_columns) |
---|
| 401 | else: |
---|
| 402 | (criterion, bind_to_col, rev) = LazyLoader._create_lazy_clause(self.parent_property, reverse_direction=reverse_direction) |
---|
| 403 | |
---|
| 404 | def visit_bindparam(bindparam): |
---|
| 405 | mapper = reverse_direction and self.parent_property.mapper or self.parent_property.parent |
---|
| 406 | if bindparam.key in bind_to_col: |
---|
| 407 | # use the "committed" (database) version to get query column values |
---|
| 408 | # also its a deferred value; so that when used by Query, the committed value is used |
---|
| 409 | # after an autoflush occurs |
---|
| 410 | o = state.obj() # strong ref |
---|
| 411 | bindparam.value = lambda: mapper._get_committed_attr_by_column(o, bind_to_col[bindparam.key]) |
---|
| 412 | |
---|
| 413 | if self.parent_property.secondary and alias_secondary: |
---|
| 414 | criterion = sql_util.ClauseAdapter(self.parent_property.secondary.alias()).traverse(criterion) |
---|
| 415 | |
---|
| 416 | criterion = visitors.cloned_traverse(criterion, {}, {'bindparam':visit_bindparam}) |
---|
| 417 | if adapt_source: |
---|
| 418 | criterion = adapt_source(criterion) |
---|
| 419 | return criterion |
---|
| 420 | |
---|
| 421 | def _lazy_none_clause(self, reverse_direction=False, adapt_source=None): |
---|
| 422 | if not reverse_direction: |
---|
| 423 | (criterion, bind_to_col, rev) = (self.__lazywhere, self.__bind_to_col, self._equated_columns) |
---|
| 424 | else: |
---|
| 425 | (criterion, bind_to_col, rev) = LazyLoader._create_lazy_clause(self.parent_property, reverse_direction=reverse_direction) |
---|
| 426 | |
---|
| 427 | def visit_binary(binary): |
---|
| 428 | mapper = reverse_direction and self.parent_property.mapper or self.parent_property.parent |
---|
| 429 | if isinstance(binary.left, expression._BindParamClause) and binary.left.key in bind_to_col: |
---|
| 430 | # reverse order if the NULL is on the left side |
---|
| 431 | binary.left = binary.right |
---|
| 432 | binary.right = expression.null() |
---|
| 433 | binary.operator = operators.is_ |
---|
| 434 | binary.negate = operators.isnot |
---|
| 435 | elif isinstance(binary.right, expression._BindParamClause) and binary.right.key in bind_to_col: |
---|
| 436 | binary.right = expression.null() |
---|
| 437 | binary.operator = operators.is_ |
---|
| 438 | binary.negate = operators.isnot |
---|
| 439 | |
---|
| 440 | criterion = visitors.cloned_traverse(criterion, {}, {'binary':visit_binary}) |
---|
| 441 | if adapt_source: |
---|
| 442 | criterion = adapt_source(criterion) |
---|
| 443 | return criterion |
---|
| 444 | |
---|
| 445 | def _class_level_loader(self, state): |
---|
| 446 | if not mapperutil._state_has_identity(state): |
---|
| 447 | return None |
---|
| 448 | |
---|
| 449 | return LoadLazyAttribute(state, self.key) |
---|
| 450 | |
---|
| 451 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
| 452 | if not self.is_class_level: |
---|
| 453 | def new_execute(state, dict_, row, **flags): |
---|
| 454 | # we are not the primary manager for this attribute on this class - set up a |
---|
| 455 | # per-instance lazyloader, which will override the class-level behavior. |
---|
| 456 | # this currently only happens when using a "lazyload" option on a "no load" |
---|
| 457 | # attribute - "eager" attributes always have a class-level lazyloader |
---|
| 458 | # installed. |
---|
| 459 | self._init_instance_attribute(state, callable_=LoadLazyAttribute(state, self.key)) |
---|
| 460 | |
---|
| 461 | if self._should_log_debug: |
---|
| 462 | new_execute = self.debug_callable(new_execute, self.logger, None, |
---|
| 463 | lambda state, dict_, row, **flags: "set instance-level lazy loader on %s" % \ |
---|
| 464 | mapperutil.state_attribute_str(state, |
---|
| 465 | self.key) |
---|
| 466 | ) |
---|
| 467 | |
---|
| 468 | return (new_execute, None) |
---|
| 469 | else: |
---|
| 470 | def new_execute(state, dict_, row, **flags): |
---|
| 471 | # we are the primary manager for this attribute on this class - reset its |
---|
| 472 | # per-instance attribute state, so that the class-level lazy loader is |
---|
| 473 | # executed when next referenced on this instance. this is needed in |
---|
| 474 | # populate_existing() types of scenarios to reset any existing state. |
---|
| 475 | state.reset(self.key, dict_) |
---|
| 476 | |
---|
| 477 | if self._should_log_debug: |
---|
| 478 | new_execute = self.debug_callable(new_execute, self.logger, None, |
---|
| 479 | lambda state, dict_, row, **flags: "set class-level lazy loader on %s" % \ |
---|
| 480 | mapperutil.state_attribute_str(state, |
---|
| 481 | self.key) |
---|
| 482 | ) |
---|
| 483 | |
---|
| 484 | return (new_execute, None) |
---|
| 485 | |
---|
| 486 | def _create_lazy_clause(cls, prop, reverse_direction=False): |
---|
| 487 | binds = util.column_dict() |
---|
| 488 | lookup = util.column_dict() |
---|
| 489 | equated_columns = util.column_dict() |
---|
| 490 | |
---|
| 491 | if reverse_direction and not prop.secondaryjoin: |
---|
| 492 | for l, r in prop.local_remote_pairs: |
---|
| 493 | _list = lookup.setdefault(r, []) |
---|
| 494 | _list.append((r, l)) |
---|
| 495 | equated_columns[l] = r |
---|
| 496 | else: |
---|
| 497 | for l, r in prop.local_remote_pairs: |
---|
| 498 | _list = lookup.setdefault(l, []) |
---|
| 499 | _list.append((l, r)) |
---|
| 500 | equated_columns[r] = l |
---|
| 501 | |
---|
| 502 | def col_to_bind(col): |
---|
| 503 | if col in lookup: |
---|
| 504 | for tobind, equated in lookup[col]: |
---|
| 505 | if equated in binds: |
---|
| 506 | return None |
---|
| 507 | if col not in binds: |
---|
| 508 | binds[col] = sql.bindparam(None, None, type_=col.type) |
---|
| 509 | return binds[col] |
---|
| 510 | return None |
---|
| 511 | |
---|
| 512 | lazywhere = prop.primaryjoin |
---|
| 513 | |
---|
| 514 | if not prop.secondaryjoin or not reverse_direction: |
---|
| 515 | lazywhere = visitors.replacement_traverse(lazywhere, {}, col_to_bind) |
---|
| 516 | |
---|
| 517 | if prop.secondaryjoin is not None: |
---|
| 518 | secondaryjoin = prop.secondaryjoin |
---|
| 519 | if reverse_direction: |
---|
| 520 | secondaryjoin = visitors.replacement_traverse(secondaryjoin, {}, col_to_bind) |
---|
| 521 | lazywhere = sql.and_(lazywhere, secondaryjoin) |
---|
| 522 | |
---|
| 523 | bind_to_col = dict((binds[col].key, col) for col in binds) |
---|
| 524 | |
---|
| 525 | return (lazywhere, bind_to_col, equated_columns) |
---|
| 526 | _create_lazy_clause = classmethod(_create_lazy_clause) |
---|
| 527 | |
---|
| 528 | log.class_logger(LazyLoader) |
---|
| 529 | |
---|
| 530 | class LoadLazyAttribute(object): |
---|
| 531 | """serializable loader object used by LazyLoader""" |
---|
| 532 | |
---|
| 533 | def __init__(self, state, key): |
---|
| 534 | self.state, self.key = state, key |
---|
| 535 | |
---|
| 536 | def __getstate__(self): |
---|
| 537 | return (self.state, self.key) |
---|
| 538 | |
---|
| 539 | def __setstate__(self, state): |
---|
| 540 | self.state, self.key = state |
---|
| 541 | |
---|
| 542 | def __call__(self): |
---|
| 543 | state = self.state |
---|
| 544 | |
---|
| 545 | instance_mapper = mapper._state_mapper(state) |
---|
| 546 | prop = instance_mapper.get_property(self.key) |
---|
| 547 | strategy = prop._get_strategy(LazyLoader) |
---|
| 548 | |
---|
| 549 | if strategy._should_log_debug: |
---|
| 550 | strategy.logger.debug("loading %s" % mapperutil.state_attribute_str(state, self.key)) |
---|
| 551 | |
---|
| 552 | session = sessionlib._state_session(state) |
---|
| 553 | if session is None: |
---|
| 554 | raise sa_exc.UnboundExecutionError( |
---|
| 555 | "Parent instance %s is not bound to a Session; " |
---|
| 556 | "lazy load operation of attribute '%s' cannot proceed" % |
---|
| 557 | (mapperutil.state_str(state), self.key) |
---|
| 558 | ) |
---|
| 559 | |
---|
| 560 | q = session.query(prop.mapper)._adapt_all_clauses() |
---|
| 561 | |
---|
| 562 | if state.load_path: |
---|
| 563 | q = q._with_current_path(state.load_path + (self.key,)) |
---|
| 564 | |
---|
| 565 | # if we have a simple primary key load, use mapper.get() |
---|
| 566 | # to possibly save a DB round trip |
---|
| 567 | if strategy.use_get: |
---|
| 568 | ident = [] |
---|
| 569 | allnulls = True |
---|
| 570 | for primary_key in prop.mapper.primary_key: |
---|
| 571 | val = instance_mapper._get_committed_state_attr_by_column(state, strategy._equated_columns[primary_key]) |
---|
| 572 | allnulls = allnulls and val is None |
---|
| 573 | ident.append(val) |
---|
| 574 | if allnulls: |
---|
| 575 | return None |
---|
| 576 | if state.load_options: |
---|
| 577 | q = q._conditional_options(*state.load_options) |
---|
| 578 | return q.get(ident) |
---|
| 579 | |
---|
| 580 | if prop.order_by: |
---|
| 581 | q = q.order_by(*util.to_list(prop.order_by)) |
---|
| 582 | |
---|
| 583 | if state.load_options: |
---|
| 584 | q = q._conditional_options(*state.load_options) |
---|
| 585 | q = q.filter(strategy.lazy_clause(state)) |
---|
| 586 | |
---|
| 587 | result = q.all() |
---|
| 588 | if strategy.uselist: |
---|
| 589 | return result |
---|
| 590 | else: |
---|
| 591 | if result: |
---|
| 592 | return result[0] |
---|
| 593 | else: |
---|
| 594 | return None |
---|
| 595 | |
---|
| 596 | class EagerLoader(AbstractRelationLoader): |
---|
| 597 | """Strategize a relation() that loads within the process of the parent object being selected.""" |
---|
| 598 | |
---|
| 599 | def init(self): |
---|
| 600 | super(EagerLoader, self).init() |
---|
| 601 | self.join_depth = self.parent_property.join_depth |
---|
| 602 | |
---|
| 603 | def init_class_attribute(self, mapper): |
---|
| 604 | self.parent_property._get_strategy(LazyLoader).init_class_attribute(mapper) |
---|
| 605 | |
---|
| 606 | def setup_query(self, context, entity, path, adapter, column_collection=None, parentmapper=None, **kwargs): |
---|
| 607 | """Add a left outer join to the statement thats being constructed.""" |
---|
| 608 | |
---|
| 609 | if not context.enable_eagerloads: |
---|
| 610 | return |
---|
| 611 | |
---|
| 612 | path = path + (self.key,) |
---|
| 613 | |
---|
| 614 | # check for user-defined eager alias |
---|
| 615 | if ("user_defined_eager_row_processor", path) in context.attributes: |
---|
| 616 | clauses = context.attributes[("user_defined_eager_row_processor", path)] |
---|
| 617 | |
---|
| 618 | adapter = entity._get_entity_clauses(context.query, context) |
---|
| 619 | if adapter and clauses: |
---|
| 620 | context.attributes[("user_defined_eager_row_processor", path)] = clauses = clauses.wrap(adapter) |
---|
| 621 | elif adapter: |
---|
| 622 | context.attributes[("user_defined_eager_row_processor", path)] = clauses = adapter |
---|
| 623 | |
---|
| 624 | add_to_collection = context.primary_columns |
---|
| 625 | |
---|
| 626 | else: |
---|
| 627 | clauses = self._create_eager_join(context, entity, path, adapter, parentmapper) |
---|
| 628 | if not clauses: |
---|
| 629 | return |
---|
| 630 | |
---|
| 631 | context.attributes[("eager_row_processor", path)] = clauses |
---|
| 632 | |
---|
| 633 | add_to_collection = context.secondary_columns |
---|
| 634 | |
---|
| 635 | for value in self.mapper._iterate_polymorphic_properties(): |
---|
| 636 | value.setup(context, entity, path + (self.mapper.base_mapper,), clauses, parentmapper=self.mapper, column_collection=add_to_collection) |
---|
| 637 | |
---|
| 638 | def _create_eager_join(self, context, entity, path, adapter, parentmapper): |
---|
| 639 | # check for join_depth or basic recursion, |
---|
| 640 | # if the current path was not explicitly stated as |
---|
| 641 | # a desired "loaderstrategy" (i.e. via query.options()) |
---|
| 642 | if ("loaderstrategy", path) not in context.attributes: |
---|
| 643 | if self.join_depth: |
---|
| 644 | if len(path) / 2 > self.join_depth: |
---|
| 645 | return |
---|
| 646 | else: |
---|
| 647 | if self.mapper.base_mapper in path: |
---|
| 648 | return |
---|
| 649 | |
---|
| 650 | if parentmapper is None: |
---|
| 651 | localparent = entity.mapper |
---|
| 652 | else: |
---|
| 653 | localparent = parentmapper |
---|
| 654 | |
---|
| 655 | # whether or not the Query will wrap the selectable in a subquery, |
---|
| 656 | # and then attach eager load joins to that (i.e., in the case of LIMIT/OFFSET etc.) |
---|
| 657 | should_nest_selectable = context.query._should_nest_selectable |
---|
| 658 | |
---|
| 659 | if entity in context.eager_joins: |
---|
| 660 | entity_key, default_towrap = entity, entity.selectable |
---|
| 661 | |
---|
| 662 | elif should_nest_selectable or not context.from_clause: |
---|
| 663 | # if no from_clause, or a subquery is going to be generated, |
---|
| 664 | # store eager joins per _MappedEntity; Query._compile_context will |
---|
| 665 | # add them as separate selectables to the select(), or splice them together |
---|
| 666 | # after the subquery is generated |
---|
| 667 | entity_key, default_towrap = entity, entity.selectable |
---|
| 668 | else: |
---|
| 669 | index, clause = sql_util.find_join_source(context.from_clause, entity.selectable) |
---|
| 670 | if clause: |
---|
| 671 | # join to an existing FROM clause on the query. |
---|
| 672 | # key it to its list index in the eager_joins dict. |
---|
| 673 | # Query._compile_context will adapt as needed and append to the |
---|
| 674 | # FROM clause of the select(). |
---|
| 675 | entity_key, default_towrap = index, clause |
---|
| 676 | else: |
---|
| 677 | # if no from_clause to join to, |
---|
| 678 | # store eager joins per _MappedEntity |
---|
| 679 | entity_key, default_towrap = entity, entity.selectable |
---|
| 680 | |
---|
| 681 | towrap = context.eager_joins.setdefault(entity_key, default_towrap) |
---|
| 682 | |
---|
| 683 | # create AliasedClauses object to build up the eager query. |
---|
| 684 | clauses = mapperutil.ORMAdapter(mapperutil.AliasedClass(self.mapper), |
---|
| 685 | equivalents=self.mapper._equivalent_columns, adapt_required=True) |
---|
| 686 | |
---|
| 687 | join_to_left = False |
---|
| 688 | if adapter: |
---|
| 689 | if getattr(adapter, 'aliased_class', None): |
---|
| 690 | onclause = getattr(adapter.aliased_class, self.key, self.parent_property) |
---|
| 691 | else: |
---|
| 692 | onclause = getattr(mapperutil.AliasedClass(self.parent, adapter.selectable), self.key, self.parent_property) |
---|
| 693 | |
---|
| 694 | if onclause is self.parent_property: |
---|
| 695 | # TODO: this is a temporary hack to account for polymorphic eager loads where |
---|
| 696 | # the eagerload is referencing via of_type(). |
---|
| 697 | join_to_left = True |
---|
| 698 | else: |
---|
| 699 | onclause = self.parent_property |
---|
| 700 | |
---|
| 701 | context.eager_joins[entity_key] = eagerjoin = mapperutil.outerjoin(towrap, clauses.aliased_class, onclause, join_to_left=join_to_left) |
---|
| 702 | |
---|
| 703 | # send a hint to the Query as to where it may "splice" this join |
---|
| 704 | eagerjoin.stop_on = entity.selectable |
---|
| 705 | |
---|
| 706 | if not self.parent_property.secondary and context.query._should_nest_selectable and not parentmapper: |
---|
| 707 | # for parentclause that is the non-eager end of the join, |
---|
| 708 | # ensure all the parent cols in the primaryjoin are actually in the |
---|
| 709 | # columns clause (i.e. are not deferred), so that aliasing applied by the Query propagates |
---|
| 710 | # those columns outward. This has the effect of "undefering" those columns. |
---|
| 711 | for col in sql_util.find_columns(self.parent_property.primaryjoin): |
---|
| 712 | if localparent.mapped_table.c.contains_column(col): |
---|
| 713 | if adapter: |
---|
| 714 | col = adapter.columns[col] |
---|
| 715 | context.primary_columns.append(col) |
---|
| 716 | |
---|
| 717 | if self.parent_property.order_by: |
---|
| 718 | context.eager_order_by += eagerjoin._target_adapter.copy_and_process(util.to_list(self.parent_property.order_by)) |
---|
| 719 | |
---|
| 720 | return clauses |
---|
| 721 | |
---|
| 722 | def _create_eager_adapter(self, context, row, adapter, path): |
---|
| 723 | if ("user_defined_eager_row_processor", path) in context.attributes: |
---|
| 724 | decorator = context.attributes[("user_defined_eager_row_processor", path)] |
---|
| 725 | # user defined eagerloads are part of the "primary" portion of the load. |
---|
| 726 | # the adapters applied to the Query should be honored. |
---|
| 727 | if context.adapter and decorator: |
---|
| 728 | decorator = decorator.wrap(context.adapter) |
---|
| 729 | elif context.adapter: |
---|
| 730 | decorator = context.adapter |
---|
| 731 | elif ("eager_row_processor", path) in context.attributes: |
---|
| 732 | decorator = context.attributes[("eager_row_processor", path)] |
---|
| 733 | else: |
---|
| 734 | if self._should_log_debug: |
---|
| 735 | self.logger.debug("Could not locate aliased clauses for key: " + str(path)) |
---|
| 736 | return False |
---|
| 737 | |
---|
| 738 | try: |
---|
| 739 | identity_key = self.mapper.identity_key_from_row(row, decorator) |
---|
| 740 | return decorator |
---|
| 741 | except KeyError, k: |
---|
| 742 | # no identity key - dont return a row processor, will cause a degrade to lazy |
---|
| 743 | if self._should_log_debug: |
---|
| 744 | self.logger.debug("could not locate identity key from row; missing column '%s'" % k) |
---|
| 745 | return False |
---|
| 746 | |
---|
| 747 | def create_row_processor(self, context, path, mapper, row, adapter): |
---|
| 748 | path = path + (self.key,) |
---|
| 749 | |
---|
| 750 | eager_adapter = self._create_eager_adapter(context, row, adapter, path) |
---|
| 751 | |
---|
| 752 | if eager_adapter is not False: |
---|
| 753 | key = self.key |
---|
| 754 | _instance = self.mapper._instance_processor(context, path + (self.mapper.base_mapper,), eager_adapter) |
---|
| 755 | |
---|
| 756 | if not self.uselist: |
---|
| 757 | def execute(state, dict_, row, isnew, **flags): |
---|
| 758 | if isnew: |
---|
| 759 | # set a scalar object instance directly on the |
---|
| 760 | # parent object, bypassing InstrumentedAttribute |
---|
| 761 | # event handlers. |
---|
| 762 | dict_[key] = _instance(row, None) |
---|
| 763 | else: |
---|
| 764 | # call _instance on the row, even though the object has been created, |
---|
| 765 | # so that we further descend into properties |
---|
| 766 | _instance(row, None) |
---|
| 767 | else: |
---|
| 768 | def execute(state, dict_, row, isnew, **flags): |
---|
| 769 | if isnew or (state, key) not in context.attributes: |
---|
| 770 | # appender_key can be absent from context.attributes with isnew=False |
---|
| 771 | # when self-referential eager loading is used; the same instance may be present |
---|
| 772 | # in two distinct sets of result columns |
---|
| 773 | |
---|
| 774 | collection = attributes.init_state_collection(state, dict_, key) |
---|
| 775 | appender = util.UniqueAppender(collection, 'append_without_event') |
---|
| 776 | |
---|
| 777 | context.attributes[(state, key)] = appender |
---|
| 778 | |
---|
| 779 | result_list = context.attributes[(state, key)] |
---|
| 780 | |
---|
| 781 | _instance(row, result_list) |
---|
| 782 | |
---|
| 783 | if self._should_log_debug: |
---|
| 784 | execute = self.debug_callable(execute, self.logger, |
---|
| 785 | "%s returning eager instance loader" % self, |
---|
| 786 | lambda state, dict_, row, isnew, **flags: "%s eagerload %s" % \ |
---|
| 787 | (self, |
---|
| 788 | self.uselist and "scalar attribute" |
---|
| 789 | or "collection") |
---|
| 790 | ) |
---|
| 791 | |
---|
| 792 | return (execute, execute) |
---|
| 793 | else: |
---|
| 794 | if self._should_log_debug: |
---|
| 795 | self.logger.debug("%s degrading to lazy loader" % self) |
---|
| 796 | return self.parent_property._get_strategy(LazyLoader).create_row_processor(context, path, mapper, row, adapter) |
---|
| 797 | |
---|
| 798 | log.class_logger(EagerLoader) |
---|
| 799 | |
---|
| 800 | class EagerLazyOption(StrategizedOption): |
---|
| 801 | |
---|
| 802 | def __init__(self, key, lazy=True, chained=False, mapper=None, propagate_to_loaders=True): |
---|
| 803 | super(EagerLazyOption, self).__init__(key, mapper) |
---|
| 804 | self.lazy = lazy |
---|
| 805 | self.chained = chained |
---|
| 806 | self.propagate_to_loaders = propagate_to_loaders |
---|
| 807 | |
---|
| 808 | def is_chained(self): |
---|
| 809 | return not self.lazy and self.chained |
---|
| 810 | |
---|
| 811 | def get_strategy_class(self): |
---|
| 812 | if self.lazy: |
---|
| 813 | return LazyLoader |
---|
| 814 | elif self.lazy is False: |
---|
| 815 | return EagerLoader |
---|
| 816 | elif self.lazy is None: |
---|
| 817 | return NoLoader |
---|
| 818 | |
---|
| 819 | class LoadEagerFromAliasOption(PropertyOption): |
---|
| 820 | |
---|
| 821 | def __init__(self, key, alias=None): |
---|
| 822 | super(LoadEagerFromAliasOption, self).__init__(key) |
---|
| 823 | if alias: |
---|
| 824 | if not isinstance(alias, basestring): |
---|
| 825 | m, alias, is_aliased_class = mapperutil._entity_info(alias) |
---|
| 826 | self.alias = alias |
---|
| 827 | |
---|
| 828 | def process_query_property(self, query, paths, mappers): |
---|
| 829 | if self.alias: |
---|
| 830 | if isinstance(self.alias, basestring): |
---|
| 831 | mapper = mappers[-1] |
---|
| 832 | (root_mapper, propname) = paths[-1][-2:] |
---|
| 833 | prop = mapper.get_property(propname, resolve_synonyms=True) |
---|
| 834 | self.alias = prop.target.alias(self.alias) |
---|
| 835 | query._attributes[("user_defined_eager_row_processor", paths[-1])] = sql_util.ColumnAdapter(self.alias) |
---|
| 836 | else: |
---|
| 837 | (root_mapper, propname) = paths[-1][-2:] |
---|
| 838 | mapper = mappers[-1] |
---|
| 839 | prop = mapper.get_property(propname, resolve_synonyms=True) |
---|
| 840 | adapter = query._polymorphic_adapters.get(prop.mapper, None) |
---|
| 841 | query._attributes[("user_defined_eager_row_processor", paths[-1])] = adapter |
---|
| 842 | |
---|
| 843 | class _SingleParentValidator(interfaces.AttributeExtension): |
---|
| 844 | def __init__(self, prop): |
---|
| 845 | self.prop = prop |
---|
| 846 | |
---|
| 847 | def _do_check(self, state, value, oldvalue, initiator): |
---|
| 848 | if value is not None: |
---|
| 849 | hasparent = initiator.hasparent(attributes.instance_state(value)) |
---|
| 850 | if hasparent and oldvalue is not value: |
---|
| 851 | raise sa_exc.InvalidRequestError("Instance %s is already associated with an instance " |
---|
| 852 | "of %s via its %s attribute, and is only allowed a single parent." % |
---|
| 853 | (mapperutil.instance_str(value), state.class_, self.prop) |
---|
| 854 | ) |
---|
| 855 | return value |
---|
| 856 | |
---|
| 857 | def append(self, state, value, initiator): |
---|
| 858 | return self._do_check(state, value, None, initiator) |
---|
| 859 | |
---|
| 860 | def set(self, state, value, oldvalue, initiator): |
---|
| 861 | return self._do_check(state, value, oldvalue, initiator) |
---|
| 862 | |
---|
| 863 | |
---|