diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-04-23 22:17:25 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-04-23 22:17:25 -0400 |
| commit | cb3913a186a01d9425e0ba97de89aa6d7d64ab96 (patch) | |
| tree | 7616eade5c09cb0b6e833d8e881f795f4ea965f5 /lib/sqlalchemy | |
| parent | 841ea194bd7cf239323ee21320210fd6dc5c551d (diff) | |
| download | sqlalchemy-cb3913a186a01d9425e0ba97de89aa6d7d64ab96.tar.gz | |
- [feature] New standalone function with_polymorphic()
provides the functionality of query.with_polymorphic()
in a standalone form. It can be applied to any
entity within a query, including as the target
of a join in place of the "of_type()" modifier.
[ticket:2333]
- redo a large portion of the inheritance docs in terms
of declarative, new with_polymorphic() function
- upgrade examples/inheritance/polymorph, rename to "joined"
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 3 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/query.py | 153 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 142 |
4 files changed, 197 insertions, 102 deletions
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 1813f57d8..d322d426b 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -35,6 +35,7 @@ from sqlalchemy.orm.util import ( outerjoin, polymorphic_union, with_parent, + with_polymorphic, ) from sqlalchemy.orm.properties import ( ColumnProperty, diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 0771bbf3d..795447763 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1358,7 +1358,8 @@ class Mapper(object): spec = self.with_polymorphic[0] if selectable is False: selectable = self.with_polymorphic[1] - + elif selectable is False: + selectable = None mappers = self._mappers_from_spec(spec, selectable) if selectable is not None: return mappers, selectable diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index b0ce9ee13..d50c3922a 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -31,6 +31,7 @@ from sqlalchemy.orm import ( ) from sqlalchemy.orm.util import ( AliasedClass, ORMAdapter, _entity_descriptor, _entity_info, + _extended_entity_info, _is_aliased_class, _is_mapped_class, _orm_columns, _orm_selectable, join as orm_join,with_parent, _attr_as_key, aliased ) @@ -92,6 +93,7 @@ class Query(object): _from_obj = () _join_entities = () _select_from_entity = None + _mapper_adapter_map = {} _filter_aliases = None _from_obj_alias = None _joinpath = _joinpoint = util.immutabledict() @@ -114,50 +116,43 @@ class Query(object): for ent in util.to_list(entities): entity_wrapper(self, ent) - self._setup_aliasizers(self._entities) + self._set_entity_selectables(self._entities) - def _setup_aliasizers(self, entities): - if hasattr(self, '_mapper_adapter_map'): - # usually safe to share a single map, but copying to prevent - # subtle leaks if end-user is reusing base query with arbitrary - # number of aliased() objects - self._mapper_adapter_map = d = self._mapper_adapter_map.copy() - else: - self._mapper_adapter_map = d = {} + def _set_entity_selectables(self, entities): + self._mapper_adapter_map = d = self._mapper_adapter_map.copy() for ent in entities: for entity in ent.entities: if entity not in d: - mapper, selectable, is_aliased_class = \ - _entity_info(entity) + mapper, selectable, \ + is_aliased_class, with_polymorphic_mappers, \ + with_polymorphic_discriminator = \ + _extended_entity_info(entity) if not is_aliased_class and mapper.with_polymorphic: - with_polymorphic = mapper._with_polymorphic_mappers if mapper.mapped_table not in \ self._polymorphic_adapters: self._mapper_loads_polymorphically_with(mapper, sql_util.ColumnAdapter( selectable, mapper._equivalent_columns)) - adapter = None + aliased_adapter = None elif is_aliased_class: - adapter = sql_util.ColumnAdapter( + aliased_adapter = sql_util.ColumnAdapter( selectable, mapper._equivalent_columns) - with_polymorphic = None else: - with_polymorphic = adapter = None + aliased_adapter = None - d[entity] = (mapper, adapter, selectable, - is_aliased_class, with_polymorphic) + d[entity] = (mapper, aliased_adapter, selectable, + is_aliased_class, with_polymorphic_mappers, + with_polymorphic_discriminator) ent.setup_entity(entity, *d[entity]) def _mapper_loads_polymorphically_with(self, mapper, adapter): for m2 in mapper._with_polymorphic_mappers: self._polymorphic_adapters[m2] = adapter for m in m2.iterate_to_root(): - self._polymorphic_adapters[m.mapped_table] = \ - self._polymorphic_adapters[m.local_table] = \ - adapter + self._polymorphic_adapters[m.local_table] = adapter def _set_select_from(self, *obj): @@ -180,10 +175,9 @@ class Query(object): for m2 in mapper._with_polymorphic_mappers: self._polymorphic_adapters.pop(m2, None) for m in m2.iterate_to_root(): - self._polymorphic_adapters.pop(m.mapped_table, None) self._polymorphic_adapters.pop(m.local_table, None) - def __adapt_polymorphic_element(self, element): + def _adapt_polymorphic_element(self, element): if isinstance(element, expression.FromClause): search = element elif hasattr(element, 'table'): @@ -241,7 +235,7 @@ class Query(object): if self._polymorphic_adapters: adapters.append( ( - orm_only, self.__adapt_polymorphic_element + orm_only, self._adapt_polymorphic_element ) ) @@ -617,35 +611,29 @@ class Query(object): @_generative(_no_clauseelement_condition) def with_polymorphic(self, cls_or_mappers, - selectable=None, discriminator=None): - """Load columns for descendant mappers of this Query's mapper. - - Using this method will ensure that each descendant mapper's - tables are included in the FROM clause, and will allow filter() - criterion to be used against those tables. The resulting - instances will also have those columns already loaded so that - no "post fetch" of those columns will be required. - - :param cls_or_mappers: a single class or mapper, or list of - class/mappers, which inherit from this Query's mapper. - Alternatively, it may also be the string ``'*'``, in which case - all descending mappers will be added to the FROM clause. - - :param selectable: a table or select() statement that will - be used in place of the generated FROM clause. This argument is - required if any of the desired mappers use concrete table - inheritance, since SQLAlchemy currently cannot generate UNIONs - among tables automatically. If used, the ``selectable`` argument - must represent the full set of tables and columns mapped by every - desired mapper. Otherwise, the unaccounted mapped columns will - result in their table being appended directly to the FROM clause - which will usually lead to incorrect results. - - :param discriminator: a column to be used as the "discriminator" - column for the given selectable. If not given, the polymorphic_on - attribute of the mapper will be used, if any. This is useful for - mappers that don't have polymorphic loading behavior by default, - such as concrete table mappers. + selectable=None, + polymorphic_on=None): + """Load columns for inheriting classes. + + :meth:`.Query.with_polymorphic` applies transformations + to the "main" mapped class represented by this :class:`.Query`. + The "main" mapped class here means the :class:`.Query` + object's first argument is a full class, i.e. ``session.query(SomeClass)``. + These transformations allow additional tables to be present + in the FROM clause so that columns for a joined-inheritance + subclass are available in the query, both for the purposes + of load-time efficiency as well as the ability to use + these columns at query time. + + See the documentation section :ref:`with_polymorphic` for + details on how this method is used. + + As of 0.8, a new and more flexible function + :func:`.orm.with_polymorphic` supersedes + :meth:`.Query.with_polymorphic`, as it can apply the equivalent + functionality to any set of columns or classes in the + :class:`.Query`, not just the "zero mapper". See that + function for a description of arguments. """ @@ -657,7 +645,7 @@ class Query(object): entity.set_with_polymorphic(self, cls_or_mappers, selectable=selectable, - discriminator=discriminator) + polymorphic_on=polymorphic_on) @_generative() def yield_per(self, count): @@ -881,7 +869,7 @@ class Query(object): self._entities = list(self._entities) m = _MapperEntity(self, entity) - self._setup_aliasizers([m]) + self._set_entity_selectables([m]) @_generative() def with_session(self, session): @@ -998,7 +986,7 @@ class Query(object): _ColumnEntity(self, c) # _ColumnEntity may add many entities if the # given arg is a FROM clause - self._setup_aliasizers(self._entities[l:]) + self._set_entity_selectables(self._entities[l:]) @util.pending_deprecation("0.7", ":meth:`.add_column` is superseded by :meth:`.add_columns`", @@ -2998,7 +2986,7 @@ class Query(object): selected from the total results. """ - for entity, (mapper, adapter, s, i, w) in \ + for entity, (mapper, adapter, s, i, w, d) in \ self._mapper_adapter_map.iteritems(): if entity in self._join_entities: continue @@ -3042,14 +3030,16 @@ class _MapperEntity(_QueryEntity): self.entities = [entity] self.entity_zero = self.expr = entity - def setup_entity(self, entity, mapper, adapter, - from_obj, is_aliased_class, with_polymorphic): + def setup_entity(self, entity, mapper, aliased_adapter, + from_obj, is_aliased_class, + with_polymorphic, + with_polymorphic_discriminator): self.mapper = mapper - self.adapter = adapter + self.aliased_adapter = aliased_adapter self.selectable = from_obj - self._with_polymorphic = with_polymorphic - self._polymorphic_discriminator = None self.is_aliased_class = is_aliased_class + self._with_polymorphic = with_polymorphic + self._polymorphic_discriminator = with_polymorphic_discriminator if is_aliased_class: self.path_entity = self.entity_zero = entity self._path = (entity,) @@ -3062,9 +3052,14 @@ class _MapperEntity(_QueryEntity): self.entity_zero = mapper self._label_name = self.mapper.class_.__name__ - def set_with_polymorphic(self, query, cls_or_mappers, - selectable, discriminator): + selectable, polymorphic_on): + if self.is_aliased_class: + raise NotImplementedError( + "Can't use with_polymorphic() against " + "an Aliased object" + ) + if cls_or_mappers is None: query._reset_polymorphic_adapter(self.mapper) return @@ -3072,15 +3067,12 @@ class _MapperEntity(_QueryEntity): mappers, from_obj = self.mapper._with_polymorphic_args( cls_or_mappers, selectable) self._with_polymorphic = mappers - self._polymorphic_discriminator = discriminator + self._polymorphic_discriminator = polymorphic_on - # TODO: do the wrapped thing here too so that - # with_polymorphic() can be applied to aliases - if not self.is_aliased_class: - self.selectable = from_obj - query._mapper_loads_polymorphically_with(self.mapper, - sql_util.ColumnAdapter(from_obj, - self.mapper._equivalent_columns)) + self.selectable = from_obj + query._mapper_loads_polymorphically_with(self.mapper, + sql_util.ColumnAdapter(from_obj, + self.mapper._equivalent_columns)) filter_fn = id @@ -3104,11 +3096,12 @@ class _MapperEntity(_QueryEntity): def _get_entity_clauses(self, query, context): adapter = None - if not self.is_aliased_class and query._polymorphic_adapters: - adapter = query._polymorphic_adapters.get(self.mapper, None) - if not adapter and self.adapter: - adapter = self.adapter + if not self.is_aliased_class: + if query._polymorphic_adapters: + adapter = query._polymorphic_adapters.get(self.mapper, None) + else: + adapter = self.aliased_adapter if adapter: if query._from_obj_alias: @@ -3194,7 +3187,10 @@ class _MapperEntity(_QueryEntity): column_collection=context.primary_columns ) - if self._polymorphic_discriminator is not None: + if self._polymorphic_discriminator is not None and \ + self._polymorphic_discriminator \ + is not self.mapper.polymorphic_on: + if adapter: pd = adapter.columns[self._polymorphic_discriminator] else: @@ -3297,7 +3293,8 @@ class _ColumnEntity(_QueryEntity): c.entities = self.entities def setup_entity(self, entity, mapper, adapter, from_obj, - is_aliased_class, with_polymorphic): + is_aliased_class, with_polymorphic, + with_polymorphic_discriminator): if 'selectable' not in self.__dict__: self.selectable = from_obj self.froms.add(from_obj) diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 3cbe3f84a..5fcb15a9a 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -294,16 +294,28 @@ class AliasedClass(object): ``adapt_on_names`` is new in 0.7.3. """ - def __init__(self, cls, alias=None, name=None, adapt_on_names=False): + def __init__(self, cls, alias=None, + name=None, + adapt_on_names=False, + with_polymorphic_mappers=(), + with_polymorphic_discriminator=None): self.__mapper = _class_to_mapper(cls) self.__target = self.__mapper.class_ self.__adapt_on_names = adapt_on_names if alias is None: - alias = self.__mapper._with_polymorphic_selectable.alias(name=name) + alias = self.__mapper._with_polymorphic_selectable.alias( + name=name) self.__adapter = sql_util.ClauseAdapter(alias, - equivalents=self.__mapper._equivalent_columns, - adapt_on_names=self.__adapt_on_names) + equivalents=self.__mapper._equivalent_columns, + adapt_on_names=self.__adapt_on_names) self.__alias = alias + self.__with_polymorphic_mappers = with_polymorphic_mappers + self.__with_polymorphic_discriminator = \ + with_polymorphic_discriminator + for poly in with_polymorphic_mappers: + setattr(self, poly.class_.__name__, + AliasedClass(poly.class_, alias)) + # used to assign a name to the RowTuple object # returned by Query. self._sa_label_name = name @@ -315,6 +327,10 @@ class AliasedClass(object): 'alias':self.__alias, 'name':self._sa_label_name, 'adapt_on_names':self.__adapt_on_names, + 'with_polymorphic_mappers': + self.__with_polymorphic_mappers, + 'with_polymorphic_discriminator': + self.__with_polymorphic_discriminator } def __setstate__(self, state): @@ -323,9 +339,13 @@ class AliasedClass(object): self.__adapt_on_names = state['adapt_on_names'] alias = state['alias'] self.__adapter = sql_util.ClauseAdapter(alias, - equivalents=self.__mapper._equivalent_columns, - adapt_on_names=self.__adapt_on_names) + equivalents=self.__mapper._equivalent_columns, + adapt_on_names=self.__adapt_on_names) self.__alias = alias + self.__with_polymorphic_mappers = \ + state.get('with_polymorphic_mappers') + self.__with_polymorphic_discriminator = \ + state.get('with_polymorphic_discriminator') name = state['name'] self._sa_label_name = name self.__name__ = 'AliasedClass_' + str(self.__target) @@ -379,10 +399,75 @@ class AliasedClass(object): def aliased(element, alias=None, name=None, adapt_on_names=False): if isinstance(element, expression.FromClause): if adapt_on_names: - raise sa_exc.ArgumentError("adapt_on_names only applies to ORM elements") + raise sa_exc.ArgumentError( + "adapt_on_names only applies to ORM elements" + ) return element.alias(name) else: - return AliasedClass(element, alias=alias, name=name, adapt_on_names=adapt_on_names) + return AliasedClass(element, alias=alias, + name=name, adapt_on_names=adapt_on_names) + +def with_polymorphic(base, classes, selectable=False, + polymorphic_on=None, aliased=False): + """Produce an :class:`.AliasedClass` construct which specifies + columns for descendant mappers of the given base. + + .. note:: + + :func:`.orm.with_polymorphic` is new in version 0.8. + It is in addition to the existing :class:`.Query` method + :meth:`.Query.with_polymorphic`, which has the same purpose + but is not as flexible in its usage. + + Using this method will ensure that each descendant mapper's + tables are included in the FROM clause, and will allow filter() + criterion to be used against those tables. The resulting + instances will also have those columns already loaded so that + no "post fetch" of those columns will be required. + + See the examples at :ref:`with_polymorphic`. + + :param base: Base class to be aliased. + + :param cls_or_mappers: a single class or mapper, or list of + class/mappers, which inherit from the base class. + Alternatively, it may also be the string ``'*'``, in which case + all descending mapped classes will be added to the FROM clause. + + :param aliased: when True, the selectable will be wrapped in an + alias, that is ``(SELECT * FROM <fromclauses>) AS anon_1``. + This can be important when using the with_polymorphic() + to create the target of a JOIN on a backend that does not + support parenthesized joins, such as SQLite and older + versions of MySQL. + + :param selectable: a table or select() statement that will + be used in place of the generated FROM clause. This argument is + required if any of the desired classes use concrete table + inheritance, since SQLAlchemy currently cannot generate UNIONs + among tables automatically. If used, the ``selectable`` argument + must represent the full set of tables and columns mapped by every + mapped class. Otherwise, the unaccounted mapped columns will + result in their table being appended directly to the FROM clause + which will usually lead to incorrect results. + + :param polymorphic_on: a column to be used as the "discriminator" + column for the given selectable. If not given, the polymorphic_on + attribute of the base classes' mapper will be used, if any. This + is useful for mappings that don't have polymorphic loading + behavior by default. + + """ + primary_mapper = class_mapper(base) + mappers, selectable = primary_mapper.\ + _with_polymorphic_args(classes, selectable) + if aliased: + selectable = selectable.alias() + return AliasedClass(base, + selectable, + with_polymorphic_mappers=mappers, + with_polymorphic_discriminator=polymorphic_on) + def _orm_annotate(element, exclude=None): """Deep copy the given ClauseElement, annotating each element with the @@ -560,19 +645,13 @@ def with_parent(instance, prop): value_is_parent=True) -def _entity_info(entity, compile=True): - """Return mapping information given a class, mapper, or AliasedClass. - - Returns 3-tuple of: mapper, mapped selectable, boolean indicating if this - is an aliased() construct. - - If the given entity is not a mapper, mapped class, or aliased construct, - returns None, the entity, False. This is typically used to allow - unmapped selectables through. - - """ +def _extended_entity_info(entity, compile=True): if isinstance(entity, AliasedClass): - return entity._AliasedClass__mapper, entity._AliasedClass__alias, True + return entity._AliasedClass__mapper, \ + entity._AliasedClass__alias, \ + True, \ + entity._AliasedClass__with_polymorphic_mappers, \ + entity._AliasedClass__with_polymorphic_discriminator if isinstance(entity, mapperlib.Mapper): mapper = entity @@ -581,15 +660,32 @@ def _entity_info(entity, compile=True): class_manager = attributes.manager_of_class(entity) if class_manager is None: - return None, entity, False + return None, entity, False, [], None mapper = class_manager.mapper else: - return None, entity, False + return None, entity, False, [], None if compile and mapperlib.module._new_mappers: mapperlib.configure_mappers() - return mapper, mapper._with_polymorphic_selectable, False + return mapper, \ + mapper._with_polymorphic_selectable, \ + False, \ + mapper._with_polymorphic_mappers, \ + mapper.polymorphic_on + +def _entity_info(entity, compile=True): + """Return mapping information given a class, mapper, or AliasedClass. + + Returns 3-tuple of: mapper, mapped selectable, boolean indicating if this + is an aliased() construct. + + If the given entity is not a mapper, mapped class, or aliased construct, + returns None, the entity, False. This is typically used to allow + unmapped selectables through. + + """ + return _extended_entity_info(entity, compile)[0:3] def _entity_descriptor(entity, key): """Return a class attribute given an entity and string name. |
