[Sqlalchemy-commits] sqlalchemy: - [feature] The of_type() construct on attributes
Brought to you by:
zzzeek
From: <co...@sq...> - 2012-06-20 23:29:35
|
details: http://hg.sqlalchemy.org/sqlalchemy/sqlalchemy/rev/fa24d5eb5270 changeset: 8393:fa24d5eb5270 user: Mike Bayer <mi...@zz...> date: Wed Jun 20 19:28:29 2012 -0400 description: - [feature] The of_type() construct on attributes now accepts aliased() class constructs as well as with_polymorphic constructs, and works with query.join(), any(), has(), and also eager loaders subqueryload(), joinedload(), contains_eager() [ticket:2438] [ticket:1106] - a rewrite of the query path system to use an object based approach for more succinct usage. the system has been designed carefully to not add an excessive method overhead. - [feature] select() features a correlate_except() method, auto correlates all selectables except those passed. Is needed here for the updated any()/has() functionality. - remove some old cruft from LoaderStrategy, init(),debug_callable() - use a namedtuple for _extended_entity_info. This method should become standard within the orm internals - some tweaks to the memory profile tests, number of runs can be customized to work around pysqlite's very annoying behavior - try to simplify PropertyOption._get_paths(), rename to _process_paths(), returns a single list now. overall works more completely as was needed for of_type() functionality diffstat: CHANGES | 12 + doc/build/orm/inheritance.rst | 35 ++ lib/sqlalchemy/orm/__init__.py | 3 +- lib/sqlalchemy/orm/attributes.py | 13 +- lib/sqlalchemy/orm/interfaces.py | 187 +++++-------- lib/sqlalchemy/orm/mapper.py | 75 +++-- lib/sqlalchemy/orm/properties.py | 22 +- lib/sqlalchemy/orm/query.py | 68 ++-- lib/sqlalchemy/orm/state.py | 6 +- lib/sqlalchemy/orm/strategies.py | 354 +++++++++++++++----------- lib/sqlalchemy/orm/util.py | 204 +++++++++++++- lib/sqlalchemy/sql/expression.py | 32 +- lib/sqlalchemy/util/__init__.py | 2 +- lib/sqlalchemy/util/compat.py | 12 + test/aaa_profiling/test_memusage.py | 158 ++++++++--- test/orm/inheritance/_poly_fixtures.py | 48 +++ test/orm/inheritance/test_polymorphic_rel.py | 184 ++++--------- test/orm/test_merge.py | 6 +- test/orm/test_pickled.py | 14 +- test/orm/test_query.py | 92 +++--- test/orm/test_subquery_relations.py | 2 - test/perf/orm2010.py | 4 +- 22 files changed, 923 insertions(+), 610 deletions(-) diffs (truncated from 3237 to 300 lines): diff -r 8e78a8a7c730 -r fa24d5eb5270 CHANGES --- a/CHANGES Wed Jun 20 18:55:13 2012 -0400 +++ b/CHANGES Wed Jun 20 19:28:29 2012 -0400 @@ -49,6 +49,14 @@ of a join in place of the "of_type()" modifier. [ticket:2333] + - [feature] The of_type() construct on attributes + now accepts aliased() class constructs as well + as with_polymorphic constructs, and works with + query.join(), any(), has(), and also + eager loaders subqueryload(), joinedload(), + contains_eager() + [ticket:2438] [ticket:1106] + - [feature] The "deferred declarative reflection" system has been moved into the declarative extension itself, using the @@ -296,6 +304,10 @@ that aren't in the target table is now an exception. [ticket:2415] + - [feature] select() features a correlate_except() + method, auto correlates all selectables except those + passed. + - [bug] All of UniqueConstraint, ForeignKeyConstraint, CheckConstraint, and PrimaryKeyConstraint will attach themselves to their parent table automatically diff -r 8e78a8a7c730 -r fa24d5eb5270 doc/build/orm/inheritance.rst --- a/doc/build/orm/inheritance.rst Wed Jun 20 18:55:13 2012 -0400 +++ b/doc/build/orm/inheritance.rst Wed Jun 20 19:28:29 2012 -0400 @@ -423,6 +423,20 @@ FROM x JOIN (SELECT * FROM y JOIN z ON <onclause>) AS anon_1 ON <onclause> +The above join can also be expressed more succinctly by combining ``of_type()`` +with the polymorphic construct:: + + manager_and_engineer = with_polymorphic( + Employee, [Manager, Engineer], + aliased=True) + + session.query(Company).\ + join(Company.employees.of_type(manager_and_engineer)).\ + filter( + or_(manager_and_engineer.Engineer.engineer_info=='someinfo', + manager_and_engineer.Manager.manager_data=='somedata') + ) + The ``any()`` and ``has()`` operators also can be used with :func:`~sqlalchemy.orm.interfaces.PropComparator.of_type` when the embedded criterion is in terms of a subclass:: @@ -448,6 +462,28 @@ ``engineers``, and also specifies criterion which correlates the EXISTS subselect back to the parent ``companies`` table. +.. versionadded:: 0.8 + :func:`~sqlalchemy.orm.interfaces.PropComparator.of_type` accepts + :func:`.orm.aliased` and :func:`.orm.with_polymorphic` constructs in conjunction + with :meth:`.Query.join`, ``any()`` and ``has()``. + +Eager Loading of Specific Subtypes +++++++++++++++++++++++++++++++++++ + +The :func:`.joinedload` and :func:`.subqueryload` options also support +paths which make use of :func:`~sqlalchemy.orm.interfaces.PropComparator.of_type`. +Below we load ``Company`` rows while eagerly loading related ``Engineer`` +objects, querying the ``employee`` and ``engineer`` tables simultaneously:: + + session.query(Company).\ + options(subqueryload_all(Company.employees.of_type(Engineer), + Engineer.machines)) + +.. versionadded:: 0.8 + :func:`.joinedload` and :func:`.subqueryload` support + paths that are qualified with + :func:`~sqlalchemy.orm.interfaces.PropComparator.of_type`. + Single Table Inheritance ------------------------ diff -r 8e78a8a7c730 -r fa24d5eb5270 lib/sqlalchemy/orm/__init__.py --- a/lib/sqlalchemy/orm/__init__.py Wed Jun 20 18:55:13 2012 -0400 +++ b/lib/sqlalchemy/orm/__init__.py Wed Jun 20 19:28:29 2012 -0400 @@ -112,7 +112,8 @@ 'synonym', 'undefer', 'undefer_group', - 'validates' + 'validates', + 'with_polymorphic' ) diff -r 8e78a8a7c730 -r fa24d5eb5270 lib/sqlalchemy/orm/attributes.py --- a/lib/sqlalchemy/orm/attributes.py Wed Jun 20 18:55:13 2012 -0400 +++ b/lib/sqlalchemy/orm/attributes.py Wed Jun 20 19:28:29 2012 -0400 @@ -103,12 +103,14 @@ """Base class for class-bound attributes. """ def __init__(self, class_, key, impl=None, - comparator=None, parententity=None): + comparator=None, parententity=None, + of_type=None): self.class_ = class_ self.key = key self.impl = impl self.comparator = comparator self.parententity = parententity + self._of_type = of_type manager = manager_of_class(class_) # manager is None in the case of AliasedClass @@ -137,6 +139,15 @@ def __clause_element__(self): return self.comparator.__clause_element__() + def of_type(self, cls): + return QueryableAttribute( + self.class_, + self.key, + self.impl, + self.comparator.of_type(cls), + self.parententity, + of_type=cls) + def label(self, name): return self.__clause_element__().label(name) diff -r 8e78a8a7c730 -r fa24d5eb5270 lib/sqlalchemy/orm/interfaces.py --- a/lib/sqlalchemy/orm/interfaces.py Wed Jun 20 18:55:13 2012 -0400 +++ b/lib/sqlalchemy/orm/interfaces.py Wed Jun 20 19:28:29 2012 -0400 @@ -42,7 +42,6 @@ 'SessionExtension', 'StrategizedOption', 'StrategizedProperty', - 'build_path', ) EXT_CONTINUE = util.symbol('EXT_CONTINUE') @@ -77,7 +76,7 @@ """ - def setup(self, context, entity, path, reduced_path, adapter, **kwargs): + def setup(self, context, entity, path, adapter, **kwargs): """Called by Query for the purposes of constructing a SQL statement. Each MapperProperty associated with the target mapper processes the @@ -87,7 +86,7 @@ pass - def create_row_processor(self, context, path, reduced_path, + def create_row_processor(self, context, path, mapper, row, adapter): """Return a 3-tuple consisting of three row processing functions. @@ -112,7 +111,7 @@ def set_parent(self, parent, init): self.parent = parent - def instrument_class(self, mapper): + def instrument_class(self, mapper): # pragma: no-coverage raise NotImplementedError() _compile_started = False @@ -308,15 +307,23 @@ strategy_wildcard_key = None - def _get_context_strategy(self, context, reduced_path): - key = ('loaderstrategy', reduced_path) + @util.memoized_property + def _wildcard_path(self): + if self.strategy_wildcard_key: + return ('loaderstrategy', (self.strategy_wildcard_key,)) + else: + return None + + def _get_context_strategy(self, context, path): + # this is essentially performance inlining. + key = ('loaderstrategy', path.reduced_path + (self.key,)) cls = None if key in context.attributes: cls = context.attributes[key] - elif self.strategy_wildcard_key: - key = ('loaderstrategy', (self.strategy_wildcard_key,)) - if key in context.attributes: - cls = context.attributes[key] + else: + wc_key = self._wildcard_path + if wc_key and wc_key in context.attributes: + cls = context.attributes[wc_key] if cls: try: @@ -335,15 +342,15 @@ self._strategies[cls] = strategy = cls(self) return strategy - def setup(self, context, entity, path, reduced_path, adapter, **kwargs): - self._get_context_strategy(context, reduced_path + (self.key,)).\ + def setup(self, context, entity, path, adapter, **kwargs): + self._get_context_strategy(context, path).\ setup_query(context, entity, path, - reduced_path, adapter, **kwargs) + adapter, **kwargs) - def create_row_processor(self, context, path, reduced_path, mapper, row, adapter): - return self._get_context_strategy(context, reduced_path + (self.key,)).\ + def create_row_processor(self, context, path, mapper, row, adapter): + return self._get_context_strategy(context, path).\ create_row_processor(context, path, - reduced_path, mapper, row, adapter) + mapper, row, adapter) def do_init(self): self._strategies = {} @@ -354,30 +361,6 @@ not mapper.class_manager._attr_has_impl(self.key): self.strategy.init_class_attribute(mapper) -def build_path(entity, key, prev=None): - if prev: - return prev + (entity, key) - else: - return (entity, key) - -def serialize_path(path): - if path is None: - return None - - return zip( - [m.class_ for m in [path[i] for i in range(0, len(path), 2)]], - [path[i] for i in range(1, len(path), 2)] + [None] - ) - -def deserialize_path(path): - if path is None: - return None - - p = tuple(chain(*[(mapperutil.class_mapper(cls), key) for cls, key in path])) - if p and p[-1] is None: - p = p[0:-1] - return p - class MapperOption(object): """Describe a modification to a Query.""" @@ -414,11 +397,11 @@ self._process(query, False) def _process(self, query, raiseerr): - paths, mappers = self._get_paths(query, raiseerr) + paths = self._process_paths(query, raiseerr) if paths: - self.process_query_property(query, paths, mappers) + self.process_query_property(query, paths) - def process_query_property(self, query, paths, mappers): + def process_query_property(self, query, paths): pass def __getstate__(self): @@ -450,8 +433,7 @@ searchfor = mapperutil._class_to_mapper(mapper) isa = True for ent in query._mapper_entities: - if searchfor is ent.path_entity or isa \ - and searchfor.common_parent(ent.path_entity): + if ent.corresponds_to(searchfor): return ent else: if raiseerr: @@ -488,15 +470,21 @@ else: return None - def _get_paths(self, query, raiseerr): - path = None + def _process_paths(self, query, raiseerr): + """reconcile the 'key' for this PropertyOption with + the current path and entities of the query. + + Return a list of affected paths. + + """ + path = mapperutil.PathRegistry.root entity = None - l = [] - mappers = [] + paths = [] + no_result = [] # _current_path implies we're in a # secondary load with an existing path - current_path = list(query._current_path) + current_path = list(query._current_path.path) |