mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-06-04 23:06:24 -04:00
- 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:
@@ -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
|
||||
========
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user