]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
added 'deferred' keyword, allowing deferred loading of a particular column
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 21 Dec 2005 03:44:46 +0000 (03:44 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 21 Dec 2005 03:44:46 +0000 (03:44 +0000)
examples/adjacencytree/basic_tree.py
lib/sqlalchemy/attributes.py
lib/sqlalchemy/mapping/__init__.py
lib/sqlalchemy/mapping/mapper.py
lib/sqlalchemy/mapping/properties.py
test/mapper.py
test/testbase.py

index 39c5c24e4588ec84ef8b86e0541b0a39c1339a3d..0f1ff9b234070bda2760b1733991f09eb7767f8d 100644 (file)
@@ -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,
index 912dfff99192c7a50d6df61ca747fdacfa228afa..ed5da32845ae6953aa69d115406d9f8713aa84ce 100644 (file)
@@ -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: 
index f640e27bb4b7f1405b353dd69b3accd526df3412..2cab0b0da8023c7f9cda2a57ba89d462b89e92e1 100644 (file)
@@ -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
index 69cde4d7919f5a3ad366eddbfff5cf49f6c27605..45196737420bf49f9cbb1fdd7858d9160a68d359 100644 (file)
@@ -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
index 59cc85a5ffd5f4a68dc1c7ff4c63f4e5441455d9..f3862ddd9db3335be8bdb7b327285cbb8d141d44 100644 (file)
@@ -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]
             
index 944082f5cc9dbad359673cbb131d3a3bddfdf561..51751de6f4545d2f1b260b4ef0b942c1f4cff567 100644 (file)
@@ -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):
 
index 1d9586b53581e0edd5cee7737c4c61eb36ca39cd..ab32f803feeb7fc6a9791d923f119fa30285ed82 100644 (file)
@@ -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)