summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Kirtland <jek@discorporate.us>2010-09-03 12:05:24 -0700
committerJason Kirtland <jek@discorporate.us>2010-09-03 12:05:24 -0700
commitf0aaeb02908677d45def8e96b8aaf88f2a9b5e3c (patch)
tree81f14eb40fd8528bd8e8e29aa2c31c8bb9e12274
parentfc46270f478796fe7eb98fae2ea2692a18e5a67c (diff)
downloadsqlalchemy-f0aaeb02908677d45def8e96b8aaf88f2a9b5e3c.tar.gz
Apply more memoization to Mapper attributes & subject to group expiry.
-rw-r--r--CHANGES6
-rw-r--r--lib/sqlalchemy/orm/evaluator.py3
-rw-r--r--lib/sqlalchemy/orm/mapper.py121
-rw-r--r--lib/sqlalchemy/orm/query.py8
-rw-r--r--lib/sqlalchemy/orm/strategies.py10
-rw-r--r--lib/sqlalchemy/orm/sync.py2
-rw-r--r--lib/sqlalchemy/orm/unitofwork.py6
-rw-r--r--lib/sqlalchemy/util.py43
8 files changed, 134 insertions, 65 deletions
diff --git a/CHANGES b/CHANGES
index d78cd6655..2fe9bbe57 100644
--- a/CHANGES
+++ b/CHANGES
@@ -155,7 +155,11 @@ CHANGES
- object_session() raises the proper
UnmappedInstanceError when presented with an
unmapped instance. [ticket:1881]
-
+
+ - Applied further memoizations to calculated Mapper properties,
+ with significant (~90%) runtime mapper.py call count reduction
+ in heavily polymorphic mapping configurations.
+
- sql
- Added basic math expression coercion for
Numeric->Integer,
diff --git a/lib/sqlalchemy/orm/evaluator.py b/lib/sqlalchemy/orm/evaluator.py
index 3ee70782d..e3cbffe98 100644
--- a/lib/sqlalchemy/orm/evaluator.py
+++ b/lib/sqlalchemy/orm/evaluator.py
@@ -35,7 +35,8 @@ class EvaluatorCompiler(object):
def visit_column(self, clause):
if 'parentmapper' in clause._annotations:
- key = clause._annotations['parentmapper']._get_col_to_prop(clause).key
+ key = clause._annotations['parentmapper'].\
+ _columntoproperty[clause].key
else:
key = clause.key
get_corresponding_attr = operator.attrgetter(key)
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 1f2216cec..f03dd6377 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -43,6 +43,7 @@ _new_mappers = False
_already_compiling = False
_none_set = frozenset([None])
+_memoized_compiled_property = util.group_expirable_memoized_property()
# a list of MapperExtensions that will be installed in all mappers by default
global_extensions = []
@@ -216,6 +217,7 @@ class Mapper(object):
global _new_mappers
_new_mappers = True
self._log("constructed")
+ self._expire_memoizations()
finally:
_COMPILE_MUTEX.release()
@@ -280,11 +282,6 @@ class Mapper(object):
self.version_id_col = self.inherits.version_id_col
self.version_id_generator = self.inherits.version_id_generator
- for mapper in self.iterate_to_root():
- util.reset_memoized(mapper, '_equivalent_columns')
- util.reset_memoized(mapper, '_sorted_tables')
- util.reset_memoized(mapper, '_compiled_cache')
-
if self.order_by is False and \
not self.concrete and \
self.inherits.order_by is not False:
@@ -533,7 +530,7 @@ class Mapper(object):
# table columns mapped to lists of MapperProperty objects
# using a list allows a single column to be defined as
# populating multiple object attributes
- self._columntoproperty = util.column_dict()
+ self._columntoproperty = _ColumnMapping(self)
# load custom properties
if self._init_properties:
@@ -809,6 +806,7 @@ class Mapper(object):
self._compile_failed = exc
raise
finally:
+ self._expire_memoizations()
_COMPILE_MUTEX.release()
def _post_configure_properties(self):
@@ -853,7 +851,11 @@ class Mapper(object):
"""
self._init_properties[key] = prop
self._configure_property(key, prop, init=self.compiled)
+ self._expire_memoizations()
+ def _expire_memoizations(self):
+ for mapper in self.iterate_to_root():
+ _memoized_compiled_property.expire_instance(mapper)
def _log(self, msg, *args):
self.logger.info(
@@ -946,7 +948,7 @@ class Mapper(object):
"""
if spec == '*':
- mappers = list(self.polymorphic_iterator())
+ mappers = list(self.self_and_descendants)
elif spec:
mappers = [_class_to_mapper(m) for m in util.to_list(spec)]
for m in mappers:
@@ -985,7 +987,7 @@ class Mapper(object):
return from_obj
- @property
+ @_memoized_compiled_property
def _single_table_criterion(self):
if self.single and \
self.inherits and \
@@ -993,18 +995,17 @@ class Mapper(object):
self.polymorphic_identity is not None:
return self.polymorphic_on.in_(
m.polymorphic_identity
- for m in self.polymorphic_iterator())
+ for m in self.self_and_descendants)
else:
return None
-
-
- @util.memoized_property
+
+ @_memoized_compiled_property
def _with_polymorphic_mappers(self):
if not self.with_polymorphic:
return [self]
return self._mappers_from_spec(*self.with_polymorphic)
- @util.memoized_property
+ @_memoized_compiled_property
def _with_polymorphic_selectable(self):
if not self.with_polymorphic:
return self.mapped_table
@@ -1029,6 +1030,11 @@ class Mapper(object):
else:
return mappers, self._selectable_from_mappers(mappers)
+ @_memoized_compiled_property
+ def _polymorphic_properties(self):
+ return tuple(self._iterate_polymorphic_properties(
+ self._with_polymorphic_mappers))
+
def _iterate_polymorphic_properties(self, mappers=None):
"""Return an iterator of MapperProperty objects which will render into
a SELECT."""
@@ -1060,7 +1066,7 @@ class Mapper(object):
"provided by the get_property() and iterate_properties "
"accessors.")
- @util.memoized_property
+ @_memoized_compiled_property
def _get_clause(self):
"""create a "get clause" based on the primary key. this is used
by query.get() and many-to-one lazyloads to load this item
@@ -1072,7 +1078,7 @@ class Mapper(object):
return sql.and_(*[k==v for (k, v) in params]), \
util.column_dict(params)
- @util.memoized_property
+ @_memoized_compiled_property
def _equivalent_columns(self):
"""Create a map of all *equivalent* columns, based on
the determination of column pairs that are equated to
@@ -1104,7 +1110,7 @@ class Mapper(object):
result[binary.right].add(binary.left)
else:
result[binary.right] = util.column_set((binary.left,))
- for mapper in self.base_mapper.polymorphic_iterator():
+ for mapper in self.base_mapper.self_and_descendants:
if mapper.inherit_condition is not None:
visitors.traverse(
mapper.inherit_condition, {},
@@ -1182,6 +1188,22 @@ class Mapper(object):
yield m
m = m.inherits
+ @_memoized_compiled_property
+ def self_and_descendants(self):
+ """The collection including this mapper and all descendant mappers.
+
+ This includes not just the immediately inheriting mappers but
+ all their inheriting mappers as well.
+
+ """
+ descendants = []
+ stack = deque([self])
+ while stack:
+ item = stack.popleft()
+ descendants.append(item)
+ stack.extend(item._inheriting_mappers)
+ return tuple(descendants)
+
def polymorphic_iterator(self):
"""Iterate through the collection including this mapper and
all descendant mappers.
@@ -1191,14 +1213,9 @@ class Mapper(object):
To iterate through an entire hierarchy, use
``mapper.base_mapper.polymorphic_iterator()``.
-
+
"""
- stack = deque([self])
- while stack:
- item = stack.popleft()
- yield item
- stack.extend(item._inheriting_mappers)
-
+ return iter(self.self_and_descendants)
def primary_mapper(self):
"""Return the primary mapper corresponding to this mapper's class key
@@ -1262,33 +1279,15 @@ class Mapper(object):
def _primary_key_from_state(self, state):
dict_ = state.dict
- return [
- self._get_state_attr_by_column(state, dict_, column) for
- column in self.primary_key]
-
- def _get_col_to_prop(self, column):
- try:
- return self._columntoproperty[column]
- except KeyError:
- prop = self._props.get(column.key, None)
- if prop:
- raise orm_exc.UnmappedColumnError(
- "Column '%s.%s' is not available, due to "
- "conflicting property '%s':%r" %
- (column.table.name, column.name,
- column.key, prop))
- else:
- raise orm_exc.UnmappedColumnError(
- "No column %s is configured on mapper %s..." %
- (column, self))
+ return [self._get_state_attr_by_column(state, dict_, column) for
+ column in self.primary_key]
# TODO: improve names?
def _get_state_attr_by_column(self, state, dict_, column):
- return self._get_col_to_prop(column)._getattr(state, dict_, column)
+ return self._columntoproperty[column]._getattr(state, dict_, column)
def _set_state_attr_by_column(self, state, dict_, column, value):
- return self._get_col_to_prop(column).\
- _setattr(state, dict_, value, column)
+ return self._columntoproperty[column]._setattr(state, dict_, value, column)
def _get_committed_attr_by_column(self, obj, column):
state = attributes.instance_state(obj)
@@ -1297,9 +1296,8 @@ class Mapper(object):
def _get_committed_state_attr_by_column(self, state, dict_, column,
passive=False):
- return self._get_col_to_prop(column).\
- _getcommitted(state, dict_,
- column, passive=passive)
+ return self._columntoproperty[column]._getcommitted(
+ state, dict_, column, passive=passive)
def _optimized_get_statement(self, state, attribute_names):
"""assemble a WHERE clause which retrieves a given state by primary
@@ -1409,14 +1407,14 @@ class Mapper(object):
except StopIteration:
visitables.pop()
- @util.memoized_property
+ @_memoized_compiled_property
def _compiled_cache(self):
return util.LRUCache(self._compiled_cache_size)
- @util.memoized_property
+ @_memoized_compiled_property
def _sorted_tables(self):
table_to_mapper = {}
- for mapper in self.base_mapper.polymorphic_iterator():
+ for mapper in self.base_mapper.self_and_descendants:
for t in mapper.tables:
table_to_mapper[t] = mapper
@@ -2433,7 +2431,8 @@ def _load_scalar_attributes(state, attribute_names):
# this codepath is rare - only valid when inside a flush, and the
# object is becoming persistent but hasn't yet been assigned an identity_key.
# check here to ensure we have the attrs we need.
- pk_attrs = [mapper._get_col_to_prop(col).key for col in mapper.primary_key]
+ pk_attrs = [mapper._columntoproperty[col].key
+ for col in mapper.primary_key]
if state.expired_attributes.intersection(pk_attrs):
raise sa_exc.InvalidRequestError("Instance %s cannot be refreshed - it's not "
" persistent and does not "
@@ -2460,3 +2459,21 @@ def _load_scalar_attributes(state, attribute_names):
raise orm_exc.ObjectDeletedError(
"Instance '%s' has been deleted." %
state_str(state))
+
+
+class _ColumnMapping(util.py25_dict):
+ """Error reporting helper for mapper._columntoproperty."""
+
+ def __init__(self, mapper):
+ self.mapper = mapper
+
+ def __missing__(self, column):
+ prop = self.mapper._props.get(column)
+ if prop:
+ raise orm_exc.UnmappedColumnError(
+ "Column '%s.%s' is not available, due to "
+ "conflicting property '%s':%r" % (
+ column.table.name, column.name, column.key, prop))
+ raise orm_exc.UnmappedColumnError(
+ "No column %s is configured on mapper %s..." %
+ (column, self.mapper))
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 18ffd108a..fdc426a07 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -2567,8 +2567,12 @@ class _MapperEntity(_QueryEntity):
)
)
- for value in self.mapper._iterate_polymorphic_properties(
- self._with_polymorphic):
+ if self._with_polymorphic:
+ poly_properties = self.mapper._iterate_polymorphic_properties(
+ self._with_polymorphic)
+ else:
+ poly_properties = self.mapper._polymorphic_properties
+ for value in poly_properties:
if query._only_load_props and \
value.key not in query._only_load_props:
continue
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 4d98e8e62..a8c079113 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -48,7 +48,7 @@ def _register_attribute(strategy, mapper, useobject,
attribute_ext.append(sessionlib.UOWEventHandler(prop.key))
- for m in mapper.polymorphic_iterator():
+ for m in mapper.self_and_descendants:
if prop is m._props.get(prop.key):
attributes.register_attribute_impl(
@@ -696,7 +696,7 @@ class SubqueryLoader(AbstractRelationshipLoader):
leftmost_cols, remote_cols = self._local_remote_columns(leftmost_prop)
leftmost_attr = [
- leftmost_mapper._get_col_to_prop(c).class_attribute
+ leftmost_mapper._columntoproperty[c].class_attribute
for c in leftmost_cols
]
@@ -743,7 +743,7 @@ class SubqueryLoader(AbstractRelationshipLoader):
self._local_remote_columns(self.parent_property)
local_attr = [
- getattr(parent_alias, self.parent._get_col_to_prop(c).key)
+ getattr(parent_alias, self.parent._columntoproperty[c].key)
for c in local_cols
]
q = q.order_by(*local_attr)
@@ -825,7 +825,7 @@ class SubqueryLoader(AbstractRelationshipLoader):
local_cols, remote_cols = self._local_remote_columns(self.parent_property)
remote_attr = [
- self.mapper._get_col_to_prop(c).key
+ self.mapper._columntoproperty[c].key
for c in remote_cols]
q = context.attributes[('subquery', path)]
@@ -943,7 +943,7 @@ class EagerLoader(AbstractRelationshipLoader):
("eager_row_processor", reduced_path)
] = clauses
- for value in self.mapper._iterate_polymorphic_properties():
+ for value in self.mapper._polymorphic_properties:
value.setup(
context,
entity,
diff --git a/lib/sqlalchemy/orm/sync.py b/lib/sqlalchemy/orm/sync.py
index 3b2a291bd..05298767d 100644
--- a/lib/sqlalchemy/orm/sync.py
+++ b/lib/sqlalchemy/orm/sync.py
@@ -71,7 +71,7 @@ def source_modified(uowcommit, source, source_mapper, synchronize_pairs):
"""
for l, r in synchronize_pairs:
try:
- prop = source_mapper._get_col_to_prop(l)
+ prop = source_mapper._columntoproperty[l]
except exc.UnmappedColumnError:
_raise_col_to_prop(False, source_mapper, l, None, r)
history = uowcommit.get_attribute_history(source, prop.key, passive=True)
diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py
index e10891924..830ac3c0c 100644
--- a/lib/sqlalchemy/orm/unitofwork.py
+++ b/lib/sqlalchemy/orm/unitofwork.py
@@ -219,7 +219,7 @@ class UOWTransaction(object):
def states_for_mapper_hierarchy(self, mapper, isdelete, listonly):
checktup = (isdelete, listonly)
- for mapper in mapper.base_mapper.polymorphic_iterator():
+ for mapper in mapper.base_mapper.self_and_descendants:
for state in self.mappers[mapper]:
if self.states[state] == checktup:
yield state
@@ -318,11 +318,11 @@ class IterateMappersMixin(object):
def _mappers(self, uow):
if self.fromparent:
return iter(
- m for m in self.dependency_processor.parent.polymorphic_iterator()
+ m for m in self.dependency_processor.parent.self_and_descendants
if uow._mapper_for_dep[(m, self.dependency_processor)]
)
else:
- return self.dependency_processor.mapper.polymorphic_iterator()
+ return self.dependency_processor.mapper.self_and_descendants
class Preprocess(IterateMappersMixin):
def __init__(self, dependency_processor, fromparent):
diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py
index 7eb0a522f..ddcab7822 100644
--- a/lib/sqlalchemy/util.py
+++ b/lib/sqlalchemy/util.py
@@ -176,6 +176,30 @@ class frozendict(dict):
def __repr__(self):
return "frozendict(%s)" % dict.__repr__(self)
+
+# find or create a dict implementation that supports __missing__
+class _probe(dict):
+ def __missing__(self, key):
+ return 1
+try:
+ _probe()['missing']
+ py25_dict = dict
+except KeyError:
+ class py25_dict(dict):
+ def __getitem__(self, key):
+ try:
+ return dict.__getitem__(self, key)
+ except KeyError:
+ try:
+ missing = self.__missing__
+ except AttributeError:
+ raise KeyError(key)
+ else:
+ return missing(key)
+finally:
+ del _probe
+
+
def to_list(x, default=None):
if x is None:
return default
@@ -1434,6 +1458,7 @@ def function_named(fn, name):
fn.func_defaults, fn.func_closure)
return fn
+
class memoized_property(object):
"""A read-only @property that is only evaluated once."""
def __init__(self, fget, doc=None):
@@ -1478,6 +1503,24 @@ class memoized_instancemethod(object):
def reset_memoized(instance, name):
instance.__dict__.pop(name, None)
+
+class group_expirable_memoized_property(object):
+ """A family of @memoized_properties that can be expired in tandem."""
+
+ def __init__(self):
+ self.attributes = []
+
+ def expire_instance(self, instance):
+ """Expire all memoized properties for *instance*."""
+ stash = instance.__dict__
+ for attribute in self.attributes:
+ stash.pop(attribute, None)
+
+ def __call__(self, fn):
+ self.attributes.append(fn.__name__)
+ return memoized_property(fn)
+
+
class WeakIdentityMapping(weakref.WeakKeyDictionary):
"""A WeakKeyDictionary with an object identity index.