diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-06-20 19:28:29 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-06-20 19:28:29 -0400 |
| commit | 3dd536ac06808adcf9c10707dbf2ebb6e3842be7 (patch) | |
| tree | d102291da86021aa4584ef836dbb0471596c3eeb /lib/sqlalchemy/orm/util.py | |
| parent | 40098941007ff3aa1593e834915c4042c1668dc2 (diff) | |
| download | sqlalchemy-3dd536ac06808adcf9c10707dbf2ebb6e3842be7.tar.gz | |
- [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 (limited to 'lib/sqlalchemy/orm/util.py')
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 204 |
1 files changed, 182 insertions, 22 deletions
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 51aaa3152..0978ab693 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -9,6 +9,7 @@ from sqlalchemy import sql, util, event, exc as sa_exc, inspection from sqlalchemy.sql import expression, util as sql_util, operators from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE,\ PropComparator, MapperProperty +from itertools import chain from sqlalchemy.orm import attributes, exc import operator import re @@ -233,6 +234,144 @@ class ORMAdapter(sql_util.ColumnAdapter): else: return None +class PathRegistry(object): + """Represent query load paths and registry functions. + + Basically represents structures like: + + (<User mapper>, "orders", <Order mapper>, "items", <Item mapper>) + + These structures are generated by things like + query options (joinedload(), subqueryload(), etc.) and are + used to compose keys stored in the query._attributes dictionary + for various options. + + They are then re-composed at query compile/result row time as + the query is formed and as rows are fetched, where they again + serve to compose keys to look up options in the context.attributes + dictionary, which is copied from query._attributes. + + The path structure has a limited amount of caching, where each + "root" ultimately pulls from a fixed registry associated with + the first mapper, that also contains elements for each of its + property keys. However paths longer than two elements, which + are the exception rather than the rule, are generated on an + as-needed basis. + + """ + + def __eq__(self, other): + return other is not None and \ + self.path == other.path + + def set(self, reg, key, value): + reg._attributes[(key, self.reduced_path)] = value + + def setdefault(self, reg, key, value): + reg._attributes.setdefault((key, self.reduced_path), value) + + def get(self, reg, key, value=None): + key = (key, self.reduced_path) + if key in reg._attributes: + return reg._attributes[key] + else: + return value + + @property + def length(self): + return len(self.path) + + def contains_mapper(self, mapper): + return mapper.base_mapper in self.reduced_path + + def contains(self, reg, key): + return (key, self.reduced_path) in reg._attributes + + def serialize(self): + path = self.path + 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] + ) + + @classmethod + def deserialize(cls, path): + if path is None: + return None + + p = tuple(chain(*[(class_mapper(mcls), key) for mcls, key in path])) + if p and p[-1] is None: + p = p[0:-1] + return cls.coerce(p) + + @classmethod + def per_mapper(cls, mapper): + return EntityRegistry( + cls.root, mapper + ) + + @classmethod + def coerce(cls, raw): + return util.reduce(lambda prev, next:prev[next], raw, cls.root) + + @classmethod + def token(cls, token): + return KeyRegistry(cls.root, token) + + def __add__(self, other): + return util.reduce( + lambda prev, next:prev[next], + other.path, self) + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.path, ) + +class RootRegistry(PathRegistry): + """Root registry, defers to mappers so that + paths are maintained per-root-mapper. + + """ + path = () + reduced_path = () + + def __getitem__(self, mapper): + return mapper._sa_path_registry +PathRegistry.root = RootRegistry() + +class KeyRegistry(PathRegistry): + def __init__(self, parent, key): + self.key = key + self.parent = parent + self.path = parent.path + (key,) + self.reduced_path = parent.reduced_path + (key,) + + def __getitem__(self, entity): + return EntityRegistry( + self, entity + ) + +class EntityRegistry(PathRegistry, dict): + is_aliased_class = False + + def __init__(self, parent, entity): + self.key = reduced_key = entity + self.parent = parent + if hasattr(entity, 'base_mapper'): + reduced_key = entity.base_mapper + else: + self.is_aliased_class = True + + self.path = parent.path + (entity,) + self.reduced_path = parent.reduced_path + (reduced_key,) + + def __nonzero__(self): + return True + + def __missing__(self, key): + self[key] = item = KeyRegistry(self, key) + return item + + class AliasedClass(object): """Represents an "aliased" form of a mapped class for usage with Query. @@ -321,6 +460,10 @@ class AliasedClass(object): self._sa_label_name = name self.__name__ = 'AliasedClass_' + str(self.__target) + @util.memoized_property + def _sa_path_registry(self): + return PathRegistry.per_mapper(self) + def __getstate__(self): return { 'mapper':self.__mapper, @@ -408,7 +551,8 @@ def aliased(element, alias=None, name=None, adapt_on_names=False): name=name, adapt_on_names=adapt_on_names) def with_polymorphic(base, classes, selectable=False, - polymorphic_on=None, aliased=False): + polymorphic_on=None, aliased=False, + innerjoin=False): """Produce an :class:`.AliasedClass` construct which specifies columns for descendant mappers of the given base. @@ -422,23 +566,23 @@ def with_polymorphic(base, classes, selectable=False, 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 @@ -455,10 +599,12 @@ def with_polymorphic(base, classes, selectable=False, is useful for mappings that don't have polymorphic loading behavior by default. + :param innerjoin: if True, an INNER JOIN will be used. This should + only be specified if querying for one specific subtype only """ - primary_mapper = class_mapper(base) + primary_mapper = _class_to_mapper(base) mappers, selectable = primary_mapper.\ - _with_polymorphic_args(classes, selectable) + _with_polymorphic_args(classes, selectable, innerjoin=innerjoin) if aliased: selectable = selectable.alias() return AliasedClass(base, @@ -478,11 +624,11 @@ def _orm_annotate(element, exclude=None): def _orm_deannotate(element): """Remove annotations that link a column to a particular mapping. - + Note this doesn't affect "remote" and "foreign" annotations passed by the :func:`.orm.foreign` and :func:`.orm.remote` annotators. - + """ return sql_util._deep_deannotate(element, @@ -644,13 +790,24 @@ def with_parent(instance, prop): value_is_parent=True) +_extended_entity_info_tuple = util.namedtuple("extended_entity_info", [ + "entity", + "mapper", + "selectable", + "is_aliased_class", + "with_polymorphic_mappers", + "with_polymorphic_discriminator" +]) def _extended_entity_info(entity, compile=True): if isinstance(entity, AliasedClass): - return entity._AliasedClass__mapper, \ - entity._AliasedClass__alias, \ - True, \ - entity._AliasedClass__with_polymorphic_mappers, \ - entity._AliasedClass__with_polymorphic_discriminator + return _extended_entity_info_tuple( + entity, + entity._AliasedClass__mapper, \ + entity._AliasedClass__alias, \ + True, \ + entity._AliasedClass__with_polymorphic_mappers, \ + entity._AliasedClass__with_polymorphic_discriminator + ) if isinstance(entity, mapperlib.Mapper): mapper = entity @@ -659,19 +816,22 @@ def _extended_entity_info(entity, compile=True): class_manager = attributes.manager_of_class(entity) if class_manager is None: - return None, entity, False, [], None + return _extended_entity_info_tuple(entity, None, entity, False, [], None) mapper = class_manager.mapper else: - return None, entity, False, [], None + return _extended_entity_info_tuple(entity, None, entity, False, [], None) if compile and mapperlib.module._new_mappers: mapperlib.configure_mappers() - return mapper, \ + return _extended_entity_info_tuple( + entity, + 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. @@ -684,7 +844,7 @@ def _entity_info(entity, compile=True): unmapped selectables through. """ - return _extended_entity_info(entity, compile)[0:3] + return _extended_entity_info(entity, compile)[1:4] def _entity_descriptor(entity, key): """Return a class attribute given an entity and string name. @@ -738,7 +898,7 @@ def object_mapper(instance): Raises UnmappedInstanceError if no mapping is configured. This function is available via the inspection system as:: - + inspect(instance).mapper """ @@ -752,7 +912,7 @@ def object_state(instance): Raises UnmappedInstanceError if no mapping is configured. This function is available via the inspection system as:: - + inspect(instance) """ @@ -776,9 +936,9 @@ def class_mapper(class_, compile=True): object is passed. This function is available via the inspection system as:: - + inspect(some_mapped_class) - + """ try: |
