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