]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- implemented [ticket:887], refresh readonly props upon save
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 21 Jun 2008 17:23:14 +0000 (17:23 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 21 Jun 2008 17:23:14 +0000 (17:23 +0000)
- moved up "eager_defaults" active refresh step (this is an option used by just one user pretty much)
to be per-instance instead of per-table
- fixed table defs from previous deferred attributes enhancement
- CompositeColumnLoader equality comparison fixed for a/b == None; I suspect the composite capability in SA
needs a lot more work than this

CHANGES
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/strategies.py
test/orm/mapper.py
test/orm/unitofwork.py

diff --git a/CHANGES b/CHANGES
index 992138a31295e47f6e1622dd623222e630074632..3edfce6738e92ed73d2fef0d2161ce5a73fd6309 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -5,9 +5,17 @@ CHANGES
 =======
 0.5beta2
 ========
+- orm
     - In addition to expired attributes, deferred attributes
-      also load if their data is present in the result set
+      also load if their data is present in the result set.
       [ticket:870]
+      
+    - column_property() attributes which represent SQL expressions
+      or columns that are not present in the mapped tables
+      (such as those from views) are automatically expired
+      after an INSERT or UPDATE, assuming they have not been
+      locally modified, so that they are refreshed with the
+      most recent data upon access.  [ticket:887]
 
 0.5beta1
 ========
index da471b4d1b2802f8045eaf8bbcb6ff4fc392169b..99e93df5d53be7c5de4bd5eec575e53fad22c7f2 100644 (file)
@@ -138,7 +138,7 @@ class Mapper(object):
         self._clause_adapter = None
         self._requires_row_aliasing = False
         self.__inherits_equated_pairs = None
-
+        
         if not issubclass(class_, object):
             raise sa_exc.ArgumentError("Class '%s' is not a new-style class" % class_.__name__)
 
@@ -521,7 +521,15 @@ class Mapper(object):
                 # ordering is important since it determines the ordering of mapper.primary_key (and therefore query.get())
                 self._pks_by_table[t] = util.OrderedSet(t.primary_key).intersection(pk_cols)
             self._cols_by_table[t] = util.OrderedSet(t.c).intersection(all_cols)
-
+        
+        # determine cols that aren't expressed within our tables; 
+        # mark these as "read only" properties which are refreshed upon 
+        # INSERT/UPDATE
+        self._readonly_props = util.Set([
+            self._columntoproperty[col] for col in all_cols if 
+                not hasattr(col, 'table') or col.table not in self._cols_by_table
+        ])
+            
         # if explicit PK argument sent, add those columns to the primary key mappings
         if self.primary_key_argument:
             for k in self.primary_key_argument:
@@ -720,6 +728,13 @@ class Mapper(object):
             # columns (included in zblog tests)
             if col is None:
                 col = prop.columns[0]
+                
+                # column is coming in after _readonly_props was initialized; check
+                # for 'readonly'
+                if hasattr(self, '_readonly_props') and \
+                    (not hasattr(col, 'table') or col.table not in self._cols_by_table):
+                        self._readonly_props.add(prop)
+                    
             else:
                 # if column is coming in after _cols_by_table was initialized, ensure the col is in the
                 # right set
@@ -1169,10 +1184,27 @@ class Mapper(object):
 
                     # testlib.pragma exempt:__hash__
                     inserted_objects.add((state, connection))
-
+        
         if not postupdate:
-            # call after_XXX extensions
             for state, mapper, connection, has_identity in tups:
+                
+                # expire readonly attributes
+                readonly = state.unmodified.intersection([
+                    p.key for p in chain(*[m._readonly_props for m in mapper.iterate_to_root()])
+                ])
+                
+                if readonly:
+                    _expire_state(state, readonly)
+
+                # if specified, eagerly refresh whatever has
+                # been expired.
+                if self.eager_defaults and state.unloaded:
+                    state.key = self._identity_key_from_state(state)
+                    uowtransaction.session.query(self)._get(
+                        state.key, refresh_state=state,
+                        only_load_props=state.unloaded)
+                
+                # call after_XXX extensions
                 if not has_identity:
                     if 'after_insert' in mapper.extension.methods:
                         mapper.extension.after_insert(mapper, connection, state.obj())
@@ -1184,12 +1216,13 @@ class Mapper(object):
         sync.populate(state, self, state, self, self.__inherits_equated_pairs)
 
     def __postfetch(self, uowtransaction, connection, table, state, resultproxy, params, value_params):
-        """After an ``INSERT`` or ``UPDATE``, assemble newly generated
-        values on an instance.  For columns which are marked as being generated
-        on the database side, set up a group-based "deferred" loader
-        which will populate those attributes in one query when next accessed.
+        """For a given Table that has just been inserted/updated,
+        mark as 'expired' those attributes which correspond to columns
+        that are marked as 'postfetch', and populate attributes which 
+        correspond to columns marked as 'prefetch' or were otherwise generated
+        within _save_obj().
+        
         """
-
         postfetch_cols = resultproxy.postfetch_cols()
         generated_cols = list(resultproxy.prefetch_cols())
 
@@ -1197,6 +1230,7 @@ class Mapper(object):
             po = table.corresponding_column(self.polymorphic_on)
             if po:
                 generated_cols.append(po)
+
         if self.version_id_col:
             generated_cols.append(self.version_id_col)
 
@@ -1205,15 +1239,9 @@ class Mapper(object):
                 self._set_state_attr_by_column(state, c, params[c.key])
 
         deferred_props = [prop.key for prop in [self._columntoproperty[c] for c in postfetch_cols]]
-
+        
         if deferred_props:
-            if self.eager_defaults:
-                state.key = self._identity_key_from_state(state)
-                uowtransaction.session.query(self)._get(
-                    state.key, refresh_state=state,
-                    only_load_props=deferred_props)
-            else:
-                _expire_state(state, deferred_props)
+            _expire_state(state, deferred_props)
 
     def _delete_obj(self, states, uowtransaction):
         """Issue ``DELETE`` statements for a list of objects.
index 829210205ed805009d7e3798a017e6748dbb86e9..66e9ccd973d9a65b3b2384394ccc115c4715feff 100644 (file)
@@ -96,6 +96,9 @@ class CompositeColumnLoader(ColumnLoader):
             return self.parent_property.composite_class(*obj.__composite_values__())
             
         def compare(a, b):
+            if a is None or b is None:
+                return a is b
+                
             for col, aprop, bprop in zip(self.columns,
                                          a.__composite_values__(),
                                          b.__composite_values__()):
index 31920cdf738254ab70f18f6ac094ac1f8ef2d8a5..7d0014002218b4780c3e1ca4f7bc045e4a42bffe 100644 (file)
@@ -1262,12 +1262,12 @@ class DeferredPopulationTest(_base.MappedTest):
     def define_tables(self, metadata):
         Table("thing", metadata,
             Column("id", Integer, primary_key=True),
-            Column("name", String))
+            Column("name", String(20)))
 
         Table("human", metadata,
             Column("id", Integer, primary_key=True),
             Column("thing_id", Integer, ForeignKey("thing.id")),
-            Column("name", String))
+            Column("name", String(20)))
 
     @testing.resolve_artifact_names
     def setup_mappers(self):
