From 9ebac6151a743b5f841e715d04b551d8995e0e61 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 21 Dec 2005 03:44:46 +0000 Subject: [PATCH] added 'deferred' keyword, allowing deferred loading of a particular column --- examples/adjacencytree/basic_tree.py | 2 +- lib/sqlalchemy/attributes.py | 5 ++- lib/sqlalchemy/mapping/__init__.py | 9 +++- lib/sqlalchemy/mapping/mapper.py | 6 ++- lib/sqlalchemy/mapping/properties.py | 62 ++++++++++++++++++++++++++-- test/mapper.py | 20 +++++++++ test/testbase.py | 3 +- 7 files changed, 96 insertions(+), 11 deletions(-) diff --git a/examples/adjacencytree/basic_tree.py b/examples/adjacencytree/basic_tree.py index 39c5c24e45..0f1ff9b234 100644 --- a/examples/adjacencytree/basic_tree.py +++ b/examples/adjacencytree/basic_tree.py @@ -46,7 +46,7 @@ class TreeNode(object): # define the mapper. we will make "convenient" property # names vs. the more verbose names in the table definition -TreeNode.mapper=assignmapper(tables.trees, class_=TreeNode, properties=dict( +assign_mapper(TreeNode, tables.trees, properties=dict( id=tables.trees.c.node_id, name=tables.trees.c.node_name, parent_id=tables.trees.c.parent_node_id, diff --git a/lib/sqlalchemy/attributes.py b/lib/sqlalchemy/attributes.py index 912dfff991..ed5da32845 100644 --- a/lib/sqlalchemy/attributes.py +++ b/lib/sqlalchemy/attributes.py @@ -391,8 +391,9 @@ class AttributeManager(object): used to create the initial value. The definition for this attribute is wrapped up into a callable which is then stored in the classes' dictionary of "class managed" attributes. When instances of the class - are created and the attribute first referenced, the callable is invoked to - create the new history container. Extra keyword arguments can be sent which + are created and the attribute first referenced, the callable is invoked with + the new object instance as an argument to create the new history container. + Extra keyword arguments can be sent which will be passed along to newly created history containers.""" def createprop(obj): if callable_ is not None: diff --git a/lib/sqlalchemy/mapping/__init__.py b/lib/sqlalchemy/mapping/__init__.py index f640e27bb4..2cab0b0da8 100644 --- a/lib/sqlalchemy/mapping/__init__.py +++ b/lib/sqlalchemy/mapping/__init__.py @@ -28,7 +28,7 @@ from mapper import * from properties import * import mapper as mapperlib -__all__ = ['relation', 'eagerload', 'lazyload', 'noload', 'assignmapper', +__all__ = ['relation', 'eagerload', 'lazyload', 'noload', 'deferred', 'assignmapper', 'column', 'deferred', 'mapper', 'clear_mappers', 'objectstore', 'sql', 'extension', 'class_mapper', 'object_mapper', 'MapperExtension', 'ColumnProperty', 'assign_mapper' ] @@ -63,6 +63,13 @@ def _relation_mapper(class_, table=None, secondary=None, live=live, association=association, lazy=lazy, selectalias=selectalias, order_by=order_by, attributeext=attributeext) +def column(*columns): + return ColumnProperty(*columns) + +def deferred(*columns): + return DeferredColumnProperty(*columns) + + class assignmapper(object): """provides a property object that will instantiate a Mapper for a given class the first time it is called off of the object. This is useful for attaching a Mapper to a class diff --git a/lib/sqlalchemy/mapping/mapper.py b/lib/sqlalchemy/mapping/mapper.py index 69cde4d791..4519673742 100644 --- a/lib/sqlalchemy/mapping/mapper.py +++ b/lib/sqlalchemy/mapping/mapper.py @@ -556,13 +556,13 @@ class Mapper(object): 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([self.table], sql.and_(*crit), use_labels=True) + statement = sql.select([], sql.and_(*crit), from_obj=[self.table], use_labels=True) if kwargs.has_key('order_by'): statement.order_by(*kwargs['order_by']) else: statement.order_by(order_by) else: - statement = sql.select([self.table], whereclause, use_labels=True, **kwargs) + statement = sql.select([], whereclause, from_obj=[self.table], use_labels=True, **kwargs) if not kwargs.get('distinct', False) and order_by is not None and kwargs.get('order_by', None) is None: statement.order_by(order_by) # plugin point @@ -572,6 +572,7 @@ class Mapper(object): value.setup(key, statement, **kwargs) return statement + def _identity_key(self, row): return objectstore.get_row_key(row, self.class_, self.primarytable, self.pks_by_table[self.table]) @@ -662,6 +663,7 @@ class MapperProperty(object): def setup(self, key, statement, **options): """called when a statement is being constructed. """ return self + def init(self, key, parent): """called when the MapperProperty is first attached to a new parent Mapper.""" pass diff --git a/lib/sqlalchemy/mapping/properties.py b/lib/sqlalchemy/mapping/properties.py index 59cc85a5ff..f3862ddd9d 100644 --- a/lib/sqlalchemy/mapping/properties.py +++ b/lib/sqlalchemy/mapping/properties.py @@ -29,8 +29,9 @@ import random class ColumnProperty(MapperProperty): """describes an object attribute that corresponds to a table column.""" def __init__(self, *columns): - """the list of columns describes a single object property populating - multiple columns, typcially across multiple tables""" + """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.columns = list(columns) def getattr(self, object): @@ -43,6 +44,13 @@ class ColumnProperty(MapperProperty): def _copy(self): return ColumnProperty(*self.columns) + def setup(self, key, statement, eagertable=None, **options): + for c in self.columns: + if eagertable is not None: + statement.append_column(eagertable._get_col_by_original(c)) + else: + statement.append_column(c) + def init(self, key, parent): self.key = key # establish a SmartProperty property manager on the object for this key @@ -54,6 +62,52 @@ class ColumnProperty(MapperProperty): if isnew: instance.__dict__[self.key] = row[self.columns[0]] +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.isoption = kwargs.get('isoption', False) + ColumnProperty.__init__(self, *columns) + + def hash_key(self): + return "DeferredColumnProperty(%s)" % repr([hash_key(c) for c in self.columns]) + + def _copy(self): + return DeferredColumnProperty(*self.columns) + + def setup_loader(self, instance): + def lazyload(): + clause = sql.and_() + for primary_key in self.parent.pks_by_table[self.parent.primarytable]: + clause.clauses.append(primary_key == self.parent._getattrbycolumn(instance, primary_key)) + return sql.select([self.parent.table.c[self.key]], clause).scalar() + return lazyload + + 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.parent._is_primary_mapper and not self.isoption + + def setup(self, key, statement, **options): + pass + + def init(self, key, parent): + self.key = key + self.parent = parent + # establish a SmartProperty property manager on the object for this key, + # containing a callable to load in the attribute + if parent._is_primary_mapper(): + objectstore.uow().register_attribute(parent.class_, key, uselist=False, callable_=lambda i:self.setup_loader(i)) + + def execute(self, instance, row, identitykey, imap, isnew): + if isnew: + if not self._is_primary(): + objectstore.global_attributes.create_history(instance, self.key, False, callable_=self.setup_loader(instance)) + else: + objectstore.global_attributes.reset_history(instance, self.key) + mapper.ColumnProperty = ColumnProperty class PropertyLoader(MapperProperty): @@ -660,13 +714,13 @@ class EagerLoader(PropertyLoader): statement.order_by(*self.eager_order_by) statement.append_from(statement._outerjoin) - statement.append_column(self.eagertarget) + #statement.append_column(self.eagertarget) recursion_stack[self] = True try: for key, value in self.mapper.props.iteritems(): if recursion_stack.has_key(value): raise "Circular eager load relationship detected on " + str(self.mapper) + " " + key + repr(self.mapper.props) - value.setup(key, statement, recursion_stack=recursion_stack) + value.setup(key, statement, recursion_stack=recursion_stack, eagertable=self.eagertarget) finally: del recursion_stack[self] diff --git a/test/mapper.py b/test/mapper.py index 944082f5cc..51751de6f4 100644 --- a/test/mapper.py +++ b/test/mapper.py @@ -174,6 +174,26 @@ class PropertyTest(MapperSuperTest): objectstore.commit() self.echo(repr(AddressUser.mapper.select(AddressUser.c.user_name == 'jack'))) + +class DeferredTest(MapperSuperTest): + + def testbasic(self): + """tests a basic "deferred" load""" + + m = mapper(Order, orders, properties={ + 'description':deferred(orders.c.description) + }) + + def go(): + l = m.select() + o2 = l[2] + print o2.description + + self.assert_sql(db, go, [ + ("SELECT orders.order_id AS orders_order_id, orders.user_id AS orders_user_id, orders.isopen AS orders_isopen FROM orders ORDER BY orders.oid", {}), + ("SELECT orders.description FROM orders WHERE orders.order_id = :orders_order_id", {'orders_order_id':3}) + ]) + class LazyTest(MapperSuperTest): diff --git a/test/testbase.py b/test/testbase.py index 1d9586b535..ab32f803fe 100644 --- a/test/testbase.py +++ b/test/testbase.py @@ -99,6 +99,7 @@ class EngineAssert(object): def execute_compiled(self, compiled, parameters, **kwargs): self.engine.logger = self.logger statement = str(compiled) + statement = re.sub(r'\n', '', statement) if self.assert_list is not None: item = self.assert_list.pop() @@ -124,7 +125,7 @@ class EngineAssert(object): repl = None counter = 0 query = re.sub(r':([\w_]+)', repl, query) - + self.unittest.assert_(statement == query and params == parameters, "Testing for query '%s' params %s, received '%s' with params %s" % (query, repr(params), statement, repr(parameters))) return self.realexec(compiled, parameters, **kwargs) -- 2.47.2