.. autofunction:: init_collection
+.. autofunction:: flag_modified
+
.. function:: instance_state
Return the :class:`InstanceState` for a given object.
--- /dev/null
+"""
+Illustrates how to build and use "mutable" types, such as dictionaries and
+user-defined classes, as scalar attributes which detect in-place changes.
+
+The example is based around the usage of the event model introduced in
+:ref:`event_toplevel`, along with the :func:`attributes.flag_modified` function
+which establishes the "dirty" flag on a particular mapped attribute. These
+functions are encapsulated in a mixin called ``TrackMutationsMixin``.
+Subclassing ``dict`` to provide "mutation tracking" looks like::
+
+ class MutationDict(TrackMutationsMixin, dict):
+ def __init__(self, other):
+ self.update(other)
+
+ def __setitem__(self, key, value):
+ dict.__setitem__(self, key, value)
+ self.on_change()
+
+ def __delitem__(self, key):
+ dict.__delitem__(self, key)
+ self.on_change()
+
+ Base = declarative_base()
+ class Foo(Base):
+ __tablename__ = 'foo'
+ id = Column(Integer, primary_key=True)
+ data = Column(JSONEncodedDict)
+
+ MutationDict.listen(Foo.data)
+
+"""
\ No newline at end of file
--- /dev/null
+from sqlalchemy.orm.attributes import flag_modified
+from sqlalchemy import event
+import weakref
+
+class TrackMutationsMixin(object):
+ """Mixin that defines transparent propagation of change
+ events to a parent object.
+
+ """
+ _key = None
+ _parent = None
+
+ def _set_parent(self, parent, key):
+ self._parent = weakref.ref(parent)
+ self._key = key
+
+ def _remove_parent(self):
+ del self._parent
+
+ def on_change(self, key=None):
+ """Subclasses should call this method whenever change events occur."""
+
+ if key is None:
+ key = self._key
+ if self._parent:
+ p = self._parent()
+ if p:
+ flag_modified(p, self._key)
+
+ @classmethod
+ def listen(cls, attribute):
+ """Establish this type as a mutation listener for the given class and
+ attribute name.
+
+ """
+ key = attribute.key
+ parent_cls = attribute.class_
+
+ def on_load(state):
+ val = state.dict.get(key, None)
+ if val is not None:
+ val = cls(val)
+ state.dict[key] = val
+ val._set_parent(state.obj(), key)
+
+ def on_set(target, value, oldvalue, initiator):
+ if not isinstance(value, cls):
+ value = cls(value)
+ value._set_parent(target.obj(), key)
+ if isinstance(oldvalue, cls):
+ oldvalue._remove_parent()
+ return value
+
+ event.listen(parent_cls, 'on_load', on_load, raw=True)
+ event.listen(parent_cls, 'on_refresh', on_load, raw=True)
+ event.listen(attribute, 'on_set', on_set, raw=True, retval=True)
+
+if __name__ == '__main__':
+ from sqlalchemy import Column, Integer, VARCHAR, create_engine
+ from sqlalchemy.orm import Session
+ from sqlalchemy.types import TypeDecorator
+ from sqlalchemy.ext.declarative import declarative_base
+ import simplejson
+
+ class JSONEncodedDict(TypeDecorator):
+ """Represents an immutable structure as a json-encoded string.
+
+ Usage::
+
+ JSONEncodedDict(255)
+
+ """
+
+ impl = VARCHAR
+
+ def process_bind_param(self, value, dialect):
+ if value is not None:
+ value = simplejson.dumps(value, use_decimal=True)
+
+ return value
+
+ def process_result_value(self, value, dialect):
+ if value is not None:
+ value = simplejson.loads(value, use_decimal=True)
+ return value
+
+ class MutationDict(TrackMutationsMixin, dict):
+ def __init__(self, other):
+ self.update(other)
+
+ def __setitem__(self, key, value):
+ dict.__setitem__(self, key, value)
+ self.on_change()
+
+ def __delitem__(self, key):
+ dict.__delitem__(self, key)
+ self.on_change()
+
+ Base = declarative_base()
+ class Foo(Base):
+ __tablename__ = 'foo'
+ id = Column(Integer, primary_key=True)
+ data = Column(JSONEncodedDict)
+
+ MutationDict.listen(Foo.data)
+
+ e = create_engine('sqlite://', echo=True)
+
+ Base.metadata.create_all(e)
+
+ sess = Session(e)
+ f1 = Foo(data={'a':'b'})
+ sess.add(f1)
+ sess.commit()
+
+ f1.data['a'] = 'c'
+ sess.commit()
+
+ assert f1.data == {'a':'c'}
+
+
\ No newline at end of file
from sqlalchemy.orm import synonym as _orm_synonym, mapper,\
comparable_property, class_mapper
from sqlalchemy.orm.interfaces import MapperProperty
-from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty
+from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty, CompositeProperty
from sqlalchemy.orm.util import _is_mapped_class
from sqlalchemy import util, exceptions
from sqlalchemy.sql import util as sql_util, expression
# extract columns from the class dict
cols = []
for key, c in our_stuff.iteritems():
- if isinstance(c, ColumnProperty):
+ if isinstance(c, (ColumnProperty, CompositeProperty)):
for col in c.columns:
if isinstance(col, Column) and col.table is None:
_undefer_column_name(key, col)
class Proxy(QueryableAttribute):
"""A combination of InsturmentedAttribute and a regular descriptor."""
- def __init__(self, key, descriptor, comparator, adapter=None):
+ def __init__(self, class_, key, descriptor, comparator, adapter=None):
+ self.class_ = class_
self.key = key
self.descriptor = descriptor
self._comparator = comparator
proxy_attr = attributes.\
create_proxied_attribute(self.descriptor or descriptor)\
(
+ self.parent.class_,
self.key,
self.descriptor or descriptor,
lambda: self._comparator_factory(mapper)
self.active_history = kwargs.get('active_history', False)
self.deferred = kwargs.get('deferred', False)
self.group = kwargs.get('group', None)
+ util.set_creation_order(self)
def fget(instance):
# this could be optimized to store the value in __dict__,
initialized, depending on what's present in the result rows.
"""
-
+
+ def on_refresh(self, target):
+ """Receive an object instance after one or more attributes have
+ been refreshed.
+
+ This hook is called after expired attributes have been reloaded.
+
+ """
+
def on_resurrect(self, target):
"""Receive an object instance as it is 'resurrected' from
garbage collection, which occurs when a "dirty" state falls
if loaded_instance:
state.manager.dispatch.on_load(state)
+ elif isnew:
+ state.manager.dispatch.on_refresh(state)
if result is not None:
if append_result:
'on_append_result',
'on_populate_instance',
'on_load',
+ 'on_refresh',
'on_before_insert',
'on_after_insert',
'on_before_update',
eq_(canary,
['on_init', 'on_before_insert',
'on_after_insert', 'on_translate_row', 'on_populate_instance',
+ 'on_refresh',
'on_append_result', 'on_translate_row', 'on_create_instance',
'on_populate_instance', 'on_load', 'on_append_result',
'on_before_update', 'on_after_update', 'on_before_delete', 'on_after_delete'])
sess.delete(am)
sess.flush()
eq_(canary1, ['on_init', 'on_before_insert', 'on_after_insert',
- 'on_translate_row', 'on_populate_instance',
+ 'on_translate_row', 'on_populate_instance','on_refresh',
'on_append_result', 'on_translate_row', 'on_create_instance'
, 'on_populate_instance', 'on_load', 'on_append_result',
'on_before_update', 'on_after_update', 'on_before_delete',
'on_after_delete'])
eq_(canary2, [])
eq_(canary3, ['on_init', 'on_before_insert', 'on_after_insert',
- 'on_translate_row', 'on_populate_instance',
+ 'on_translate_row', 'on_populate_instance','on_refresh',
'on_append_result', 'on_translate_row', 'on_create_instance'
, 'on_populate_instance', 'on_load', 'on_append_result',
'on_before_update', 'on_after_update', 'on_before_delete',