/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