index 5149d2739858f0da67024bdb94d58d9fb469951d..f0ab709afa0d5da2a8d4aba4d47cc8980ef6d734 100644 (file)
@@ -7,8 +7,8 @@ import operator
 from sqlalchemy.orm import mapper as orm_mapper
 
 from testlib import engines, sa, testing
-from testlib.sa import Table, Column, Integer, String, ForeignKey
-from testlib.sa.orm import mapper, relation, create_session
+from testlib.sa import Table, Column, Integer, String, ForeignKey, literal_column
+from testlib.sa.orm import mapper, relation, create_session, column_property
 from testlib.testing import eq_, ne_
 from testlib.compat import set
 from orm import _base, _fixtures
@@ -985,7 +985,50 @@ class DefaultTest(_base.MappedTest):
                     Secondary(data='s1'),
                     Secondary(data='s2')]))
 
+class ColumnPropertyTest(_base.MappedTest):
+    def define_tables(self, metadata):
+        Table('data', metadata, 
+            Column('id', Integer, primary_key=True),
+            Column('a', String(50)),
+            Column('b', String(50))
+            )
+            
+    def setup_mappers(self):
+        class Data(_base.BasicEntity):
+            pass
+        
+    @testing.resolve_artifact_names
+    def test_refreshes(self):
+        mapper(Data, data, properties={
+            'aplusb':column_property(data.c.a + literal_column("' '") + data.c.b)
+        })
+        self._test()
 
+    @testing.resolve_artifact_names
+    def test_refreshes_post_init(self):
+        m = mapper(Data, data)
+        m.add_property('aplusb', column_property(data.c.a + literal_column("' '") + data.c.b))
+        self._test()
+        
+    @testing.resolve_artifact_names
+    def _test(self):
+        sess = create_session()
+        
+        d1 = Data(a="hello", b="there")
+        sess.add(d1)
+        sess.flush()
+        
+        self.assertEquals(d1.aplusb, "hello there")
+        
+        d1.b = "bye"
+        sess.flush()
+        self.assertEquals(d1.aplusb, "hello bye")
+        
+        d1.b = 'foobar'
+        d1.aplusb = 'im setting this explicitly'
+        sess.flush()
+        self.assertEquals(d1.aplusb, "im setting this explicitly")
+    
 class OneToManyTest(_fixtures.FixtureTest):
     run_inserts = None