- 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
This commit is contained in:
Mike Bayer
2008-06-21 17:23:14 +00:00
parent 060c3ce33c
commit c5e2d673a9
5 changed files with 104 additions and 22 deletions
+9 -1
View File
@@ -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
========
+45 -17
View 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.
+3
View 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__()):
+2 -2
View 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):
+45 -2
View 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