- this is a wide refactoring to "attribute loader" and "options" architectures.
ColumnProperty and PropertyLoader define their loading behaivor via switchable
"strategies", and MapperOptions no longer use mapper/property copying
in order to function; they are instead propigated via QueryContext
and SelectionContext objects at query/instnaces time.
All of the copying of mappers and properties that was used to handle
inheritance as well as options() has been removed and the structure
of mappers and properties is much simpler and more clearly laid out.
The basic issue thats fixed is detecting changes on PickleType
objects, but also generalizes type handling and "modified" object
checking to be more complete and extensible.
- - internal refactoring to mapper instances() method to use a
- SelectionContext object to track state during the operation.
+ - a wide refactoring to "attribute loader" and "options" architectures.
+ ColumnLoader and PropertyLoader define their loading behaivor via switchable
+ "strategies", and MapperOptions no longer use mapper/property copying
+ in order to function; they are instead propigated via QueryContext
+ and SelectionContext objects at query/instnaces time.
+ All of the copying of mappers and properties that was used to handle
+ inheritance as well as options() has been removed and the structure
+ of mappers and properties is much simpler and clearly laid out.
+ - related to the mapper/property overhaul, internal refactoring to
+ mapper instances() method to use a SelectionContext object to track
+ state during the operation.
SLIGHT API BREAKAGE: the append_result() and populate_instances()
methods on MapperExtension have a slightly different method signature
now as a result of the change; hoping that these methods are not
try:
rec = self.props[key.key.lower()]
except KeyError:
- rec = self.props[key.name.lower()]
+# rec = self.props[key.name.lower()]
+ try:
+ rec = self.props[key.name.lower()]
+ except KeyError:
+ raise exceptions.NoSuchColumnError("Could not locate column in row for column '%s'" % str(key))
elif isinstance(key, str):
rec = self.props[key.lower()]
else:
try:
self._convert_key(key)
return True
- except KeyError:
+ except exceptions.NoSuchColumnError:
return False
def _get_col(self, row, key):
class AssertionError(SQLAlchemyError):
"""corresponds to internal state being detected in an invalid state"""
pass
-
+
+class NoSuchColumnError(KeyError, SQLAlchemyError):
+ """raised by RowProxy when a nonexistent column is requested from a row"""
+ pass
+
class DBAPIError(SQLAlchemyError):
"""something weird happened with a particular DBAPI version"""
def __init__(self, message, orig):
new = self.clone()
new._ops['from_obj'] = from_obj
return new
-
+
def join_to(self, prop):
"""join the table of this SelectResults to the table located against the given property name.
handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s'))
rootlogger.addHandler(handler)
-def _get_instance_name(instance):
+def _get_instance_name(instance):
# since getLogger() does not have any way of removing logger objects from memory,
- # instance logging displays the instance id as a modulus of 10 to prevent endless memory growth
+ # instance logging displays the instance id as a modulus of 16 to prevent endless memory growth
+ # also speeds performance as logger initialization is apparently slow
return instance.__class__.__module__ + "." + instance.__class__.__name__ + ".0x.." + hex(id(instance))[-2:]
def instance_logger(instance):
from mapper import mapper_registry
from query import Query
from util import polymorphic_union
-import properties
+import properties, strategies
from session import Session as create_session
__all__ = ['relation', 'backref', 'eagerload', 'lazyload', 'noload', 'deferred', 'defer', 'undefer',
return _relation_loader(*args, **kwargs)
def _relation_loader(mapper, secondary=None, primaryjoin=None, secondaryjoin=None, lazy=True, **kwargs):
- if lazy:
- return properties.LazyLoader(mapper, secondary, primaryjoin, secondaryjoin, **kwargs)
- elif lazy is None:
- return properties.PropertyLoader(mapper, secondary, primaryjoin, secondaryjoin, **kwargs)
- else:
- return properties.EagerLoader(mapper, secondary, primaryjoin, secondaryjoin, **kwargs)
+ return properties.PropertyLoader(mapper, secondary, primaryjoin, secondaryjoin, lazy=lazy, **kwargs)
def backref(name, **kwargs):
return properties.BackRef(name, **kwargs)
def deferred(*columns, **kwargs):
"""returns a DeferredColumnProperty, which indicates this object attributes should only be loaded
from its corresponding table column when first accessed."""
- return properties.DeferredColumnProperty(*columns, **kwargs)
+ return properties.ColumnProperty(deferred=True, *columns, **kwargs)
def mapper(class_, table=None, *args, **params):
"""returns a newMapper object."""
"""returns a MapperOption that will add the given MapperExtension to the
mapper returned by mapper.options()."""
return ExtensionOption(ext)
-def eagerload(name, **kwargs):
+
+def eagerload(name):
"""returns a MapperOption that will convert the property of the given name
- into an eager load. Used with mapper.options()"""
- return properties.EagerLazyOption(name, toeager=True, **kwargs)
+ into an eager load."""
+ return strategies.EagerLazyOption(name, lazy=False)
-def lazyload(name, **kwargs):
+def lazyload(name):
"""returns a MapperOption that will convert the property of the given name
- into a lazy load. Used with mapper.options()"""
- return properties.EagerLazyOption(name, toeager=False, **kwargs)
+ into a lazy load"""
+ return strategies.EagerLazyOption(name, lazy=True)
-def noload(name, **kwargs):
- """returns a MapperOption that will convert the property of the given name
- into a non-load. Used with mapper.options()"""
- return properties.EagerLazyOption(name, toeager=None, **kwargs)
+def noload(name):
+ """return a MapperOption that will convert the property of the given name
+ into a non-load."""
+ return strategies.EagerLazyOption(name, lazy=None)
-def defer(name, **kwargs):
+def defer(name):
"""returns a MapperOption that will convert the column property of the given
name into a deferred load. Used with mapper.options()"""
- return properties.DeferredOption(name, defer=True)
-def undefer(name, **kwargs):
+ return strategies.DeferredOption(name, defer=True)
+def undefer(name):
"""returns a MapperOption that will convert the column property of the given
name into a non-deferred (regular column) load. Used with mapper.options."""
- return properties.DeferredOption(name, defer=False)
+ return strategies.DeferredOption(name, defer=False)
--- /dev/null
+# interfaces.py
+# Copyright (C) 2005,2006 Michael Bayer mike_mp@zzzcomputing.com
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+
+from sqlalchemy import util
+
+class MapperProperty(object):
+ """manages the relationship of a Mapper to a single class attribute, as well
+ as that attribute as it appears on individual instances of the class, including
+ attribute instrumentation, attribute access, loading behavior, and dependency calculations."""
+ def setup(self, querycontext, **kwargs):
+ """called when a statement is being constructed. """
+ pass
+ def execute(self, selectcontext, instance, row, identitykey, isnew):
+ """called when the mapper receives a row. instance is the parent instance
+ corresponding to the row. """
+ raise NotImplementedError()
+ def cascade_iterator(self, type, object, recursive=None):
+ return []
+ def cascade_callable(self, type, object, callable_, recursive=None):
+ return []
+ def copy(self):
+ raise NotImplementedError()
+ def get_criterion(self, query, key, value):
+ """Returns a WHERE clause suitable for this MapperProperty corresponding to the
+ given key/value pair, where the key is a column or object property name, and value
+ is a value to be matched. This is only picked up by PropertyLoaders.
+
+ this is called by a Query's join_by method to formulate a set of key/value pairs into
+ a WHERE criterion that spans multiple tables if needed."""
+ return None
+ def set_parent(self, parent):
+ self.parent = parent
+ def init(self, key, parent):
+ """called after all mappers are compiled to assemble relationships between
+ mappers, establish instrumented class attributes"""
+ self.key = key
+ self.do_init()
+ def adapt_to_inherited(self, key, newparent):
+ """adapt this MapperProperty to a new parent, assuming the new parent is an inheriting
+ descendant of the old parent. """
+ newparent._compile_property(key, self, init=False, setparent=False)
+ def do_init(self):
+ """template method for subclasses"""
+ pass
+ def register_deleted(self, object, uow):
+ """called when the instance is being deleted"""
+ pass
+ def register_dependencies(self, *args, **kwargs):
+ """called by the Mapper in response to the UnitOfWork calling the Mapper's
+ register_dependencies operation. Should register with the UnitOfWork all
+ inter-mapper dependencies as well as dependency processors (see UOW docs for more details)"""
+ pass
+ def is_primary(self):
+ """return True if this MapperProperty's mapper is the primary mapper for its class.
+
+ This flag is used to indicate that the MapperProperty can define attribute instrumentation
+ for the class at the class level (as opposed to the individual instance level.)"""
+ return self.parent._is_primary_mapper()
+
+class StrategizedProperty(MapperProperty):
+ """a MapperProperty which uses selectable strategies to affect loading behavior.
+ There is a single default strategy selected, and alternate strategies can be selected
+ at selection time through the usage of StrategizedOption objects."""
+ def _get_context_strategy(self, context):
+ return self._get_strategy(context.attributes.get((LoaderStrategy, self), self.strategy.__class__))
+ def _get_strategy(self, cls):
+ try:
+ return self._all_strategies[cls]
+ except KeyError:
+ strategy = cls(self)
+ strategy.init()
+ strategy.is_default = False
+ self._all_strategies[cls] = strategy
+ return strategy
+ def setup(self, querycontext, **kwargs):
+ self._get_context_strategy(querycontext).setup_query(querycontext, **kwargs)
+ def execute(self, selectcontext, instance, row, identitykey, isnew):
+ self._get_context_strategy(selectcontext).process_row(selectcontext, instance, row, identitykey, isnew)
+ def do_init(self):
+ self._all_strategies = {}
+ self.strategy = self.create_strategy()
+ self._all_strategies[self.strategy.__class__] = self.strategy
+ self.strategy.init()
+ if self.is_primary():
+ self.strategy.init_class_attribute()
+
+class OperationContext(object):
+ """serves as a context during a query construction or instance loading operation.
+ accepts MapperOption objects which may modify its state before proceeding."""
+ def __init__(self, mapper, options):
+ self.mapper = mapper
+ self.options = options
+ self.attributes = {}
+ self.recursion_stack = util.Set()
+ for opt in options:
+ opt.process_context(self)
+
+class MapperOption(object):
+ """describes a modification to an OperationContext."""
+ def process_context(self, context):
+ pass
+
+class StrategizedOption(MapperOption):
+ """a MapperOption that affects which LoaderStrategy will be used for an operation
+ by a StrategizedProperty."""
+ def __init__(self, key):
+ self.key = key
+ def get_strategy_class(self):
+ raise NotImplementedError()
+ def process_context(self, context):
+ try:
+ key = self.__key
+ except AttributeError:
+ mapper = context.mapper
+ for token in self.key.split('.'):
+ prop = mapper.props[token]
+ mapper = getattr(prop, 'mapper', None)
+ self.__key = (LoaderStrategy, prop)
+ key = self.__key
+ context.attributes[key] = self.get_strategy_class()
+
+
+class LoaderStrategy(object):
+ """describes the loading behavior of a StrategizedProperty object. The LoaderStrategy
+ interacts with the querying process in three ways:
+ * it controls the configuration of the InstrumentedAttribute placed on a class to
+ handle the behavior of the attribute. this may involve setting up class-level callable
+ functions to fire off a select operation when the attribute is first accessed (i.e. a lazy load)
+ * it processes the QueryContext at statement construction time, where it can modify the SQL statement
+ that is being produced. simple column attributes may add their represented column to the list of
+ selected columns, "eager loading" properties may add LEFT OUTER JOIN clauses to the statement.
+ * it processes the SelectionContext at row-processing time. This may involve setting instance-level
+ lazyloader functions on newly constructed instances, or may involve recursively appending child items
+ to a list in response to additionally eager-loaded objects in the query.
+ """
+ def __init__(self, parent):
+ self.parent_property = parent
+ self.is_default = True
+ def init(self):
+ self.parent = self.parent_property.parent
+ self.key = self.parent_property.key
+ def init_class_attribute(self):
+ pass
+ def setup_query(self, context, **kwargs):
+ pass
+ def process_row(self, selectcontext, instance, row, identitykey, isnew):
+ pass
+
from sqlalchemy import sql_util as sqlutil
import util as mapperutil
import sync
+from interfaces import MapperProperty, MapperOption, OperationContext
import query as querylib
import session as sessionlib
import weakref
for ext_obj in util.to_list(extension):
extlist.add(ext_obj)
- self.extension = ExtensionCarrier()
+ self.extension = _ExtensionCarrier()
for ext in extlist:
self.extension.elements.append(ext)
self.order_by = self.inherits.order_by
self.polymorphic_map = self.inherits.polymorphic_map
self.batch = self.inherits.batch
+ self.inherits._inheriting_mappers.add(self)
else:
self._synchronizer = None
self.mapped_table = self.local_table
# 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 = TranslatingDict(self.mapped_table)
+ self.columntoproperty = mapperutil.TranslatingDict(self.mapped_table)
# load custom properties
if self.properties is not None:
self._compile_property(key, prop, False)
if self.inherits is not None:
- # transfer properties from the inherited mapper to here.
- # this includes column properties as well as relations.
- # the column properties will attempt to be translated from the selectable unit
- # of the parent mapper to this mapper's selectable unit.
- self.inherits._inheriting_mappers.add(self)
for key, prop in self.inherits.__props.iteritems():
if not self.__props.has_key(key):
prop.adapt_to_inherited(key, self)
prop.set_parent(self)
self.__log("adding ColumnProperty %s" % (column.key))
elif isinstance(prop, ColumnProperty):
+ if prop.parent is not self:
+ prop = ColumnProperty(deferred=prop.deferred, group=prop.group, *prop.columns)
+ prop.set_parent(self)
+ self.__props[column.key] = prop
prop.columns.append(column)
self.__log("appending to existing ColumnProperty %s" % (column.key))
else:
proplist = self.columntoproperty.setdefault(column, [])
proplist.append(prop)
+
def _initialize_properties(self):
"""calls the init() method on all MapperProperties attached to this mapper. this will incur the
compilation of related mappers."""
while m is not self and m.inherits is not None:
m = m.inherits
return m is self
-
+
+ def accept_mapper_option(self, option):
+ option.process_mapper(self)
+
def add_properties(self, dict_of_properties):
"""adds the given dictionary of properties to this mapper, using add_property."""
for key, value in dict_of_properties.iteritems():
else:
return None
- def _compile_property(self, key, prop, init=True, skipmissing=False, localparent=None):
+ def _compile_property(self, key, prop, init=True, skipmissing=False, setparent=True):
"""add a MapperProperty to this or another Mapper, including configuration of the property.
The properties' parent attribute will be set, and the property will also be
if prop is None:
raise exceptions.ArgumentError("'%s' is not an instance of MapperProperty or Column" % repr(prop))
- effectiveparent = localparent or self
- effectiveparent.__props[key] = prop
- prop.set_parent(self)
+ self.__props[key] = prop
+ if setparent:
+ prop.set_parent(self)
if isinstance(prop, ColumnProperty):
col = self.select_table.corresponding_column(prop.columns[0], keys_ok=True, raiseerr=False)
proplist.append(prop)
if init:
- prop.init(key, effectiveparent)
+ prop.init(key, self)
- for mapper in effectiveparent._inheriting_mappers:
+ for mapper in self._inheriting_mappers:
prop.adapt_to_inherited(key, mapper)
def __str__(self):
return [self._getattrbycolumn(instance, column) for column in self.pks_by_table[self.mapped_table]]
- def copy(self, **kwargs):
- mapper = Mapper.__new__(Mapper)
- mapper.__dict__.update(self.__dict__)
- mapper.__dict__.update(kwargs)
- mapper.__props = self.__props.copy()
- mapper._inheriting_mappers = []
- for m in self._inheriting_mappers:
- mapper._inheriting_mappers.append(m.copy())
- return mapper
-
- def options(self, *options, **kwargs):
- """uses this mapper as a prototype for a new mapper with different behavior.
- *options is a list of options directives, which include eagerload(), lazyload(), and noload()"""
- # TODO: this whole options() scheme is going to change, and not rely upon
- # making huge chains of copies anymore. stay tuned !
- self.compile()
- optkey = repr([hash_key(o) for o in options])
- try:
- return self._options[optkey]
- except KeyError:
- mapper = self.copy(**kwargs)
- for option in options:
- option.process(mapper)
- self._options[optkey] = mapper
- return mapper
-
-
def _getpropbycolumn(self, column, raiseerror=True):
try:
prop = self.columntoproperty[column]
if result is not None:
result.append(instance)
return instance
-
+ else:
+ self.__log_debug("_instance(): identity key %s not in session" % str(identitykey) + repr([mapperutil.instance_str(x) for x in context.session]))
# look in result-local identitymap for it.
exists = context.identity_map.has_key(identitykey)
if not exists:
Mapper.logger = logging.class_logger(Mapper)
-class SelectionContext(object):
+class SelectionContext(OperationContext):
"""created within the mapper.instances() method to store and share
state among all the Mappers and MapperProperty objects used in a load operation.
"""
def __init__(self, mapper, session, **kwargs):
- self.mapper = mapper
- self.populate_existing = kwargs.get('populate_existing', False)
- self.version_check = kwargs.get('version_check', False)
+ self.populate_existing = kwargs.pop('populate_existing', False)
+ self.version_check = kwargs.pop('version_check', False)
self.session = session
self.identity_map = {}
- self.attributes = {}
-
-
-class MapperProperty(object):
- """an element attached to a Mapper that describes and assists in the loading and saving
- of an attribute on an object instance."""
- def setup(self, statement, **options):
- """called when a statement is being constructed. """
- return self
- def execute(self, selectcontext, instance, row, identitykey, isnew):
- """called when the mapper receives a row. instance is the parent instance
- corresponding to the row. """
- raise NotImplementedError()
- def cascade_iterator(self, type, object, recursive=None):
- return []
- def cascade_callable(self, type, object, callable_, recursive=None):
- return []
- def copy(self):
- raise NotImplementedError()
- def get_criterion(self, query, key, value):
- """Returns a WHERE clause suitable for this MapperProperty corresponding to the
- given key/value pair, where the key is a column or object property name, and value
- is a value to be matched. This is only picked up by PropertyLoaders.
-
- this is called by a mappers select_by method to formulate a set of key/value pairs into
- a WHERE criterion that spans multiple tables if needed."""
- return None
- def set_parent(self, parent):
- self.parent = parent
- def init(self, key, parent):
- """called after all mappers are compiled to assemble relationships between
- mappers, establish instrumented class attributes"""
- self.key = key
- self.localparent = parent
- if not hasattr(self, 'inherits'):
- self.inherits = None
- self.do_init()
- def adapt_to_inherited(self, key, newparent):
- """adapt this MapperProperty to a new parent, assuming the new parent is an inheriting
- descendant of the old parent. """
- p = self.copy()
- newparent._compile_property(key, p, init=False)
- p.localparent = newparent
- p.parent = self.parent
- p.inherits = getattr(self, 'inherits', self)
- def do_init(self):
- """template method for subclasses"""
- pass
- def register_deleted(self, object, uow):
- """called when the instance is being deleted"""
- pass
- def register_dependencies(self, *args, **kwargs):
- """called by the Mapper in response to the UnitOfWork calling the Mapper's
- register_dependencies operation. Should register with the UnitOfWork all
- inter-mapper dependencies as well as dependency processors (see UOW docs for more details)"""
- pass
- def is_primary(self):
- """a return value of True indicates we are the primary MapperProperty for this loader's
- attribute on our mapper's class. It means we can set the object's attribute behavior
- at the class level. otherwise we have to set attribute behavior on a per-instance level."""
- return self.inherits is None and self.parent._is_primary_mapper()
-
-class MapperOption(object):
- """describes a modification to a Mapper in the context of making a copy
- of it. This is used to assist in the prototype pattern used by mapper.options()."""
- def process(self, mapper):
- raise NotImplementedError()
- def hash_key(self):
- return repr(self)
+ super(SelectionContext, self).__init__(mapper, kwargs.pop('with_options', []), **kwargs)
+
class ExtensionOption(MapperOption):
"""adds a new MapperExtension to a mapper's chain of extensions"""
def __init__(self, ext):
"""called after an object instance is DELETEed"""
return EXT_PASS
-class ExtensionCarrier(MapperExtension):
+class _ExtensionCarrier(MapperExtension):
def __init__(self):
self.elements = []
# TODO: shrink down this approach using __getattribute__ or similar
else:
return EXT_PASS
-class TranslatingDict(dict):
- """a dictionary that stores ColumnElement objects as keys. incoming ColumnElement
- keys are translated against those of an underling FromClause for all operations.
- This way the columns from any Selectable that is derived from or underlying this
- TranslatingDict's selectable can be used as keys."""
- def __init__(self, selectable):
- super(TranslatingDict, self).__init__()
- self.selectable = selectable
- def __translate_col(self, col):
- ourcol = self.selectable.corresponding_column(col, keys_ok=False, raiseerr=False)
- #if col is not ourcol:
- # print "TD TRANSLATING ", col, "TO", ourcol
- if ourcol is None:
- return col
- else:
- return ourcol
- def __getitem__(self, col):
- return super(TranslatingDict, self).__getitem__(self.__translate_col(col))
- def has_key(self, col):
- return super(TranslatingDict, self).has_key(self.__translate_col(col))
- def __setitem__(self, col, value):
- return super(TranslatingDict, self).__setitem__(self.__translate_col(col), value)
- def __contains__(self, col):
- return self.has_key(col)
- def setdefault(self, col, value):
- return super(TranslatingDict, self).setdefault(self.__translate_col(col), value)
class ClassKey(object):
"""keys a class and an entity name to a mapper, via the mapper_registry."""
from sqlalchemy import sql, schema, util, attributes, exceptions, sql_util, logging
import mapper
import sync
+import strategies
import session as sessionlib
import dependency
import util as mapperutil
import sets, random
+from interfaces import *
-class ColumnProperty(mapper.MapperProperty):
+
+class ColumnProperty(StrategizedProperty):
"""describes an object attribute that corresponds to a table column."""
def __init__(self, *columns, **kwargs):
"""the list of columns describes a single object property. if there
are multiple tables joined together for the mapper, this list represents
the equivalent column as it appears across each table."""
- self.deepcheck = kwargs.get('deepcheck', False)
self.columns = list(columns)
+ self.group = kwargs.pop('group', None)
+ self.deferred = kwargs.pop('deferred', False)
+ def create_strategy(self):
+ if self.deferred:
+ return strategies.DeferredColumnLoader(self)
+ else:
+ return strategies.ColumnLoader(self)
def getattr(self, object):
return getattr(object, self.key, None)
def setattr(self, object, value):
setattr(object, self.key, value)
def get_history(self, obj, passive=False):
return sessionlib.attribute_manager.get_history(obj, self.key, passive=passive)
- def copy(self):
- return ColumnProperty(*self.columns)
- def setup(self, statement, eagertable=None, **options):
- for c in self.columns:
- if eagertable is not None:
- statement.append_column(eagertable.corresponding_column(c))
- else:
- statement.append_column(c)
- def do_init(self):
- # establish a SmartProperty property manager on the object for this key
- if self.is_primary():
- self.logger.info("register managed attribute %s on class %s" % (self.key, self.parent.class_.__name__))
- sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, copy_function=lambda x: self.columns[0].type.copy_value(x), compare_function=lambda x,y:self.columns[0].type.compare_values(x,y), mutable_scalars=self.columns[0].type.is_mutable())
- def execute(self, selectcontext, instance, row, identitykey, isnew):
- if isnew:
- self.logger.debug("populating %s with %s/%s" % (mapperutil.attribute_str(instance, self.key), row.__class__.__name__, self.columns[0].key))
- # set a scalar object instance directly on the object,
- # bypassing SmartProperty event handlers.
- instance.__dict__[self.key] = row[self.columns[0]]
- def adapt_to_inherited(self, key, newparent):
- if newparent.concrete:
- return
- else:
- super(ColumnProperty, self).adapt_to_inherited(key, newparent)
- def __repr__(self):
- return "ColumnProperty(%s)" % repr([str(c) for c in self.columns])
ColumnProperty.logger = logging.class_logger(ColumnProperty)
-class DeferredColumnProperty(ColumnProperty):
- """describes an object attribute that corresponds to a table column, which also
- will "lazy load" its value from the table. this is per-column lazy loading."""
- def __init__(self, *columns, **kwargs):
- self.group = kwargs.pop('group', None)
- ColumnProperty.__init__(self, *columns, **kwargs)
- def copy(self):
- return DeferredColumnProperty(*self.columns)
- def do_init(self):
- # establish a SmartProperty property manager on the object for this key,
- # containing a callable to load in the attribute
- if self.is_primary():
- self.logger.info("register managed attribute %s on class %s" % (self.key, self.parent.class_.__name__))
- sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, callable_=lambda i:self.setup_loader(i), copy_function=lambda x: self.columns[0].type.copy_value(x), compare_function=lambda x,y:self.columns[0].type.compare_values(x,y), mutable_scalars=self.columns[0].type.is_mutable())
- def setup_loader(self, instance):
- if not self.localparent.is_assigned(instance):
- return mapper.object_mapper(instance).props[self.key].setup_loader(instance)
- def lazyload():
- self.logger.debug("deferred load %s group %s" % (mapperutil.attribute_str(instance, self.key), str(self.group)))
- try:
- pk = self.parent.pks_by_table[self.columns[0].table]
- except KeyError:
- pk = self.columns[0].table.primary_key
-
- clause = sql.and_()
- for primary_key in pk:
- attr = self.parent._getattrbycolumn(instance, primary_key)
- if not attr:
- return None
- clause.clauses.append(primary_key == attr)
- session = sessionlib.object_session(instance)
- if session is None:
- raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session; deferred load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key))
-
- if self.group is not None:
- groupcols = [p for p in self.localparent.props.values() if isinstance(p, DeferredColumnProperty) and p.group==self.group]
- result = session.execute(self.localparent, sql.select([g.columns[0] for g in groupcols], clause, use_labels=True), None)
- try:
- row = result.fetchone()
- for prop in groupcols:
- if prop is self:
- continue
- # set a scalar object instance directly on the object,
- # bypassing SmartProperty event handlers.
- sessionlib.attribute_manager.init_instance_attribute(instance, prop.key, uselist=False)
- instance.__dict__[prop.key] = row[prop.columns[0]]
- return row[self.columns[0]]
- finally:
- result.close()
- else:
- return session.scalar(self.localparent, sql.select([self.columns[0]], clause, use_labels=True),None)
-
- return lazyload
- def setup(self, statement, **options):
- pass
- def execute(self, selectcontext, instance, row, identitykey, isnew):
- if isnew:
- if not self.is_primary():
- sessionlib.attribute_manager.init_instance_attribute(instance, self.key, False, callable_=self.setup_loader(instance))
- else:
- sessionlib.attribute_manager.reset_instance_attribute(instance, self.key)
-
-DeferredColumnProperty.logger = logging.class_logger(DeferredColumnProperty)
-
mapper.ColumnProperty = ColumnProperty
-class PropertyLoader(mapper.MapperProperty):
- ONETOMANY = 0
- MANYTOONE = 1
- MANYTOMANY = 2
-
+class PropertyLoader(StrategizedProperty):
"""describes an object property that holds a single item or list of items that correspond
to a related database table."""
- def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreignkey=None, uselist=None, private=False, association=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False, cascade=None, viewonly=False):
+ def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreignkey=None, uselist=None, private=False, association=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False, cascade=None, viewonly=False, lazy=True):
self.uselist = uselist
self.argument = argument
self.secondary = secondary
self.post_update = post_update
self.direction = None
self.viewonly = viewonly
-
+ self.lazy = lazy
self.foreignkey = util.to_set(foreignkey)
if cascade is not None:
private = property(lambda s:s.cascade.delete_orphan)
+ def create_strategy(self):
+ if self.lazy:
+ return strategies.LazyLoader(self)
+ elif self.lazy is False:
+ return strategies.EagerLoader(self)
+ elif self.lazy is None:
+ return strategies.NoLoader(self)
+
def __str__(self):
return self.__class__.__name__ + " " + str(self.parent) + "->" + self.key + "->" + str(self.mapper)
callable_(c, mapper.entity_name)
mapper.cascade_callable(type, c, callable_, recursive)
- def copy(self):
- x = self.__class__.__new__(self.__class__)
- x.__dict__.update(self.__dict__)
- return x
-
- def do_init_subclass(self):
- """template method for subclasses of PropertyLoader"""
- pass
-
def _get_target_class(self):
"""return the target class of the relation, even if the property has not been initialized yet."""
if isinstance(self.argument, type):
if self.primaryjoin is None:
self.primaryjoin = sql.join(self.parent.unjoined_table, self.target).onclause
except exceptions.ArgumentError, e:
- raise exceptions.ArgumentError("Error determining primary and/or secondary join for relationship '%s' between mappers '%s' and '%s'. If the underlying error cannot be corrected, you should specify the 'primaryjoin' (and 'secondaryjoin', if there is an association table present) keyword arguments to the relation() function (or for backrefs, by specifying the backref using the backref() function with keyword arguments) to explicitly specify the join conditions. Nested error is \"%s\"" % (self.key, self.localparent, self.mapper, str(e)))
+ raise exceptions.ArgumentError("Error determining primary and/or secondary join for relationship '%s' between mappers '%s' and '%s'. If the underlying error cannot be corrected, you should specify the 'primaryjoin' (and 'secondaryjoin', if there is an association table present) keyword arguments to the relation() function (or for backrefs, by specifying the backref using the backref() function with keyword arguments) to explicitly specify the join conditions. Nested error is \"%s\"" % (self.key, self.parent, self.mapper, str(e)))
# if the foreign key wasnt specified and theres no assocaition table, try to figure
# out who is dependent on who. we dont need all the foreign keys represented in the join,
# just one of them.
if self.uselist is None:
self.uselist = True
-
- if self.inherits is not None:
- if hasattr(self.inherits, '_dependency_processor'):
- self._dependency_processor = self.inherits._dependency_processor
-
- if not hasattr(self, '_dependency_processor'):
- self._dependency_processor = dependency.create_dependency_processor(self)
-
- if self.inherits is not None and not hasattr(self.inherits, '_dependency_processor'):
- self.inherits._dependency_processor = self._dependency_processor
-
+ self._dependency_processor = dependency.create_dependency_processor(self)
# primary property handler, set up class attributes
if self.is_primary():
if self.backref is not None:
self.attributeext = self.backref.get_extension()
- # set our class attribute
- self._set_class_attribute(self.parent.class_, self.key)
-
if self.backref is not None:
self.backref.compile(self)
- elif self.inherits is None and not sessionlib.attribute_manager.is_class_managed(self.parent.class_, self.key):
+ elif not sessionlib.attribute_manager.is_class_managed(self.parent.class_, self.key):
raise exceptions.ArgumentError("Attempting to assign a new relation '%s' to a non-primary mapper on class '%s'. New relations can only be added to the primary mapper, i.e. the very first mapper created for class '%s' " % (self.key, self.parent.class_.__name__, self.parent.class_.__name__))
- self.do_init_subclass()
-
- def _register_attribute(self, class_, callable_=None):
- self.logger.info("register managed %s attribute %s on class %s" % ((self.uselist and "list-holding" or "scalar"), self.key, self.parent.class_.__name__))
- sessionlib.attribute_manager.register_attribute(class_, self.key, uselist = self.uselist, extension=self.attributeext, cascade=self.cascade, trackparent=True, callable_=callable_)
-
- def _init_instance_attribute(self, instance, callable_=None):
- return sessionlib.attribute_manager.init_instance_attribute(instance, self.key, self.uselist, cascade=self.cascade, trackparent=True, callable_=callable_)
+ super(PropertyLoader, self).do_init()
- def _set_class_attribute(self, class_, key):
- """sets attribute behavior on our target class."""
- self._register_attribute(class_)
-
def _is_self_referential(self):
return self.parent.mapped_table is self.target or self.parent.select_table is self.target
foreignkeys.add(binary.left)
elif binary.right.foreign_key is not None and binary.right.foreign_key.references(binary.left.table):
foreignkeys.add(binary.right)
- visitor = BinaryVisitor(foo)
+ visitor = mapperutil.BinaryVisitor(foo)
self.primaryjoin.accept_visitor(visitor)
self.foreignkey = foreignkeys
else:
return self.primaryjoin
- def execute(self, selectcontext, instance, row, identitykey, isnew):
- if self.is_primary():
- return
- #print "PLAIN PROPLOADER EXEC NON-PRIAMRY", repr(id(self)), repr(self.mapper.class_), self.key
- self._init_instance_attribute(instance)
-
def register_dependencies(self, uowcommit):
if not self.viewonly:
self._dependency_processor.register_dependencies(uowcommit)
-
PropertyLoader.logger = logging.class_logger(PropertyLoader)
-class LazyLoader(PropertyLoader):
- def do_init_subclass(self):
- (self.lazywhere, self.lazybinds, self.lazyreverse) = create_lazy_clause(self.parent.unjoined_table, self.primaryjoin, self.secondaryjoin, self.foreignkey)
- # determine if our "lazywhere" clause is the same as the mapper's
- # get() clause. then we can just use mapper.get()
- self.use_get = not self.uselist and self.mapper.query()._get_clause.compare(self.lazywhere)
-
- def _set_class_attribute(self, class_, key):
- # establish a class-level lazy loader on our class
- #print "SETCLASSATTR LAZY", repr(class_), key
- self._register_attribute(class_, callable_=lambda i: self.setup_loader(i))
-
- def setup_loader(self, instance):
- #print self, "setup_loader", "parent", self.parent.mapped_table, "child", self.mapper.mapped_table, "join", self.lazywhere
- # make sure our parent mapper is the one thats assigned to this instance, else call that one
- if not self.localparent.is_assigned(instance):
- # if no mapper association with this instance (i.e. not in a session, not loaded by a mapper),
- # then we cant set up a lazy loader
- if not mapper.has_mapper(instance):
- return None
- else:
- return mapper.object_mapper(instance).props[self.key].setup_loader(instance)
-
- def lazyload():
- self.logger.debug("lazy load attribute %s on instance %s" % (self.key, mapperutil.instance_str(instance)))
- params = {}
- allparams = True
- # if the instance wasnt loaded from the database, then it cannot lazy load
- # child items. one reason for this is that a bi-directional relationship
- # will not update properly, since bi-directional uses lazy loading functions
- # in both directions, and this instance will not be present in the lazily-loaded
- # results of the other objects since its not in the database
- if not mapper.has_identity(instance):
- return None
- #print "setting up loader, lazywhere", str(self.lazywhere), "binds", self.lazybinds
- for col, bind in self.lazybinds.iteritems():
- params[bind.key] = self.parent._getattrbycolumn(instance, col)
- if params[bind.key] is None:
- allparams = False
- break
-
- if not allparams:
- return None
-
- session = sessionlib.object_session(instance)
- if session is None:
- try:
- session = mapper.object_mapper(instance).get_session()
- except exceptions.InvalidRequestError:
- raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key))
-
- # if we have a simple straight-primary key load, use mapper.get()
- # to possibly save a DB round trip
- if self.use_get:
- ident = []
- for primary_key in self.mapper.pks_by_table[self.mapper.mapped_table]:
- bind = self.lazyreverse[primary_key]
- ident.append(params[bind.key])
- return self.mapper.using(session).get(ident)
- elif self.order_by is not False:
- order_by = self.order_by
- elif self.secondary is not None and self.secondary.default_order_by() is not None:
- order_by = self.secondary.default_order_by()
- else:
- order_by = False
- result = self.mapper.using(session).select_whereclause(self.lazywhere, order_by=order_by, params=params)
-
- if self.uselist:
- return result
- else:
- if len(result):
- return result[0]
- else:
- return None
- return lazyload
-
- def execute(self, selectcontext, instance, row, identitykey, isnew):
- if isnew:
- # new object instance being loaded from a result row
- if not self.is_primary():
- self.logger.debug("set instance-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key))
- # we are not the primary manager for this attribute on this class - set up a per-instance lazyloader,
- # which will override the clareset_instance_attributess-level behavior
- self._init_instance_attribute(instance, callable_=self.setup_loader(instance))
- else:
- self.logger.debug("set class-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key))
- # we are the primary manager for this attribute on this class - reset its per-instance attribute state,
- # so that the class-level lazy loader is executed when next referenced on this instance.
- # this usually is not needed unless the constructor of the object referenced the attribute before we got
- # to load data into it.
- sessionlib.attribute_manager.reset_instance_attribute(instance, self.key)
-
-LazyLoader.logger = logging.class_logger(LazyLoader)
-
-def create_lazy_clause(table, primaryjoin, secondaryjoin, foreignkey):
- binds = {}
- reverse = {}
- def column_in_table(table, column):
- return table.corresponding_column(column, raiseerr=False, keys_ok=False) is not None
-
- def bind_label():
- return "lazy_" + hex(random.randint(0, 65535))[2:]
- def visit_binary(binary):
- circular = isinstance(binary.left, schema.Column) and isinstance(binary.right, schema.Column) and binary.left.table is binary.right.table
- if isinstance(binary.left, schema.Column) and isinstance(binary.right, schema.Column) and ((not circular and column_in_table(table, binary.left)) or (circular and binary.right in foreignkey)):
- col = binary.left
- binary.left = binds.setdefault(binary.left,
- sql.BindParamClause(bind_label(), None, shortname=binary.left.name, type=binary.right.type))
- reverse[binary.right] = binds[col]
-
- if isinstance(binary.right, schema.Column) and isinstance(binary.left, schema.Column) and ((not circular and column_in_table(table, binary.right)) or (circular and binary.left in foreignkey)):
- col = binary.right
- binary.right = binds.setdefault(binary.right,
- sql.BindParamClause(bind_label(), None, shortname=binary.right.name, type=binary.left.type))
- reverse[binary.left] = binds[col]
-
- lazywhere = primaryjoin.copy_container()
- li = BinaryVisitor(visit_binary)
- lazywhere.accept_visitor(li)
- if secondaryjoin is not None:
- lazywhere = sql.and_(lazywhere, secondaryjoin)
- LazyLoader.logger.debug("create_lazy_clause " + str(lazywhere))
- return (lazywhere, binds, reverse)
-
-
-class EagerLoader(LazyLoader):
- """loads related objects inline with a parent query."""
- def do_init_subclass(self, recursion_stack=None):
- if self.parent.isa(self.mapper):
- raise exceptions.ArgumentError("Error creating eager relationship '%s' on parent class '%s' to child class '%s': Cant use eager loading on a self referential relationship." % (self.key, repr(self.parent.class_), repr(self.mapper.class_)))
- if recursion_stack is None:
- LazyLoader.do_init_subclass(self)
- self.parent._has_eager = True
-
- self.eagertarget = self.target.alias()
- if self.secondary:
- self.eagersecondary = self.secondary.alias()
- self.aliasizer = sql_util.Aliasizer(self.target, self.secondary, aliases={
- self.target:self.eagertarget,
- self.secondary:self.eagersecondary
- })
- #print "TARGET", self.target
- self.eagersecondaryjoin = self.secondaryjoin.copy_container()
- self.eagersecondaryjoin.accept_visitor(self.aliasizer)
- self.eagerprimary = self.primaryjoin.copy_container()
- self.eagerprimary.accept_visitor(self.aliasizer)
- #print "JOINS:", str(self.eagerprimary), "|", str(self.eagersecondaryjoin)
- else:
- self.aliasizer = sql_util.Aliasizer(self.target, aliases={self.target:self.eagertarget})
- self.eagerprimary = self.primaryjoin.copy_container()
- self.eagerprimary.accept_visitor(self.aliasizer)
-
- if self.order_by:
- self.eager_order_by = self._aliasize_orderby(self.order_by)
- else:
- self.eager_order_by = None
-
- def _create_eager_chain(self, recursion_stack=None):
- try:
- if self.__eager_chain_init == id(self):
- return
- except AttributeError:
- pass
-
- if recursion_stack is None:
- recursion_stack = {}
-
- eagerprops = []
- # create a new "eager chain", starting from this eager loader and descending downwards
- # through all sub-eagerloaders. this will copy all those eagerloaders and have them set up
- # aliases distinct to this eager chain. if a recursive relationship to any of the tables is detected,
- # the chain will terminate by copying that eager loader into a lazy loader.
- for key, prop in self.mapper.props.iteritems():
- if isinstance(prop, EagerLoader):
- eagerprops.append(prop)
-
- if len(eagerprops):
- recursion_stack[self.localparent.mapped_table] = True
- self.eagermapper = self.mapper.copy()
- try:
- for prop in eagerprops:
- if recursion_stack.has_key(prop.target):
- # recursion - set the relationship as a LazyLoader
- p = EagerLazyOption(None, False).create_prop(self.eagermapper, prop.key)
- continue
- p = prop.copy()
- self.eagermapper.props[prop.key] = p
-# print "we are:", id(self), self.target.name, (self.secondary and self.secondary.name or "None"), self.parent.mapped_table.name
-# print "prop is",id(prop), prop.target.name, (prop.secondary and prop.secondary.name or "None"), prop.parent.mapped_table.name
- p.do_init_subclass(recursion_stack)
- p._create_eager_chain(recursion_stack=recursion_stack)
- p.eagerprimary = p.eagerprimary.copy_container()
-# aliasizer = sql_util.Aliasizer(p.parent.mapped_table, aliases={p.parent.mapped_table:self.eagertarget})
- p.eagerprimary.accept_visitor(self.aliasizer)
- #print "new eagertqarget", p.eagertarget.name, (p.secondary and p.secondary.name or "none"), p.parent.mapped_table.name
- finally:
- del recursion_stack[self.localparent.mapped_table]
- else:
- self.eagermapper = self.mapper
- self._row_decorator = self._create_decorator_row()
- self.__eager_chain_init = id(self)
-
-# print "ROW DECORATOR", self._row_decorator
-
- def _aliasize_orderby(self, orderby, copy=True):
- if copy:
- orderby = [o.copy_container() for o in util.to_list(orderby)]
- else:
- orderby = util.to_list(orderby)
- for i in range(0, len(orderby)):
- if isinstance(orderby[i], schema.Column):
- orderby[i] = self.eagertarget.corresponding_column(orderby[i])
- else:
- orderby[i].accept_visitor(self.aliasizer)
- return orderby
-
- def setup(self, statement, eagertable=None, **options):
- """add a left outer join to the statement thats being constructed"""
-
- # initialize the "eager" chain of EagerLoader objects
- # this can't quite be done in the do_init_mapper() step
- self._create_eager_chain()
-
- if hasattr(statement, '_outerjoin'):
- towrap = statement._outerjoin
- elif isinstance(self.localparent.mapped_table, schema.Table):
- # if the mapper is against a plain Table, look in the from_obj of the select statement
- # to join against whats already there.
- for (fromclause, finder) in [(x, sql_util.TableFinder(x)) for x in statement.froms]:
- # dont join against an Alias'ed Select. we are really looking either for the
- # table itself or a Join that contains the table. this logic still might need
- # adjustments for scenarios not thought of yet.
- if not isinstance(fromclause, sql.Alias) and self.localparent.mapped_table in finder:
- towrap = fromclause
- break
- else:
- raise exceptions.InvalidRequestError("EagerLoader cannot locate a clause with which to outer join to, in query '%s' %s" % (str(statement), self.localparent.mapped_table))
- else:
- # if the mapper is against a select statement or something, we cant handle that at the
- # same time as a custom FROM clause right now.
- towrap = self.localparent.mapped_table
-
- if self.secondaryjoin is not None:
- statement._outerjoin = sql.outerjoin(towrap, self.eagersecondary, self.eagerprimary).outerjoin(self.eagertarget, self.eagersecondaryjoin)
- if self.order_by is False and self.secondary.default_order_by() is not None:
- statement.order_by(*self.eagersecondary.default_order_by())
- else:
- statement._outerjoin = towrap.outerjoin(self.eagertarget, self.eagerprimary)
- if self.order_by is False and self.eagertarget.default_order_by() is not None:
- statement.order_by(*self.eagertarget.default_order_by())
-
- if self.eager_order_by:
- statement.order_by(*util.to_list(self.eager_order_by))
- elif getattr(statement, 'order_by_clause', None):
- self._aliasize_orderby(statement.order_by_clause, False)
-
- statement.append_from(statement._outerjoin)
- for value in self.eagermapper.props.values():
- value.setup(statement, eagertable=self.eagertarget)
-
- def execute(self, selectcontext, instance, row, identitykey, isnew):
- """receive a row. tell our mapper to look for a new object instance in the row, and attach
- it to a list on the parent instance."""
-
- decorated_row = self._decorate_row(row)
- try:
- # check for identity key
- identity_key = self.eagermapper._row_identity_key(decorated_row)
- except KeyError:
- # else degrade to a lazy loader
- self.logger.debug("degrade to lazy loader on %s" % mapperutil.attribute_str(instance, self.key))
- LazyLoader.execute(self, selectcontext, instance, row, identitykey, isnew)
- return
-
-
- if not self.uselist:
- self.logger.debug("eagerload scalar instance on %s" % mapperutil.attribute_str(instance, self.key))
- if isnew:
- # set a scalar object instance directly on the parent object,
- # bypassing SmartProperty event handlers.
- instance.__dict__[self.key] = self.eagermapper._instance(selectcontext, decorated_row, None)
- else:
- # call _instance on the row, even though the object has been created,
- # so that we further descend into properties
- self.eagermapper._instance(selectcontext, decorated_row, None)
- else:
- if isnew:
- self.logger.debug("initialize UniqueAppender on %s" % mapperutil.attribute_str(instance, self.key))
- # call the SmartProperty's initialize() method to create a new, blank list
- l = getattr(instance.__class__, self.key).initialize(instance)
-
- # create an appender object which will add set-like semantics to the list
- appender = util.UniqueAppender(l.data)
-
- # store it in the "scratch" area, which is local to this load operation.
- selectcontext.attributes[(instance, self.key)] = appender
- result_list = selectcontext.attributes[(instance, self.key)]
- self.logger.debug("eagerload list instance on %s" % mapperutil.attribute_str(instance, self.key))
- self.eagermapper._instance(selectcontext, decorated_row, result_list)
-
- def _create_decorator_row(self):
- class EagerRowAdapter(object):
- def __init__(self, row):
- self.row = row
- def has_key(self, key):
- return map.has_key(key) or self.row.has_key(key)
- def __getitem__(self, key):
- if map.has_key(key):
- key = map[key]
- return self.row[key]
- def keys(self):
- return map.keys()
- map = {}
- for c in self.eagertarget.c:
- parent = self.target.corresponding_column(c)
- map[parent] = c
- map[parent._label] = c
- map[parent.name] = c
- return EagerRowAdapter
-
- def _decorate_row(self, row):
- # since the EagerLoader makes an Alias of its mapper's table,
- # we translate the actual result columns back to what they
- # would normally be into a "virtual row" which is passed to the child mapper.
- # that way the mapper doesnt have to know about the modified column name
- # (neither do any MapperExtensions). The row is keyed off the Column object
- # (which is what mappers use) as well as its "label" (which might be what
- # user-defined code is using)
- try:
- return self._row_decorator(row)
- except AttributeError:
- # insure the "eager chain" step occurred
- self._create_eager_chain()
- return self._row_decorator(row)
-
-EagerLoader.logger = logging.class_logger(EagerLoader)
-
-class GenericOption(mapper.MapperOption):
- """a mapper option that can handle dotted property names,
- descending down through the relations of a mapper until it
- reaches the target."""
- def __init__(self, key):
- self.key = key
- def process(self, mapper):
- self.process_by_key(mapper, self.key)
- def process_by_key(self, mapper, key):
- tokens = key.split('.', 1)
- if len(tokens) > 1:
- oldprop = mapper.props[tokens[0]]
- newprop = oldprop.copy()
- newprop.argument = self.process_by_key(oldprop.mapper.copy(), tokens[1])
- mapper._compile_property(tokens[0], newprop)
- else:
- self.create_prop(mapper, tokens[0])
- return mapper
-
- def create_prop(self, mapper, key):
- kwargs = util.constructor_args(oldprop)
- mapper._compile_property(key, class_(**kwargs ))
-
-
class BackRef(object):
"""stores the name of a backreference property as well as options to
be used on the resulting PropertyLoader."""
if not mapper.props.has_key(self.key):
pj = self.kwargs.pop('primaryjoin', None)
sj = self.kwargs.pop('secondaryjoin', None)
- # the backref will compile its own primary/secondary join conditions. if you have it
- # use the pj/sj of the parent relation in all cases, a bunch of polymorphic unit tests
- # fail (maybe we can look into that too).
- # the PropertyLoader class is currently constructing BackRef objects using the explictly
- # passed primary/secondary join conditions, if the backref was passed to it as just a string.
- #if pj is None:
- # if prop.secondaryjoin is not None:
- # if setting up a backref to a many-to-many, reverse the order
- # of the "primary" and "secondary" joins
- # pj = prop.secondaryjoin
- # sj = prop.primaryjoin
- # else:
- # pj = prop.primaryjoin
- # sj = None
- lazy = self.kwargs.pop('lazy', True)
- if lazy:
- cls = LazyLoader
- else:
- cls = EagerLoader
# the backref property is set on the primary mapper
parent = prop.parent.primary_mapper()
- relation = cls(parent, prop.secondary, pj, sj, backref=prop.key, is_backref=True, **self.kwargs)
+ relation = PropertyLoader(parent, prop.secondary, pj, sj, backref=prop.key, is_backref=True, **self.kwargs)
mapper._compile_property(self.key, relation);
elif not isinstance(mapper.props[self.key], PropertyLoader):
raise exceptions.ArgumentError("Cant create backref '%s' on mapper '%s'; an incompatible property of that name already exists" % (self.key, str(mapper)))
def get_extension(self):
"""returns an attribute extension to use with this backreference."""
return attributes.GenericBackrefExtension(self.key)
-
-class EagerLazyOption(GenericOption):
- """an option that switches a PropertyLoader to be an EagerLoader or LazyLoader"""
- def __init__(self, key, toeager = True, **kwargs):
- self.key = key
- self.toeager = toeager
- self.kwargs = kwargs
-
- def hash_key(self):
- return "EagerLazyOption(%s, %s)" % (repr(self.key), repr(self.toeager))
-
- def create_prop(self, mapper, key):
- if self.toeager:
- class_ = EagerLoader
- elif self.toeager is None:
- class_ = PropertyLoader
- else:
- class_ = LazyLoader
-
- oldprop = mapper.props[key]
- newprop = class_.__new__(class_)
- newprop.__dict__.update(oldprop.__dict__)
- #newprop.do_init_subclass()
- p = newprop
- while p.inherits is not None:
- p = p.inherits
- real_parent_mapper = p.parent
- real_parent_mapper._compile_property(key, newprop, localparent=mapper)
-
-class DeferredOption(GenericOption):
- def __init__(self, key, defer=False, **kwargs):
- self.key = key
- self.defer = defer
- self.kwargs = kwargs
- def hash_key(self):
- return "DeferredOption(%s,%s)" % (self.key, self.defer)
- def create_prop(self, mapper, key):
- oldprop = mapper.props[key]
- if self.defer:
- prop = DeferredColumnProperty(*oldprop.columns, **self.kwargs)
- else:
- prop = ColumnProperty(*oldprop.columns, **self.kwargs)
- mapper._compile_property(key, prop)
-
-class DeferGroupOption(mapper.MapperOption):
- def __init__(self, group, defer=False, **kwargs):
- self.group = group
- self.defer = defer
- self.kwargs = kwargs
- def process(self, mapper):
- self.process_by_key(mapper, self.key)
-
-class BinaryVisitor(sql.ClauseVisitor):
- def __init__(self, func):
- self.func = func
- def visit_binary(self, binary):
- self.func(binary)
from sqlalchemy import sql, util, exceptions, sql_util
import mapper
-
+from interfaces import OperationContext
class Query(object):
"""encapsulates the object-fetching operations provided by Mappers."""
- def __init__(self, class_or_mapper, session=None, entity_name=None, lockmode=None, **kwargs):
+ def __init__(self, class_or_mapper, session=None, entity_name=None, lockmode=None, with_options=None, **kwargs):
if isinstance(class_or_mapper, type):
self.mapper = mapper.class_mapper(class_or_mapper, entity_name=entity_name)
else:
self.mapper = class_or_mapper.compile()
+ self.with_options = with_options or []
self.mapper = self.mapper.get_select_mapper().compile()
self.always_refresh = kwargs.pop('always_refresh', self.mapper.always_refresh)
self.order_by = kwargs.pop('order_by', self.mapper.order_by)
def options(self, *args, **kwargs):
"""returns a new Query object using the given MapperOptions."""
- return self.mapper.options(*args, **kwargs).using(session=self._session)
+ return Query(self.mapper, self._session, with_options=args)
def with_lockmode(self, mode):
"""return a new Query object with the specified locking mode."""
def instances(self, clauseelement, params=None, *args, **kwargs):
result = self.session.execute(self.mapper, clauseelement, params=params)
try:
- return self.mapper.instances(result, self.session, **kwargs)
+ return self.mapper.instances(result, self.session, with_options=self.with_options, **kwargs)
finally:
result.close()
params = {}
return self.instances(statement, params=params, **kwargs)
- def _should_nest(self, **kwargs):
+ def _should_nest(self, querycontext):
"""return True if the given statement options indicate that we should "nest" the
generated query as a subquery inside of a larger eager-loading query. this is used
with keywords like distinct, limit and offset and the mapper defines eager loads."""
return (
self.mapper.has_eager()
- and self._nestable(**kwargs)
+ and self._nestable(**querycontext.select_args())
)
def _nestable(self, **kwargs):
"""return true if the given statement options imply it should be nested."""
- return (kwargs.has_key('limit') or kwargs.has_key('offset') or kwargs.get('distinct', False))
+ return (kwargs.get('limit') is not None or kwargs.get('offset') is not None or kwargs.get('distinct', False))
def compile(self, whereclause = None, **kwargs):
- order_by = kwargs.pop('order_by', False)
- from_obj = kwargs.pop('from_obj', [])
- lockmode = kwargs.pop('lockmode', self.lockmode)
+ context = kwargs.pop('query_context', None)
+ if context is None:
+ context = QueryContext(self, kwargs)
+ order_by = context.order_by
+ from_obj = context.from_obj
+ lockmode = context.lockmode
+ distinct = context.distinct
+ limit = context.limit
+ offset = context.offset
if order_by is False:
order_by = self.order_by
if order_by is False:
if self.table not in alltables:
from_obj.append(self.table)
- if self._should_nest(**kwargs):
+ if self._should_nest(context):
# if theres an order by, add those columns to the column list
# of the "rowcount" query we're going to make
if order_by:
else:
cf = []
- s2 = sql.select(self.table.primary_key + list(cf), whereclause, use_labels=True, from_obj=from_obj, **kwargs)
-# raise "ok first thing", str(s2)
- if not kwargs.get('distinct', False) and order_by:
+ s2 = sql.select(self.table.primary_key + list(cf), whereclause, use_labels=True, from_obj=from_obj, **context.select_args())
+ if not distinct and order_by:
s2.order_by(*util.to_list(order_by))
s3 = s2.alias('tbl_row_count')
crit = []
for i in range(0, len(self.table.primary_key)):
crit.append(s3.primary_key[i] == self.table.primary_key[i])
statement = sql.select([], sql.and_(*crit), from_obj=[self.table], use_labels=True, for_update=for_update)
- # raise "OK statement", str(statement)
-
# now for the order by, convert the columns to their corresponding columns
# in the "rowcount" query, and tack that new order by onto the "rowcount" query
if order_by:
[o.accept_visitor(aliasizer) for o in order_by]
statement.order_by(*util.to_list(order_by))
else:
- statement = sql.select([], whereclause, from_obj=from_obj, use_labels=True, for_update=for_update, **kwargs)
+ statement = sql.select([], whereclause, from_obj=from_obj, use_labels=True, for_update=for_update, **context.select_args())
if order_by:
statement.order_by(*util.to_list(order_by))
# for a DISTINCT query, you need the columns explicitly specified in order
# TODO: this should be done at the SQL level not the mapper level
if kwargs.get('distinct', False) and order_by:
[statement.append_column(c) for c in util.to_list(order_by)]
- # plugin point
+ context.statement = statement
# give all the attached properties a chance to modify the query
for value in self.mapper.props.values():
- value.setup(statement, **kwargs)
+ value.setup(context)
return statement
+class QueryContext(OperationContext):
+ """created within the Query.compile() method to store and share
+ state among all the Mappers and MapperProperty objects used in a query construction."""
+ def __init__(self, query, kwargs):
+ self.query = query
+ self.order_by = kwargs.pop('order_by', False)
+ self.from_obj = kwargs.pop('from_obj', [])
+ self.lockmode = kwargs.pop('lockmode', query.lockmode)
+ self.distinct = kwargs.pop('distinct', False)
+ self.limit = kwargs.pop('limit', None)
+ self.offset = kwargs.pop('offset', None)
+ self.statement = None
+ super(QueryContext, self).__init__(query.mapper, query.with_options, **kwargs)
+ def select_args(self):
+ return {'limit':self.limit, 'offset':self.offset, 'distinct':self.distinct}
+
\ No newline at end of file
if e is None:
raise exceptions.InvalidRequestError("Could not locate any Engine bound to mapper '%s'" % str(mapper))
return e
- def query(self, mapper_or_class, entity_name=None):
+ def query(self, mapper_or_class, entity_name=None, **kwargs):
"""return a new Query object corresponding to this Session and the mapper, or the classes' primary mapper."""
if isinstance(mapper_or_class, type):
- return query.Query(class_mapper(mapper_or_class, entity_name=entity_name), self)
+ return query.Query(class_mapper(mapper_or_class, entity_name=entity_name), self, **kwargs)
else:
- return query.Query(mapper_or_class, self)
+ return query.Query(mapper_or_class, self, **kwargs)
def _sql(self):
class SQLProxy(object):
def __getattr__(self, key):
--- /dev/null
+# strategies.py
+# Copyright (C) 2005,2006 Michael Bayer mike_mp@zzzcomputing.com
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+from sqlalchemy import sql, schema, util, attributes, exceptions, sql_util, logging
+import mapper
+from interfaces import *
+import session as sessionlib
+import util as mapperutil
+import sets, random
+
+
+class ColumnLoader(LoaderStrategy):
+ def init(self):
+ super(ColumnLoader, self).init()
+ self.columns = self.parent_property.columns
+ def setup_query(self, context, eagertable=None, **kwargs):
+ for c in self.columns:
+ if eagertable is not None:
+ context.statement.append_column(eagertable.corresponding_column(c))
+ else:
+ context.statement.append_column(c)
+
+ def init_class_attribute(self):
+ self.logger.info("register managed attribute %s on class %s" % (self.key, self.parent.class_.__name__))
+ sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, copy_function=lambda x: self.columns[0].type.copy_value(x), compare_function=lambda x,y:self.columns[0].type.compare_values(x,y), mutable_scalars=self.columns[0].type.is_mutable())
+
+ def process_row(self, selectcontext, instance, row, identitykey, isnew):
+ if isnew:
+ self.logger.debug("populating %s with %s/%s" % (mapperutil.attribute_str(instance, self.key), row.__class__.__name__, self.columns[0].key))
+ instance.__dict__[self.key] = row[self.columns[0]]
+
+ColumnLoader.logger = logging.class_logger(ColumnLoader)
+
+class DeferredColumnLoader(LoaderStrategy):
+ """describes an object attribute that corresponds to a table column, which also
+ will "lazy load" its value from the table. this is per-column lazy loading."""
+ def init(self):
+ super(DeferredColumnLoader, self).init()
+ self.columns = self.parent_property.columns
+ self.group = self.parent_property.group
+
+ def init_class_attribute(self):
+ self.logger.info("register managed attribute %s on class %s" % (self.key, self.parent.class_.__name__))
+ sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, callable_=lambda i:self.setup_loader(i), copy_function=lambda x: self.columns[0].type.copy_value(x), compare_function=lambda x,y:self.columns[0].type.compare_values(x,y), mutable_scalars=self.columns[0].type.is_mutable())
+
+ def setup_query(self, context, **kwargs):
+ pass
+
+ def process_row(self, selectcontext, instance, row, identitykey, isnew):
+ if isnew:
+ if not self.is_default or len(selectcontext.options):
+ sessionlib.attribute_manager.init_instance_attribute(instance, self.key, False, callable_=self.setup_loader(instance, selectcontext.options))
+ else:
+ sessionlib.attribute_manager.reset_instance_attribute(instance, self.key)
+
+ def setup_loader(self, instance, options=None):
+ if not mapper.has_mapper(instance):
+ return None
+ else:
+ prop = mapper.object_mapper(instance).props[self.key]
+ if prop is not self.parent_property:
+ return prop._get_strategy(DeferredColumnLoader).setup_loader(instance)
+ def lazyload():
+ self.logger.debug("deferred load %s group %s" % (mapperutil.attribute_str(instance, self.key), str(self.group)))
+ try:
+ pk = self.parent.pks_by_table[self.columns[0].table]
+ except KeyError:
+ pk = self.columns[0].table.primary_key
+
+ clause = sql.and_()
+ for primary_key in pk:
+ attr = self.parent._getattrbycolumn(instance, primary_key)
+ if not attr:
+ return None
+ clause.clauses.append(primary_key == attr)
+
+ session = sessionlib.object_session(instance)
+ if session is None:
+ raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session; deferred load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key))
+
+ localparent = mapper.object_mapper(instance)
+ if self.group is not None:
+ groupcols = [p for p in localparent.props.values() if isinstance(p.strategy, DeferredColumnLoader) and p.group==self.group]
+ result = session.execute(localparent, sql.select([g.columns[0] for g in groupcols], clause, use_labels=True), None)
+ try:
+ row = result.fetchone()
+ for prop in groupcols:
+ if prop is self:
+ continue
+ # set a scalar object instance directly on the object,
+ # bypassing SmartProperty event handlers.
+ sessionlib.attribute_manager.init_instance_attribute(instance, prop.key, uselist=False)
+ instance.__dict__[prop.key] = row[prop.columns[0]]
+ return row[self.columns[0]]
+ finally:
+ result.close()
+ else:
+ return session.scalar(localparent, sql.select([self.columns[0]], clause, use_labels=True),None)
+
+ return lazyload
+
+DeferredColumnLoader.logger = logging.class_logger(DeferredColumnLoader)
+
+class DeferredOption(StrategizedOption):
+ def __init__(self, key, defer=False):
+ super(DeferredOption, self).__init__(key)
+ self.defer = defer
+ def get_strategy_class(self):
+ if self.defer:
+ return DeferredColumnLoader
+ else:
+ return ColumnLoader
+
+class AbstractRelationLoader(LoaderStrategy):
+ def init(self):
+ super(AbstractRelationLoader, self).init()
+ self.primaryjoin = self.parent_property.primaryjoin
+ self.secondaryjoin = self.parent_property.secondaryjoin
+ self.secondary = self.parent_property.secondary
+ self.foreignkey = self.parent_property.foreignkey
+ self.mapper = self.parent_property.mapper
+ self.target = self.parent_property.target
+ self.uselist = self.parent_property.uselist
+ self.cascade = self.parent_property.cascade
+ self.attributeext = self.parent_property.attributeext
+ self.order_by = self.parent_property.order_by
+
+ def _init_instance_attribute(self, instance, callable_=None):
+ return sessionlib.attribute_manager.init_instance_attribute(instance, self.key, self.uselist, cascade=self.cascade, trackparent=True, callable_=callable_)
+
+ def _register_attribute(self, class_, callable_=None):
+ self.logger.info("register managed %s attribute %s on class %s" % ((self.uselist and "list-holding" or "scalar"), self.key, self.parent.class_.__name__))
+ sessionlib.attribute_manager.register_attribute(class_, self.key, uselist = self.uselist, extension=self.attributeext, cascade=self.cascade, trackparent=True, callable_=callable_)
+
+class NoLoader(AbstractRelationLoader):
+ def process_row(self, selectcontext, instance, row, identitykey, isnew):
+ if isnew:
+ if not self.is_default or len(selectcontext.options):
+ self.logger.debug("set instance-level no loader on %s" % mapperutil.attribute_str(instance, self.key))
+ self._init_instance_attribute(instance)
+
+NoLoader.logger = logging.class_logger(NoLoader)
+
+class LazyLoader(AbstractRelationLoader):
+ def init(self):
+ super(LazyLoader, self).init()
+ (self.lazywhere, self.lazybinds, self.lazyreverse) = self._create_lazy_clause(self.parent.unjoined_table, self.primaryjoin, self.secondaryjoin, self.foreignkey)
+ # determine if our "lazywhere" clause is the same as the mapper's
+ # get() clause. then we can just use mapper.get()
+ self.use_get = not self.uselist and self.mapper.query()._get_clause.compare(self.lazywhere)
+
+ def init_class_attribute(self):
+ self._register_attribute(self.parent.class_, callable_=lambda i: self.setup_loader(i))
+
+ def setup_loader(self, instance, options=None):
+ if not mapper.has_mapper(instance):
+ return None
+ else:
+ prop = mapper.object_mapper(instance).props[self.key]
+ if prop is not self.parent_property:
+ return prop._get_strategy(LazyLoader).setup_loader(instance)
+ def lazyload():
+ self.logger.debug("lazy load attribute %s on instance %s" % (self.key, mapperutil.instance_str(instance)))
+ params = {}
+ allparams = True
+ # if the instance wasnt loaded from the database, then it cannot lazy load
+ # child items. one reason for this is that a bi-directional relationship
+ # will not update properly, since bi-directional uses lazy loading functions
+ # in both directions, and this instance will not be present in the lazily-loaded
+ # results of the other objects since its not in the database
+ if not mapper.has_identity(instance):
+ return None
+ #print "setting up loader, lazywhere", str(self.lazywhere), "binds", self.lazybinds
+ for col, bind in self.lazybinds.iteritems():
+ params[bind.key] = self.parent._getattrbycolumn(instance, col)
+ if params[bind.key] is None:
+ allparams = False
+ break
+
+ if not allparams:
+ return None
+
+ session = sessionlib.object_session(instance)
+ if session is None:
+ try:
+ session = mapper.object_mapper(instance).get_session()
+ except exceptions.InvalidRequestError:
+ raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key))
+
+ # if we have a simple straight-primary key load, use mapper.get()
+ # to possibly save a DB round trip
+ if self.use_get:
+ ident = []
+ for primary_key in self.mapper.pks_by_table[self.mapper.mapped_table]:
+ bind = self.lazyreverse[primary_key]
+ ident.append(params[bind.key])
+ return self.mapper.using(session).get(ident)
+ elif self.order_by is not False:
+ order_by = self.order_by
+ elif self.secondary is not None and self.secondary.default_order_by() is not None:
+ order_by = self.secondary.default_order_by()
+ else:
+ order_by = False
+ result = session.query(self.mapper, with_options=options).select_whereclause(self.lazywhere, order_by=order_by, params=params)
+
+ if self.uselist:
+ return result
+ else:
+ if len(result):
+ return result[0]
+ else:
+ return None
+ return lazyload
+
+ def process_row(self, selectcontext, instance, row, identitykey, isnew):
+ if isnew:
+ # new object instance being loaded from a result row
+ if not self.is_default or len(selectcontext.options):
+ self.logger.debug("set instance-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key))
+ # we are not the primary manager for this attribute on this class - set up a per-instance lazyloader,
+ # which will override the clareset_instance_attributess-level behavior
+ self._init_instance_attribute(instance, callable_=self.setup_loader(instance, selectcontext.options))
+ else:
+ self.logger.debug("set class-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key))
+ # we are the primary manager for this attribute on this class - reset its per-instance attribute state,
+ # so that the class-level lazy loader is executed when next referenced on this instance.
+ # this usually is not needed unless the constructor of the object referenced the attribute before we got
+ # to load data into it.
+ sessionlib.attribute_manager.reset_instance_attribute(instance, self.key)
+
+ def _create_lazy_clause(self, table, primaryjoin, secondaryjoin, foreignkey):
+ binds = {}
+ reverse = {}
+ def column_in_table(table, column):
+ return table.corresponding_column(column, raiseerr=False, keys_ok=False) is not None
+
+ def bind_label():
+ return "lazy_" + hex(random.randint(0, 65535))[2:]
+ def visit_binary(binary):
+ circular = isinstance(binary.left, schema.Column) and isinstance(binary.right, schema.Column) and binary.left.table is binary.right.table
+ if isinstance(binary.left, schema.Column) and isinstance(binary.right, schema.Column) and ((not circular and column_in_table(table, binary.left)) or (circular and binary.right in foreignkey)):
+ col = binary.left
+ binary.left = binds.setdefault(binary.left,
+ sql.BindParamClause(bind_label(), None, shortname=binary.left.name, type=binary.right.type))
+ reverse[binary.right] = binds[col]
+
+ if isinstance(binary.right, schema.Column) and isinstance(binary.left, schema.Column) and ((not circular and column_in_table(table, binary.right)) or (circular and binary.left in foreignkey)):
+ col = binary.right
+ binary.right = binds.setdefault(binary.right,
+ sql.BindParamClause(bind_label(), None, shortname=binary.right.name, type=binary.left.type))
+ reverse[binary.left] = binds[col]
+
+ lazywhere = primaryjoin.copy_container()
+ li = mapperutil.BinaryVisitor(visit_binary)
+ lazywhere.accept_visitor(li)
+ if secondaryjoin is not None:
+ lazywhere = sql.and_(lazywhere, secondaryjoin)
+ LazyLoader.logger.debug("create_lazy_clause " + str(lazywhere))
+ return (lazywhere, binds, reverse)
+
+LazyLoader.logger = logging.class_logger(LazyLoader)
+
+
+
+class EagerLoader(AbstractRelationLoader):
+ """loads related objects inline with a parent query."""
+ def init(self):
+ super(EagerLoader, self).init()
+ if self.parent.isa(self.mapper):
+ raise exceptions.ArgumentError("Error creating eager relationship '%s' on parent class '%s' to child class '%s': Cant use eager loading on a self referential relationship." % (self.key, repr(self.parent.class_), repr(self.mapper.class_)))
+ self.parent._has_eager = True
+
+ self.clauses = {}
+ self.clauses_by_lead_mapper = {}
+
+ class AliasedClauses(object):
+ """defines a set of join conditions and table aliases which are aliased on a randomly-generated
+ alias name, corresponding to the connection of an optional parent AliasedClauses object and a
+ target mapper.
+
+ EagerLoader has a distinct AliasedClauses object per parent AliasedClauses object,
+ so that all paths from one mapper to another across a chain of eagerloaders generates a distinct
+ chain of joins. The AliasedClauses objects are generated and cached on an as-needed basis.
+
+ e.g.:
+
+ mapper A -->
+ (EagerLoader 'items') -->
+ mapper B -->
+ (EagerLoader 'keywords') -->
+ mapper C
+
+ will generate:
+
+ EagerLoader 'items' --> {
+ None : AliasedClauses(items, None, alias_suffix='AB34') # mappera JOIN mapperb_AB34
+ }
+
+ EagerLoader 'keywords' --> [
+ None : AliasedClauses(keywords, None, alias_suffix='43EF') # mapperb JOIN mapperc_43EF
+ AliasedClauses(items, None, alias_suffix='AB34') :
+ AliasedClauses(keywords, items, alias_suffix='8F44') # mapperb_AB34 JOIN mapperc_8F44
+ ]
+ """
+ def __init__(self, eagerloader, parentclauses=None):
+ self.parent = eagerloader
+ self.target = eagerloader.target
+ self.eagertarget = eagerloader.target.alias()
+ if eagerloader.secondary:
+ self.eagersecondary = eagerloader.secondary.alias()
+ self.aliasizer = sql_util.Aliasizer(eagerloader.target, eagerloader.secondary, aliases={
+ eagerloader.target:self.eagertarget,
+ eagerloader.secondary:self.eagersecondary
+ })
+ self.eagersecondaryjoin = eagerloader.secondaryjoin.copy_container()
+ self.eagersecondaryjoin.accept_visitor(self.aliasizer)
+ self.eagerprimary = eagerloader.primaryjoin.copy_container()
+ self.eagerprimary.accept_visitor(self.aliasizer)
+ else:
+ self.aliasizer = sql_util.Aliasizer(eagerloader.target, aliases={eagerloader.target:self.eagertarget})
+ self.eagerprimary = eagerloader.primaryjoin.copy_container()
+ self.eagerprimary.accept_visitor(self.aliasizer)
+
+ if parentclauses is not None:
+ self.eagerprimary.accept_visitor(parentclauses.aliasizer)
+
+ if eagerloader.order_by:
+ self.eager_order_by = self._aliasize_orderby(eagerloader.order_by)
+ else:
+ self.eager_order_by = None
+
+ self._row_decorator = self._create_decorator_row()
+
+ def _aliasize_orderby(self, orderby, copy=True):
+ if copy:
+ orderby = [o.copy_container() for o in util.to_list(orderby)]
+ else:
+ orderby = util.to_list(orderby)
+ for i in range(0, len(orderby)):
+ if isinstance(orderby[i], schema.Column):
+ orderby[i] = self.eagertarget.corresponding_column(orderby[i])
+ else:
+ orderby[i].accept_visitor(self.aliasizer)
+ return orderby
+
+ def _create_decorator_row(self):
+ class EagerRowAdapter(object):
+ def __init__(self, row):
+ self.row = row
+ def has_key(self, key):
+ return map.has_key(key) or self.row.has_key(key)
+ def __getitem__(self, key):
+ if map.has_key(key):
+ key = map[key]
+ return self.row[key]
+ def keys(self):
+ return map.keys()
+ map = {}
+ for c in self.eagertarget.c:
+ parent = self.target.corresponding_column(c)
+ map[parent] = c
+ map[parent._label] = c
+ map[parent.name] = c
+ return EagerRowAdapter
+
+ def _decorate_row(self, row):
+ # adapts a row at row iteration time to transparently
+ # convert plain columns into the aliased columns that were actually
+ # added to the column clause of the SELECT.
+ return self._row_decorator(row)
+
+ def init_class_attribute(self):
+ self.parent_property._get_strategy(LazyLoader).init_class_attribute()
+
+ def setup_query(self, context, eagertable=None, parentclauses=None, parentmapper=None, **kwargs):
+ """add a left outer join to the statement thats being constructed"""
+ if parentmapper is None:
+ localparent = context.mapper
+ else:
+ localparent = parentmapper
+
+ if self in context.recursion_stack:
+ return
+ else:
+ context.recursion_stack.add(self)
+
+ statement = context.statement
+
+ if hasattr(statement, '_outerjoin'):
+ towrap = statement._outerjoin
+ elif isinstance(localparent.mapped_table, schema.Table):
+ # if the mapper is against a plain Table, look in the from_obj of the select statement
+ # to join against whats already there.
+ for (fromclause, finder) in [(x, sql_util.TableFinder(x)) for x in statement.froms]:
+ # dont join against an Alias'ed Select. we are really looking either for the
+ # table itself or a Join that contains the table. this logic still might need
+ # adjustments for scenarios not thought of yet.
+ if not isinstance(fromclause, sql.Alias) and localparent.mapped_table in finder:
+ towrap = fromclause
+ break
+ else:
+ raise exceptions.InvalidRequestError("EagerLoader cannot locate a clause with which to outer join to, in query '%s' %s" % (str(statement), self.localparent.mapped_table))
+ else:
+ # if the mapper is against a select statement or something, we cant handle that at the
+ # same time as a custom FROM clause right now.
+ towrap = localparent.mapped_table
+
+ try:
+ clauses = self.clauses[parentclauses]
+ except KeyError:
+ clauses = EagerLoader.AliasedClauses(self, parentclauses)
+ self.clauses[parentclauses] = clauses
+ self.clauses_by_lead_mapper[context.mapper] = clauses
+
+ if self.secondaryjoin is not None:
+ statement._outerjoin = sql.outerjoin(towrap, clauses.eagersecondary, clauses.eagerprimary).outerjoin(clauses.eagertarget, clauses.eagersecondaryjoin)
+ if self.order_by is False and self.secondary.default_order_by() is not None:
+ statement.order_by(*clauses.eagersecondary.default_order_by())
+ else:
+ statement._outerjoin = towrap.outerjoin(clauses.eagertarget, clauses.eagerprimary)
+ if self.order_by is False and clauses.eagertarget.default_order_by() is not None:
+ statement.order_by(*clauses.eagertarget.default_order_by())
+
+ if clauses.eager_order_by:
+ statement.order_by(*util.to_list(clauses.eager_order_by))
+ elif getattr(statement, 'order_by_clause', None):
+ clauses._aliasize_orderby(statement.order_by_clause, False)
+
+ statement.append_from(statement._outerjoin)
+ for value in self.mapper.props.values():
+ value.setup(context, eagertable=clauses.eagertarget, parentclauses=clauses, parentmapper=self.mapper)
+
+ def process_row(self, selectcontext, instance, row, identitykey, isnew):
+ """receive a row. tell our mapper to look for a new object instance in the row, and attach
+ it to a list on the parent instance."""
+
+ if self in selectcontext.recursion_stack:
+ return
+
+ try:
+ clauses = self.clauses_by_lead_mapper[selectcontext.mapper]
+ decorated_row = clauses._decorate_row(row)
+ # check for identity key
+ identity_key = self.mapper._row_identity_key(decorated_row)
+ except KeyError:
+ # else degrade to a lazy loader
+ self.logger.debug("degrade to lazy loader on %s" % mapperutil.attribute_str(instance, self.key))
+ self.parent_property._get_strategy(LazyLoader).process_row(selectcontext, instance, row, identitykey, isnew)
+ return
+
+ if not self.uselist:
+ self.logger.debug("eagerload scalar instance on %s" % mapperutil.attribute_str(instance, self.key))
+ if isnew:
+ # set a scalar object instance directly on the parent object,
+ # bypassing SmartProperty event handlers.
+ instance.__dict__[self.key] = self.mapper._instance(selectcontext, decorated_row, None)
+ else:
+ # call _instance on the row, even though the object has been created,
+ # so that we further descend into properties
+ self.mapper._instance(selectcontext, decorated_row, None)
+ else:
+ if isnew:
+ self.logger.debug("initialize UniqueAppender on %s" % mapperutil.attribute_str(instance, self.key))
+ # call the SmartProperty's initialize() method to create a new, blank list
+ l = getattr(instance.__class__, self.key).initialize(instance)
+
+ # create an appender object which will add set-like semantics to the list
+ appender = util.UniqueAppender(l.data)
+
+ # store it in the "scratch" area, which is local to this load operation.
+ selectcontext.attributes[(instance, self.key)] = appender
+ result_list = selectcontext.attributes[(instance, self.key)]
+ self.logger.debug("eagerload list instance on %s" % mapperutil.attribute_str(instance, self.key))
+ # TODO: recursion check a speed hit...? try to get a "termination point" into the AliasedClauses
+ # or EagerRowAdapter ?
+ selectcontext.recursion_stack.add(self)
+ try:
+ self.mapper._instance(selectcontext, decorated_row, result_list)
+ finally:
+ selectcontext.recursion_stack.remove(self)
+
+EagerLoader.logger = logging.class_logger(EagerLoader)
+
+class EagerLazyOption(StrategizedOption):
+ def __init__(self, key, lazy=True):
+ super(EagerLazyOption, self).__init__(key)
+ self.lazy = lazy
+ def get_strategy_class(self):
+ if self.lazy:
+ return LazyLoader
+ elif self.lazy is False:
+ return EagerLoader
+ elif self.lazy is None:
+ return NoLoader
+
+
+
result.append(sql.select([col(name, table) for name in colnames], from_obj=[table]))
return sql.union_all(*result).alias(aliasname)
+class TranslatingDict(dict):
+ """a dictionary that stores ColumnElement objects as keys. incoming ColumnElement
+ keys are translated against those of an underling FromClause for all operations.
+ This way the columns from any Selectable that is derived from or underlying this
+ TranslatingDict's selectable can be used as keys."""
+ def __init__(self, selectable):
+ super(TranslatingDict, self).__init__()
+ self.selectable = selectable
+ def __translate_col(self, col):
+ ourcol = self.selectable.corresponding_column(col, keys_ok=False, raiseerr=False)
+ #if col is not ourcol:
+ # print "TD TRANSLATING ", col, "TO", ourcol
+ if ourcol is None:
+ return col
+ else:
+ return ourcol
+ def __getitem__(self, col):
+ return super(TranslatingDict, self).__getitem__(self.__translate_col(col))
+ def has_key(self, col):
+ return super(TranslatingDict, self).has_key(self.__translate_col(col))
+ def __setitem__(self, col, value):
+ return super(TranslatingDict, self).__setitem__(self.__translate_col(col), value)
+ def __contains__(self, col):
+ return self.has_key(col)
+ def setdefault(self, col, value):
+ return super(TranslatingDict, self).setdefault(self.__translate_col(col), value)
+
+class BinaryVisitor(sql.ClauseVisitor):
+ def __init__(self, func):
+ self.func = func
+ def visit_binary(self, binary):
+ self.func(binary)
+
def instance_str(instance):
"""return a string describing an instance"""
return instance.__class__.__name__ + "@" + hex(id(instance))
a2.email='a2@foo.com'
u2.addresses.append(a2)
sess.save(u2, entity_name='user2')
+ print u2.__dict__
sess.flush()
assert user1.select().execute().fetchall() == [(u1.user_id, u1.name)]
assert len(u1list) == len(u2list) == 1
assert u1list[0] is not u2list[0]
assert len(u1list[0].addresses) == len(u2list[0].addresses) == 1
+ # the lazy load requires that setup_loader() check that the correct LazyLoader
+ # is setting up for each load
assert isinstance(u1list[0].addresses[0], Address1)
assert isinstance(u2list[0].addresses[0], Address2)
-
+
+ def testpolymorphic_deferred(self):
+ """test that deferred columns load properly using entity names"""
+ class User(object):pass
+ u1mapper = mapper(User, user1, entity_name='user1', properties ={
+ 'name':deferred(user1.c.name)
+ }, extension=ctx.mapper_extension)
+ u2mapper =mapper(User, user2, entity_name='user2', properties={
+ 'name':deferred(user2.c.name)
+ }, extension=ctx.mapper_extension)
+
+ u1 = User(_sa_entity_name='user1')
+ u1.name = 'this is user 1'
+
+ u2 = User(_sa_entity_name='user2')
+ u2.name='this is user 2'
+
+ ctx.current.flush()
+ assert user1.select().execute().fetchall() == [(u1.user_id, u1.name)]
+ assert user2.select().execute().fetchall() == [(u2.user_id, u2.name)]
+
+ ctx.current.clear()
+ u1list = ctx.current.query(User, entity_name='user1').select()
+ u2list = ctx.current.query(User, entity_name='user2').select()
+ assert len(u1list) == len(u2list) == 1
+ assert u1list[0] is not u2list[0]
+ # the deferred column load requires that setup_loader() check that the correct DeferredColumnLoader
+ # is setting up for each load
+ assert u1list[0].name == 'this is user 1'
+ assert u2list[0].name == 'this is user 2'
+
+
if __name__ == "__main__":
testbase.main()
)
def testbidirectional(self):
- """tests a bi-directional many-to-many relationship."""
+ """tests a many-to-many backrefs"""
Place.mapper = mapper(Place, place)
Transition.mapper = mapper(Transition, transition, properties = dict(
inputs = relation(Place.mapper, place_output, lazy=True, backref='inputs'),
)
)
- Place.mapper.options()
- print Place.mapper.props['inputs']
- print Transition.mapper.props['inputs']
- return
-
- Place.eagermapper = Place.mapper.options(
- eagerload('inputs', selectalias='ip_alias'),
- eagerload('outputs', selectalias='op_alias')
- )
-
t1 = Transition('transition1')
t2 = Transition('transition2')
t3 = Transition('transition3')
# then select just from users. run it into instances.
# then assert the data, which will launch 3 more lazy loads
+ # (previous users in session fell out of scope and were removed from session's identity map)
def go():
r = users.select().execute()
l = usermapper.instances(r, sess)
# users, they will have no addresses or orders. the number of lazy loads when
# traversing the whole thing will be three for the addresses and three for the
# orders.
+ # (previous users in session fell out of scope and were removed from session's identity map)
usermapper = mapper(User, users,
properties = {
'addresses':relation(mapper(Address, addresses), lazy=False),
{'user_id' : 8, 'addresses' : (Address, [{'address_id' : 2, 'email_address':'ed@wood.com'}, {'address_id':3, 'email_address':'ed@bettyboop.com'}, {'address_id':4, 'email_address':'ed@lala.com'}])},
)
-
+ def testcircular(self):
+ """test that a circular eager relationship breaks the cycle with a lazy loader"""
+ m = mapper(User, users, properties = dict(
+ addresses = relation(mapper(Address, addresses), lazy=False, backref=backref('user', lazy=False))
+ ))
+ assert class_mapper(User).props['addresses'].lazy is False
+ assert class_mapper(Address).props['user'].lazy is False
+ session = create_session()
+ l = session.query(User).select()
+ self.assert_result(l, User, *user_address_result)
+
def testcompile(self):
"""tests deferred operation of a pre-compiled mapper statement"""
session = create_session()
m = mapper(Item, items, properties = dict(
keywords = relation(mapper(Keyword, keywords), itemkeywords, lazy=True, order_by=[keywords.c.keyword_id]),
))
- m2 = m.options(eagerload('keywords'))
- q = create_session().query(m2)
+ q = create_session().query(m).options(eagerload('keywords'))
def go():
l = q.select()
self.assert_result(l, Item, *item_keyword_result)