]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- added query.populate_existing().. - marks the query to reload
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 20 Jul 2007 20:02:41 +0000 (20:02 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 20 Jul 2007 20:02:41 +0000 (20:02 +0000)
  all attributes and collections of all instances touched in the query,
  including eagerly-loaded entities [ticket:660]

- added eagerload_all(), allows eagerload_all('x.y.z') to specify eager
  loading of all properties in the given path

CHANGES
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/sql.py
test/orm/fixtures.py
test/orm/mapper.py
test/orm/query.py

diff --git a/CHANGES b/CHANGES
index b9f8b8bf098c190e05d04cfc989900418c10f86f..ce23ba9e6342fc20fcbcb80bc3459a8d05470441 100644 (file)
--- a/CHANGES
+++ b/CHANGES
         - AttributeExtension moved to interfaces, and .delete is now
           .remove The event method signature has also been swapped around.
 
-    - major interface pare-down for Query:  all selectXXX methods
+    - major overhaul for Query:  all selectXXX methods
       are deprecated.  generative methods are now the standard
       way to do things, i.e. filter(), filter_by(), all(), one(),
       etc.  Deprecated methods are docstring'ed with their 
       new replacements.
+
+        - Class-level properties are now usable as query elements ...no 
+          more '.c.' !  "Class.c.propname" is now superceded by "Class.propname".
+          All clause operators are supported, as well as higher level operators
+          such as Class.prop==<some instance> for scalar attributes and 
+          Class.prop.contains(<some instance>) for collection-based attributes
+          (both are also negatable).  Table-based column expressions as well as 
+          columns mounted on mapped classes via 'c' are of course still fully available
+          and can be freely mixed with the new attributes.
+          [ticket:643]
+      
         - removed ancient query.select_by_attributename() capability.
+        
         - added "aliased joins" positional argument to the front of
           filter_by(). this allows auto-creation of joins that are aliased
           locally to the individual filter_by() call. This allows the
           querying divergent criteria. ClauseElements at the front of
           filter_by() are removed (use filter()).
 
+        - added query.populate_existing().. - marks the query to reload
+          all attributes and collections of all instances touched in the query,
+          including eagerly-loaded entities [ticket:660]
+
+        - added eagerload_all(), allows eagerload_all('x.y.z') to specify eager
+          loading of all properties in the given path
+          
     - Eager loading has been enhanced to allow even more joins in more places.
       It now functions at any arbitrary depth along self-referential 
       and cyclical structures.  When loading cyclical structures, specify "join_depth" 
       scheme now and are a lot easier on the eyes, as well as of course
       completely deterministic. [ticket:659]
 
-    - Class-level properties are now usable as query elements ...no 
-      more '.c.' !  "Class.c.propname" is now superceded by "Class.propname".
-      All clause operators are supported, as well as higher level operators
-      such as Class.prop==<some instance> for scalar attributes and 
-      Class.prop.contains(<some instance>) for collection-based attributes
-      (both are also negatable).  Table-based column expressions as well as 
-      columns mounted on mapped classes via 'c' are of course still fully available
-      and can be freely mixed with the new attributes.
-      [ticket:643]
       
     - added composite column properties. This allows you to create a 
       type which is represented by more than one column, when using the 
index 9e9615635c4ce2c5de601a8f7cc22ab2ed48d1e6..6a790def4000a9ac9cdcca48c82b0b60852fb571 100644 (file)
@@ -22,7 +22,7 @@ from sqlalchemy.orm.session import Session as create_session
 from sqlalchemy.orm.session import object_session, attribute_manager
 
 __all__ = ['relation', 'column_property', 'composite', 'backref', 'eagerload',
-           'lazyload', 'noload', 'deferred', 'defer', 'undefer',
+           'eagerload_all', 'lazyload', 'noload', 'deferred', 'defer', 'undefer',
            'undefer_group', 'extension', 'mapper', 'clear_mappers',
            'compile_mappers', 'class_mapper', 'object_mapper',
            'MapperExtension', 'Query', 'polymorphic_union', 'create_session',
@@ -153,6 +153,22 @@ def eagerload(name):
 
     return strategies.EagerLazyOption(name, lazy=False)
 
+def eagerload_all(name):
+    """Return a ``MapperOption`` that will convert all
+    properties along the given dot-separated path into an 
+    eager load.
+    
+    e.g::
+        query.options(eagerload_all('orders.items.keywords'))...
+        
+    will set all of 'orders', 'orders.items', and 'orders.items.keywords'
+    to load in one eager load.
+
+    Used with ``query.options()``.
+    """
+
+    return strategies.EagerLazyOption(name, lazy=False, chained=True)
+
 def lazyload(name):
     """Return a ``MapperOption`` that will convert the property of the
     given name into a lazy load.
index 4cad68e346528cbbf4f24c73db2076d31ebcda35..42ae5c8ddf50aac9f29180b849675807a8763827 100644 (file)
@@ -497,28 +497,30 @@ class PropertyOption(MapperOption):
     def __init__(self, key):
         self.key = key
 
-    def process_query_property(self, context, property):
+    def process_query_property(self, context, properties):
         pass
 
-    def process_selection_property(self, context, property):
+    def process_selection_property(self, context, properties):
         pass
 
     def process_query_context(self, context):
-        self.process_query_property(context, self._get_property(context))
+        self.process_query_property(context, self._get_properties(context))
 
     def process_selection_context(self, context):
-        self.process_selection_property(context, self._get_property(context))
+        self.process_selection_property(context, self._get_properties(context))
 
-    def _get_property(self, context):
+    def _get_properties(self, context):
         try:
-            prop = self.__prop
+            l = self.__prop
         except AttributeError:
+            l = []
             mapper = context.mapper
             for token in self.key.split('.'):
                 prop = mapper.get_property(token, resolve_synonyms=True)
+                l.append(prop)
                 mapper = getattr(prop, 'mapper', None)
-            self.__prop = prop
-        return prop
+            self.__prop = l
+        return l
 
 PropertyOption.logger = logging.class_logger(PropertyOption)
 
@@ -543,13 +545,24 @@ class StrategizedOption(PropertyOption):
     for an operation by a StrategizedProperty.
     """
 
-    def process_query_property(self, context, property):
+    def is_chained(self):
+        return False
+        
+    def process_query_property(self, context, properties):
         self.logger.debug("applying option to QueryContext, property key '%s'" % self.key)
-        context.attributes[("loaderstrategy", property)] = self.get_strategy_class()
+        if self.is_chained():
+            for prop in properties:
+                context.attributes[("loaderstrategy", prop)] = self.get_strategy_class()
+        else:
+            context.attributes[("loaderstrategy", properties[-1])] = self.get_strategy_class()
 
-    def process_selection_property(self, context, property):
+    def process_selection_property(self, context, properties):
         self.logger.debug("applying option to SelectionContext, property key '%s'" % self.key)
-        context.attributes[("loaderstrategy", property)] = self.get_strategy_class()
+        if self.is_chained():
+            for prop in properties:
+                context.attributes[("loaderstrategy", prop)] = self.get_strategy_class()
+        else:     
+            context.attributes[("loaderstrategy", properties[-1])] = self.get_strategy_class()
 
     def get_strategy_class(self):
         raise NotImplementedError()
index aeae60365ca09c90fb7595b32540f0f801336af1..6a9ddc57ac6fe337c84460d07415c98b3b03b9c8 100644 (file)
@@ -122,6 +122,22 @@ class Query(object):
         criterion = prop.compare(operator.eq, instance, value_is_parent=True)
         return Query(target, **kwargs).filter(criterion)
     query_from_parent = classmethod(query_from_parent)
+    
+    def populate_existing(self):
+        """return a Query that will refresh all instances loaded.
+        
+        this includes all entities accessed from the database, including
+        secondary entities, eagerly-loaded collection items.
+        
+        All changes present on entities which are already present in the session will 
+        be reset and the entities will all be marked "clean".
+        
+        This is essentially the en-masse version of load().
+        """
+        
+        q = self._clone()
+        q._populate_existing = True
+        return q
         
     def with_parent(self, instance, property=None):
         """add a join criterion corresponding to a relationship to the given parent instance.
index 473fe7729afae2f406f4a07c1fac0a59413177ac..0ceb96955938ea7f742d758601f40b6b23808ddb 100644 (file)
@@ -740,17 +740,22 @@ class EagerLoader(AbstractRelationLoader):
 EagerLoader.logger = logging.class_logger(EagerLoader)
 
 class EagerLazyOption(StrategizedOption):
-    def __init__(self, key, lazy=True):
+    def __init__(self, key, lazy=True, chained=False):
         super(EagerLazyOption, self).__init__(key)
         self.lazy = lazy
-
-    def process_query_property(self, context, prop):
+        self.chained = chained
+        
+    def is_chained(self):
+        return not self.lazy and self.chained
+        
+    def process_query_property(self, context, properties):
         if self.lazy:
-            if prop in context.eager_loaders:
-                context.eager_loaders.remove(prop)
+            if properties[-1] in context.eager_loaders:
+                context.eager_loaders.remove(properties[-1])
         else:
-            context.eager_loaders.add(prop)
-        super(EagerLazyOption, self).process_query_property(context, prop)
+            for prop in properties:
+                context.eager_loaders.add(prop)
+        super(EagerLazyOption, self).process_query_property(context, properties)
 
     def get_strategy_class(self):
         if self.lazy:
@@ -775,8 +780,8 @@ class FetchModeOption(PropertyOption):
             raise exceptions.ArgumentError("Fetchmode must be one of 'join' or 'select'")
         self.type = type
         
-    def process_selection_property(self, context, property):
-        context.attributes[('fetchmode', property)] = self.type
+    def process_selection_property(self, context, properties):
+        context.attributes[('fetchmode', properties[-1])] = self.type
         
 class RowDecorateOption(PropertyOption):
     def __init__(self, key, decorator=None, alias=None):
@@ -784,17 +789,17 @@ class RowDecorateOption(PropertyOption):
         self.decorator = decorator
         self.alias = alias
 
-    def process_selection_property(self, context, property):
+    def process_selection_property(self, context, properties):
         if self.alias is not None and self.decorator is None:
             if isinstance(self.alias, basestring):
-                self.alias = property.target.alias(self.alias)
+                self.alias = properties[-1].target.alias(self.alias)
             def decorate(row):
                 d = {}
-                for c in property.target.columns:
+                for c in properties[-1].target.columns:
                     d[c] = row[self.alias.corresponding_column(c)]
                 return d
             self.decorator = decorate
-        context.attributes[("eager_row_processor", property)] = self.decorator
+        context.attributes[("eager_row_processor", properties[-1])] = self.decorator
 
 RowDecorateOption.logger = logging.class_logger(RowDecorateOption)
         
index 7677aab9c7d67baef29b89978dccb7674f4b3f17..3ca5b0921bbdcbfbaf3448d2ae7bca83cde71841 100644 (file)
@@ -1311,6 +1311,8 @@ class _CompareMixin(ColumnOperators):
         obj = self._check_literal(obj)
 
         type_ = self._compare_type(obj)
+        
+        # TODO: generalize operator overloading like this out into the types module
         if op == operator.add and isinstance(type_, (sqltypes.Concatenable)):
             op = ColumnOperators.concat_op
         
index ddec0a28125bb60d347c27d720c24fc712623944..39b0da383f0eba4cafe8b8f6c3333476c4d74243 100644 (file)
@@ -36,7 +36,7 @@ class Base(object):
                         continue
                 else:
                     if value is not None:
-                        if value != getattr(other, attr):
+                        if value != getattr(other, attr, None):
                             return False
             else:
                 return True
index e6c03161c2cf2ad409b159671049d0b7a4c5dab1..b0542cfbe8127cade3c27082732192d74dad79d2 100644 (file)
@@ -55,13 +55,12 @@ class MapperTest(MapperSuperTest):
         assert not hasattr(u, 'user_name')
           
     def testrefresh(self):
-        mapper(User, users, properties={'addresses':relation(mapper(Address, addresses))})
+        mapper(User, users, properties={'addresses':relation(mapper(Address, addresses), backref='user')})
         s = create_session()
         u = s.get(User, 7)
         u.user_name = 'foo'
         a = Address()
-        import sqlalchemy.orm.session
-        assert sqlalchemy.orm.session.object_session(a) is None
+        assert object_session(a) is None
         u.addresses.append(a)
 
         self.assert_(a in u.addresses)
@@ -87,6 +86,7 @@ class MapperTest(MapperSuperTest):
         # get the attribute, it refreshes
         self.assert_(u.user_name == 'jack')
         self.assert_(a not in u.addresses)
+
     
     def testexpirecascade(self):
         mapper(User, users, properties={'addresses':relation(mapper(Address, addresses), cascade="all, refresh-expire")})
@@ -616,8 +616,8 @@ class MapperTest(MapperSuperTest):
         
         
         print "-------MARK----------"
-        # eagerload orders, orders.items, orders.items.keywords
-        q2 = sess.query(User).options(eagerload('orders'), eagerload('orders.items'), eagerload('orders.items.keywords'))
+        # eagerload orders.items.keywords; eagerload_all() implies eager load of orders, orders.items
+        q2 = sess.query(User).options(eagerload_all('orders.items.keywords'))
         u = q2.select()
         def go():
             print u[0].orders[1].items[0].keywords[1]
index 01815374da2779329ebdd2369d81eb49eb47f24d..7b809d74fb59c95306a8f6851a004f34e73ea210 100644 (file)
@@ -73,7 +73,6 @@ class QueryTest(testbase.ORMTest):
         })
         mapper(Keyword, keywords)
 
-            
 class GetTest(QueryTest):
     def test_get(self):
         s = create_session()
@@ -85,6 +84,33 @@ class GetTest(QueryTest):
         u2 = s.query(User).get(7)
         assert u is not u2
 
+    def test_load(self):
+        s = create_session()
+        
+        try:
+            assert s.query(User).load(19) is None
+            assert False
+        except exceptions.InvalidRequestError:
+            assert True
+            
+        u = s.query(User).load(7)
+        u2 = s.query(User).load(7)
+        assert u is u2
+        s.clear()
+        u2 = s.query(User).load(7)
+        assert u is not u2
+        
+        u2.name = 'some name'
+        a = Address(name='some other name')
+        u2.addresses.append(a)
+        assert u2 in s.dirty
+        assert a in u2.addresses
+        
+        s.query(User).load(7)
+        assert u2 not in s.dirty
+        assert u2.name =='jack'
+        assert a not in u2.addresses
+        
     def test_unicode(self):
         """test that Query.get properly sets up the type for the bind parameter.  using unicode would normally fail 
         on postgres, mysql and oracle unless it is converted to an encoded string"""
@@ -99,6 +125,38 @@ class GetTest(QueryTest):
         mapper(LocalFoo, table)
         assert create_session().query(LocalFoo).get(ustring) == LocalFoo(id=ustring, data=ustring)
 
+    def test_populate_existing(self):
+        s = create_session()
+
+        userlist = s.query(User).all()
+
+        u = userlist[0]
+        u.name = 'foo'
+        a = Address(name='ed')
+        u.addresses.append(a)
+
+        self.assert_(a in u.addresses)
+
+        s.query(User).populate_existing().all()
+
+        self.assert_(u not in s.dirty)
+
+        self.assert_(u.name == 'jack')
+
+        self.assert_(a not in u.addresses)
+
+        u.addresses[0].email_address = 'lala'
+        u.orders[1].items[2].description = 'item 12'
+        # test that lazy load doesnt change child items
+        s.query(User).populate_existing().all()
+        assert u.addresses[0].email_address == 'lala'
+        assert u.orders[1].items[2].description == 'item 12'
+        
+        # eager load does
+        s.query(User).options(eagerload('addresses'), eagerload_all('orders.items')).populate_existing().all()
+        assert u.addresses[0].email_address == 'jack@bean.com'
+        assert u.orders[1].items[2].description == 'item 5'
+        
 class OperatorTest(QueryTest):
     """test sql.Comparator implementation for MapperProperties"""