From: Mike Bayer Date: Sat, 21 Jun 2008 17:23:14 +0000 (+0000) Subject: - implemented [ticket:887], refresh readonly props upon save X-Git-Tag: rel_0_5beta2~51 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c5e2d673a996a09e8cd399ebdde855773745b865;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - implemented [ticket:887], refresh readonly props upon save - 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 --- diff --git a/CHANGES b/CHANGES index 992138a312..3edfce6738 100644 --- 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 ======== diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index da471b4d1b..99e93df5d5 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -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. diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 829210205e..66e9ccd973 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -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__()): diff --git a/test/orm/mapper.py b/test/orm/mapper.py index 31920cdf73..7d00140022 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -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): diff --git a/test/orm/unitofwork.py b/test/orm/unitofwork.py index 5149d27398..f0ab709afa 100644 --- a/test/orm/unitofwork.py +++ b/test/orm/unitofwork.py @@ -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