PageRenderTime 36ms CodeModel.GetById 17ms app.highlight 15ms RepoModel.GetById 0ms app.codeStats 0ms

/SQLAlchemy-0.7.8/lib/sqlalchemy/ext/mutable.py

#
Python | 564 lines | 474 code | 68 blank | 22 comment | 36 complexity | 6caeab47d56ca0f95795f66bd8b7fdb7 MD5 | raw file
  1# ext/mutable.py
  2# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
  3#
  4# This module is part of SQLAlchemy and is released under
  5# the MIT License: http://www.opensource.org/licenses/mit-license.php
  6
  7"""Provide support for tracking of in-place changes to scalar values,
  8which are propagated into ORM change events on owning parent objects.
  9
 10The :mod:`sqlalchemy.ext.mutable` extension replaces SQLAlchemy's legacy approach to in-place
 11mutations of scalar values, established by the :class:`.types.MutableType`
 12class as well as the ``mutable=True`` type flag, with a system that allows
 13change events to be propagated from the value to the owning parent, thereby
 14removing the need for the ORM to maintain copies of values as well as the very
 15expensive requirement of scanning through all "mutable" values on each flush
 16call, looking for changes.
 17
 18.. _mutable_scalars:
 19
 20Establishing Mutability on Scalar Column Values
 21===============================================
 22
 23A typical example of a "mutable" structure is a Python dictionary.
 24Following the example introduced in :ref:`types_toplevel`, we 
 25begin with a custom type that marshals Python dictionaries into 
 26JSON strings before being persisted::
 27
 28    from sqlalchemy.types import TypeDecorator, VARCHAR
 29    import json
 30
 31    class JSONEncodedDict(TypeDecorator):
 32        "Represents an immutable structure as a json-encoded string."
 33
 34        impl = VARCHAR
 35
 36        def process_bind_param(self, value, dialect):
 37            if value is not None:
 38                value = json.dumps(value)
 39            return value
 40
 41        def process_result_value(self, value, dialect):
 42            if value is not None:
 43                value = json.loads(value)
 44            return value
 45
 46The usage of ``json`` is only for the purposes of example. The :mod:`sqlalchemy.ext.mutable` 
 47extension can be used
 48with any type whose target Python type may be mutable, including
 49:class:`.PickleType`, :class:`.postgresql.ARRAY`, etc.
 50
 51When using the :mod:`sqlalchemy.ext.mutable` extension, the value itself
 52tracks all parents which reference it.  Here we will replace the usage
 53of plain Python dictionaries with a dict subclass that implements
 54the :class:`.Mutable` mixin::
 55
 56    import collections
 57    from sqlalchemy.ext.mutable import Mutable
 58
 59    class MutationDict(Mutable, dict):
 60        @classmethod
 61        def coerce(cls, key, value):
 62            "Convert plain dictionaries to MutationDict."
 63
 64            if not isinstance(value, MutationDict):
 65                if isinstance(value, dict):
 66                    return MutationDict(value)
 67
 68                # this call will raise ValueError
 69                return Mutable.coerce(key, value)
 70            else:
 71                return value
 72
 73        def __setitem__(self, key, value):
 74            "Detect dictionary set events and emit change events."
 75
 76            dict.__setitem__(self, key, value)
 77            self.changed()
 78
 79        def __delitem__(self, key):
 80            "Detect dictionary del events and emit change events."
 81
 82            dict.__delitem__(self, key)
 83            self.changed()
 84
 85The above dictionary class takes the approach of subclassing the Python
 86built-in ``dict`` to produce a dict
 87subclass which routes all mutation events through ``__setitem__``. There are
 88many variants on this approach, such as subclassing ``UserDict.UserDict``,
 89the newer ``collections.MutableMapping``,  etc. The part that's important to this 
 90example is that the :meth:`.Mutable.changed` method is called whenever an in-place change to the
 91datastructure takes place.
 92
 93We also redefine the :meth:`.Mutable.coerce` method which will be used to
 94convert any values that are not instances of ``MutationDict``, such
 95as the plain dictionaries returned by the ``json`` module, into the
 96appropriate type.  Defining this method is optional; we could just as well created our
 97``JSONEncodedDict`` such that it always returns an instance of ``MutationDict``,
 98and additionally ensured that all calling code uses ``MutationDict`` 
 99explicitly.  When :meth:`.Mutable.coerce` is not overridden, any values
100applied to a parent object which are not instances of the mutable type
101will raise a ``ValueError``.
102
103Our new ``MutationDict`` type offers a class method
104:meth:`~.Mutable.as_mutable` which we can use within column metadata
105to associate with types. This method grabs the given type object or
106class and associates a listener that will detect all future mappings
107of this type, applying event listening instrumentation to the mapped
108attribute. Such as, with classical table metadata::
109
110    from sqlalchemy import Table, Column, Integer
111    
112    my_data = Table('my_data', metadata,
113        Column('id', Integer, primary_key=True),
114        Column('data', MutationDict.as_mutable(JSONEncodedDict))
115    )
116
117Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict``
118(if the type object was not an instance already), which will intercept any 
119attributes which are mapped against this type.  Below we establish a simple
120mapping against the ``my_data`` table::
121
122    from sqlalchemy import mapper
123
124    class MyDataClass(object):
125        pass
126
127    # associates mutation listeners with MyDataClass.data
128    mapper(MyDataClass, my_data)
129
130The ``MyDataClass.data`` member will now be notified of in place changes
131to its value.
132
133There's no difference in usage when using declarative::
134
135    from sqlalchemy.ext.declarative import declarative_base
136
137    Base = declarative_base()
138
139    class MyDataClass(Base):
140        __tablename__ = 'my_data'
141        id = Column(Integer, primary_key=True)
142        data = Column(MutationDict.as_mutable(JSONEncodedDict))
143
144Any in-place changes to the ``MyDataClass.data`` member
145will flag the attribute as "dirty" on the parent object::
146
147    >>> from sqlalchemy.orm import Session
148
149    >>> sess = Session()
150    >>> m1 = MyDataClass(data={'value1':'foo'})
151    >>> sess.add(m1)
152    >>> sess.commit()
153
154    >>> m1.data['value1'] = 'bar'
155    >>> assert m1 in sess.dirty
156    True
157
158The ``MutationDict`` can be associated with all future instances
159of ``JSONEncodedDict`` in one step, using :meth:`~.Mutable.associate_with`.  This
160is similar to :meth:`~.Mutable.as_mutable` except it will intercept 
161all occurrences of ``MutationDict`` in all mappings unconditionally, without
162the need to declare it individually::
163
164    MutationDict.associate_with(JSONEncodedDict)
165
166    class MyDataClass(Base):
167        __tablename__ = 'my_data'
168        id = Column(Integer, primary_key=True)
169        data = Column(JSONEncodedDict)
170    
171    
172Supporting Pickling
173--------------------
174
175The key to the :mod:`sqlalchemy.ext.mutable` extension relies upon the
176placement of a ``weakref.WeakKeyDictionary`` upon the value object, which
177stores a mapping of parent mapped objects keyed to the attribute name under
178which they are associated with this value. ``WeakKeyDictionary`` objects are
179not picklable, due to the fact that they contain weakrefs and function
180callbacks. In our case, this is a good thing, since if this dictionary were
181picklable, it could lead to an excessively large pickle size for our value
182objects that are pickled by themselves outside of the context of the parent.
183The developer responsibility here is only to provide a ``__getstate__`` method
184that excludes the :meth:`~.MutableBase._parents` collection from the pickle
185stream::
186
187    class MyMutableType(Mutable):
188        def __getstate__(self):
189            d = self.__dict__.copy()
190            d.pop('_parents', None)
191            return d
192
193With our dictionary example, we need to return the contents of the dict itself
194(and also restore them on __setstate__)::
195
196    class MutationDict(Mutable, dict):
197        # ....
198
199        def __getstate__(self):
200            return dict(self)
201
202        def __setstate__(self, state):
203            self.update(state)
204
205In the case that our mutable value object is pickled as it is attached to one
206or more parent objects that are also part of the pickle, the :class:`.Mutable`
207mixin will re-establish the :attr:`.Mutable._parents` collection on each value
208object as the owning parents themselves are unpickled.
209
210.. _mutable_composites:
211
212Establishing Mutability on Composites
213=====================================
214
215Composites are a special ORM feature which allow a single scalar attribute to
216be assigned an object value which represents information "composed" from one
217or more columns from the underlying mapped table. The usual example is that of
218a geometric "point", and is introduced in :ref:`mapper_composite`.
219
220.. versionchanged:: 0.7
221    The internals of :func:`.orm.composite` have been
222    greatly simplified and in-place mutation detection is no longer enabled by
223    default; instead, the user-defined value must detect changes on its own and
224    propagate them to all owning parents. The :mod:`sqlalchemy.ext.mutable`
225    extension provides the helper class :class:`.MutableComposite`, which is a
226    slight variant on the :class:`.Mutable` class.
227
228As is the case with :class:`.Mutable`, the user-defined composite class
229subclasses :class:`.MutableComposite` as a mixin, and detects and delivers
230change events to its parents via the :meth:`.MutableComposite.changed` method.
231In the case of a composite class, the detection is usually via the usage of
232Python descriptors (i.e. ``@property``), or alternatively via the special
233Python method ``__setattr__()``. Below we expand upon the ``Point`` class
234introduced in :ref:`mapper_composite` to subclass :class:`.MutableComposite`
235and to also route attribute set events via ``__setattr__`` to the
236:meth:`.MutableComposite.changed` method::
237
238    from sqlalchemy.ext.mutable import MutableComposite
239
240    class Point(MutableComposite):
241        def __init__(self, x, y):
242            self.x = x
243            self.y = y
244
245        def __setattr__(self, key, value):
246            "Intercept set events"
247
248            # set the attribute
249            object.__setattr__(self, key, value)
250
251            # alert all parents to the change
252            self.changed()
253
254        def __composite_values__(self):
255            return self.x, self.y
256
257        def __eq__(self, other):
258            return isinstance(other, Point) and \\
259                other.x == self.x and \\
260                other.y == self.y
261
262        def __ne__(self, other):
263            return not self.__eq__(other)
264
265The :class:`.MutableComposite` class uses a Python metaclass to automatically
266establish listeners for any usage of :func:`.orm.composite` that specifies our
267``Point`` type. Below, when ``Point`` is mapped to the ``Vertex`` class,
268listeners are established which will route change events from ``Point``
269objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes::
270
271    from sqlalchemy.orm import composite, mapper
272    from sqlalchemy import Table, Column
273
274    vertices = Table('vertices', metadata,
275        Column('id', Integer, primary_key=True),
276        Column('x1', Integer),
277        Column('y1', Integer),
278        Column('x2', Integer),
279        Column('y2', Integer),
280        )
281
282    class Vertex(object):
283        pass
284
285    mapper(Vertex, vertices, properties={
286        'start': composite(Point, vertices.c.x1, vertices.c.y1),
287        'end': composite(Point, vertices.c.x2, vertices.c.y2)
288    })
289
290Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members
291will flag the attribute as "dirty" on the parent object::
292
293    >>> from sqlalchemy.orm import Session
294
295    >>> sess = Session()
296    >>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15))
297    >>> sess.add(v1)
298    >>> sess.commit()
299
300    >>> v1.end.x = 8
301    >>> assert v1 in sess.dirty
302    True
303
304Supporting Pickling
305--------------------
306
307As is the case with :class:`.Mutable`, the :class:`.MutableComposite` helper
308class uses a ``weakref.WeakKeyDictionary`` available via the
309:meth:`.MutableBase._parents` attribute which isn't picklable. If we need to
310pickle instances of ``Point`` or its owning class ``Vertex``, we at least need
311to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary.
312Below we define both a ``__getstate__`` and a ``__setstate__`` that package up
313the minimal form of our ``Point`` class::
314
315    class Point(MutableComposite):
316        # ...
317        
318        def __getstate__(self):
319            return self.x, self.y
320        
321        def __setstate__(self, state):
322            self.x, self.y = state
323
324As with :class:`.Mutable`, the :class:`.MutableComposite` augments the
325pickling process of the parent's object-relational state so that the
326:meth:`.MutableBase._parents` collection is restored to all ``Point`` objects.
327
328"""
329from sqlalchemy.orm.attributes import flag_modified
330from sqlalchemy import event, types
331from sqlalchemy.orm import mapper, object_mapper
332from sqlalchemy.util import memoized_property
333import weakref
334
335class MutableBase(object):
336    """Common base class to :class:`.Mutable` and :class:`.MutableComposite`."""
337
338    @memoized_property
339    def _parents(self):
340        """Dictionary of parent object->attribute name on the parent.
341        
342        This attribute is a so-called "memoized" property.  It initializes
343        itself with a new ``weakref.WeakKeyDictionary`` the first time
344        it is accessed, returning the same object upon subsequent access.
345        
346        """
347
348        return weakref.WeakKeyDictionary()
349
350    @classmethod
351    def coerce(cls, key, value):
352        """Given a value, coerce it into this type.
353
354        By default raises ValueError.
355        """
356        if value is None:
357            return None
358        raise ValueError("Attribute '%s' does not accept objects of type %s" % (key, type(value)))
359
360    @classmethod
361    def _listen_on_attribute(cls, attribute, coerce, parent_cls):
362        """Establish this type as a mutation listener for the given 
363        mapped descriptor.
364
365        """
366        key = attribute.key
367        if parent_cls is not attribute.class_:
368            return
369
370        # rely on "propagate" here
371        parent_cls = attribute.class_
372
373        def load(state, *args):
374            """Listen for objects loaded or refreshed.
375
376            Wrap the target data member's value with 
377            ``Mutable``.
378
379            """
380            val = state.dict.get(key, None)
381            if val is not None:
382                if coerce:
383                    val = cls.coerce(key, val)
384                    state.dict[key] = val
385                val._parents[state.obj()] = key
386
387        def set(target, value, oldvalue, initiator):
388            """Listen for set/replace events on the target
389            data member.
390
391            Establish a weak reference to the parent object
392            on the incoming value, remove it for the one 
393            outgoing.
394
395            """
396            if not isinstance(value, cls):
397                value = cls.coerce(key, value)
398            if value is not None:
399                value._parents[target.obj()] = key
400            if isinstance(oldvalue, cls):
401                oldvalue._parents.pop(target.obj(), None)
402            return value
403
404        def pickle(state, state_dict):
405            val = state.dict.get(key, None)
406            if val is not None:
407                if 'ext.mutable.values' not in state_dict:
408                    state_dict['ext.mutable.values'] = []
409                state_dict['ext.mutable.values'].append(val)
410
411        def unpickle(state, state_dict):
412            if 'ext.mutable.values' in state_dict:
413                for val in state_dict['ext.mutable.values']:
414                    val._parents[state.obj()] = key
415
416
417        event.listen(parent_cls, 'load', load, raw=True, propagate=True)
418        event.listen(parent_cls, 'refresh', load, raw=True, propagate=True)
419        event.listen(attribute, 'set', set, raw=True, retval=True, propagate=True)
420        event.listen(parent_cls, 'pickle', pickle, raw=True, propagate=True)
421        event.listen(parent_cls, 'unpickle', unpickle, raw=True, propagate=True)
422
423class Mutable(MutableBase):
424    """Mixin that defines transparent propagation of change
425    events to a parent object.
426
427    See the example in :ref:`mutable_scalars` for usage information.
428
429    """
430
431    def changed(self):
432        """Subclasses should call this method whenever change events occur."""
433
434        for parent, key in self._parents.items():
435            flag_modified(parent, key)
436
437    @classmethod
438    def associate_with_attribute(cls, attribute):
439        """Establish this type as a mutation listener for the given 
440        mapped descriptor.
441
442        """
443        cls._listen_on_attribute(attribute, True, attribute.class_)
444
445    @classmethod
446    def associate_with(cls, sqltype):
447        """Associate this wrapper with all future mapped columns 
448        of the given type.
449
450        This is a convenience method that calls ``associate_with_attribute`` automatically.
451
452        .. warning:: 
453        
454           The listeners established by this method are *global*
455           to all mappers, and are *not* garbage collected.   Only use 
456           :meth:`.associate_with` for types that are permanent to an application,
457           not with ad-hoc types else this will cause unbounded growth
458           in memory usage.
459
460        """
461
462        def listen_for_type(mapper, class_):
463            for prop in mapper.iterate_properties:
464                if hasattr(prop, 'columns'):
465                    if isinstance(prop.columns[0].type, sqltype):
466                        cls.associate_with_attribute(getattr(class_, prop.key))
467
468        event.listen(mapper, 'mapper_configured', listen_for_type)
469
470    @classmethod
471    def as_mutable(cls, sqltype):
472        """Associate a SQL type with this mutable Python type.
473
474        This establishes listeners that will detect ORM mappings against
475        the given type, adding mutation event trackers to those mappings.
476
477        The type is returned, unconditionally as an instance, so that 
478        :meth:`.as_mutable` can be used inline::
479
480            Table('mytable', metadata,
481                Column('id', Integer, primary_key=True),
482                Column('data', MyMutableType.as_mutable(PickleType))
483            )
484
485        Note that the returned type is always an instance, even if a class
486        is given, and that only columns which are declared specifically with that
487        type instance receive additional instrumentation.
488
489        To associate a particular mutable type with all occurrences of a 
490        particular type, use the :meth:`.Mutable.associate_with` classmethod
491        of the particular :meth:`.Mutable` subclass to establish a global
492        association.
493
494        .. warning:: 
495        
496           The listeners established by this method are *global*
497           to all mappers, and are *not* garbage collected.   Only use 
498           :meth:`.as_mutable` for types that are permanent to an application,
499           not with ad-hoc types else this will cause unbounded growth
500           in memory usage.
501
502        """
503        sqltype = types.to_instance(sqltype)
504
505        def listen_for_type(mapper, class_):
506            for prop in mapper.iterate_properties:
507                if hasattr(prop, 'columns'):
508                    if prop.columns[0].type is sqltype:
509                        cls.associate_with_attribute(getattr(class_, prop.key))
510
511        event.listen(mapper, 'mapper_configured', listen_for_type)
512
513        return sqltype
514
515class _MutableCompositeMeta(type):
516    def __init__(cls, classname, bases, dict_):
517        cls._setup_listeners()
518        return type.__init__(cls, classname, bases, dict_)
519
520class MutableComposite(MutableBase):
521    """Mixin that defines transparent propagation of change
522    events on a SQLAlchemy "composite" object to its
523    owning parent or parents.
524    
525    See the example in :ref:`mutable_composites` for usage information.
526    
527    .. warning:: 
528    
529       The listeners established by the :class:`.MutableComposite`
530       class are *global* to all mappers, and are *not* garbage collected.   Only use 
531       :class:`.MutableComposite` for types that are permanent to an application,
532       not with ad-hoc types else this will cause unbounded growth
533       in memory usage.
534
535    """
536    __metaclass__ = _MutableCompositeMeta
537
538    def changed(self):
539        """Subclasses should call this method whenever change events occur."""
540
541        for parent, key in self._parents.items():
542
543            prop = object_mapper(parent).get_property(key)
544            for value, attr_name in zip(
545                                    self.__composite_values__(), 
546                                    prop._attribute_keys):
547                setattr(parent, attr_name, value)
548
549    @classmethod
550    def _setup_listeners(cls):
551        """Associate this wrapper with all future mapped composites
552        of the given type.
553
554        This is a convenience method that calls ``associate_with_attribute`` automatically.
555
556        """
557
558        def listen_for_type(mapper, class_):
559            for prop in mapper.iterate_properties:
560                if hasattr(prop, 'composite_class') and issubclass(prop.composite_class, cls):
561                    cls._listen_on_attribute(getattr(class_, prop.key), False, class_)
562
563        event.listen(mapper, 'mapper_configured', listen_for_type)
564