[source] implement storages right in the source rather than in hooks
authorAdrien Di Mascio <Adrien.DiMascio@logilab.fr>
Fri, 19 Mar 2010 19:21:31 +0100
changeset 4964 d9e8af8a7a42
parent 4963 6b0832bbd1da
child 4965 04543ed0bbdc
[source] implement storages right in the source rather than in hooks The problem is that Storage objects will most probably change entity's dictionary so that values are correctly set before the source's corresponding method (e.g. entity_added()) is called. For instance, the BFSFileStorage will change the original binary data and replace it with the destination file path in order to store the file path in the database. This change must be local to the source in order not to impact other hooks or attribute access during the transaction, the whole idea being that the same application code should work exactly the same whether or not a BFSStorage is used or not.
hooks/storages.py
server/sources/native.py
server/sources/storages.py
--- a/hooks/storages.py	Fri Mar 19 15:27:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-"""hooks to handle attributes mapped to a custom storage
-"""
-from cubicweb.server.hook import Hook
-from cubicweb.server.sources.storages import ETYPE_ATTR_STORAGE
-
-
-class BFSSHook(Hook):
-    """abstract class for bytes file-system storage hooks"""
-    __abstract__ = True
-    category = 'bfss'
-
-
-class PreAddEntityHook(BFSSHook):
-    """"""
-    __regid__ = 'bfss_add_entity'
-    events = ('before_add_entity', )
-
-    def __call__(self):
-        etype = self.entity.__regid__
-        for attr in ETYPE_ATTR_STORAGE.get(etype, ()):
-            ETYPE_ATTR_STORAGE[etype][attr].entity_added(self.entity, attr)
-
-class PreUpdateEntityHook(BFSSHook):
-    """"""
-    __regid__ = 'bfss_update_entity'
-    events = ('before_update_entity', )
-
-    def __call__(self):
-        etype = self.entity.__regid__
-        for attr in ETYPE_ATTR_STORAGE.get(etype, ()):
-            ETYPE_ATTR_STORAGE[etype][attr].entity_updated(self.entity, attr)
-
-class PreDeleteEntityHook(BFSSHook):
-    """"""
-    __regid__ = 'bfss_delete_entity'
-    events = ('before_delete_entity', )
-
-    def __call__(self):
-        etype = self.entity.__regid__
-        for attr in ETYPE_ATTR_STORAGE.get(etype, ()):
-            ETYPE_ATTR_STORAGE[etype][attr].entity_deleted(self.entity, attr)
--- a/server/sources/native.py	Fri Mar 19 15:27:45 2010 +0100
+++ b/server/sources/native.py	Fri Mar 19 19:21:31 2010 +0100
@@ -19,6 +19,7 @@
 from threading import Lock
 from datetime import datetime
 from base64 import b64decode, b64encode
+from contextlib import contextmanager
 
 from logilab.common.compat import any
 from logilab.common.cache import Cache
@@ -191,6 +192,8 @@
         self._cache = Cache(repo.config['rql-cache-size'])
         self._temp_table_data = {}
         self._eid_creation_lock = Lock()
+        # (etype, attr) / storage mapping
+        self._storages = {}
         # XXX no_sqlite_wrap trick since we've a sqlite locking pb when
         # running unittest_multisources with the wrapping below
         if self.dbdriver == 'sqlite' and \
@@ -267,6 +270,18 @@
     def unmap_attribute(self, etype, attr):
         self._rql_sqlgen.attr_map.pop('%s.%s' % (etype, attr), None)
 
+    def set_storage(self, etype, attr, storage):
+        storage_dict = self._storages.setdefault(etype, {})
+        storage_dict[attr] = storage
+        self.map_attribute(etype, attr, storage.sqlgen_callback)
+
+    def unset_storage(self, etype, attr):
+        self._storages[etype].pop(attr)
+        # if etype has no storage left, remove the entry
+        if not self._storages[etype]:
+            del self._storages[etype]
+        self.unmap_attribute(etype, attr)
+
     # ISource interface #######################################################
 
     def compile_rql(self, rql, sols):
@@ -402,40 +417,63 @@
                 except KeyError:
                     continue
 
+    @contextmanager
+    def _storage_handler(self, entity, event):
+        # 1/ memorize values as they are before the storage is called.
+        #    For instance, the BFSStorage will replace the `data`
+        #    binary value with a Binary containing the destination path
+        #    on the filesystem. To make the entity.data usage absolutely
+        #    transparent, we'll have to reset entity.data to its binary
+        #    value once the SQL query will be executed
+        orig_values = {}
+        etype = entity.__regid__
+        for attr, storage in self._storages.get(etype, {}).items():
+            if attr in entity.edited_attributes:
+                orig_values[attr] = entity[attr]
+                handler = getattr(storage, 'entity_%s' % event)
+                handler(entity, attr)
+        yield # 2/ execute the source's instructions
+        # 3/ restore original values
+        for attr, value in orig_values.items():
+            entity[attr] = value
+
     def add_entity(self, session, entity):
         """add a new entity to the source"""
-        attrs = self.preprocess_entity(entity)
-        sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs)
-        self.doexec(session, sql, attrs)
-        if session.undoable_action('C', entity.__regid__):
-            self._record_tx_action(session, 'tx_entity_actions', 'C',
-                                   etype=entity.__regid__, eid=entity.eid)
+        with self._storage_handler(entity, 'added'):
+            attrs = self.preprocess_entity(entity)
+            sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs)
+            self.doexec(session, sql, attrs)
+            if session.undoable_action('C', entity.__regid__):
+                self._record_tx_action(session, 'tx_entity_actions', 'C',
+                                       etype=entity.__regid__, eid=entity.eid)
 
     def update_entity(self, session, entity):
         """replace an entity in the source"""
-        attrs = self.preprocess_entity(entity)
-        if session.undoable_action('U', entity.__regid__):
-            changes = self._save_attrs(session, entity, attrs)
-            self._record_tx_action(session, 'tx_entity_actions', 'U',
-                                   etype=entity.__regid__, eid=entity.eid,
-                                   changes=self._binary(dumps(changes)))
-        sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, attrs,
-                                 ['cw_eid'])
-        self.doexec(session, sql, attrs)
+        with self._storage_handler(entity, 'updated'):
+            attrs = self.preprocess_entity(entity)
+            if session.undoable_action('U', entity.__regid__):
+                changes = self._save_attrs(session, entity, attrs)
+                self._record_tx_action(session, 'tx_entity_actions', 'U',
+                                       etype=entity.__regid__, eid=entity.eid,
+                                       changes=self._binary(dumps(changes)))
+            sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, attrs,
+                                     ['cw_eid'])
+            self.doexec(session, sql, attrs)
 
     def delete_entity(self, session, entity):
         """delete an entity from the source"""
-        if session.undoable_action('D', entity.__regid__):
-            attrs = [SQL_PREFIX + r.type
-                     for r in entity.e_schema.subject_relations()
-                     if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
-            changes = self._save_attrs(session, entity, attrs)
-            self._record_tx_action(session, 'tx_entity_actions', 'D',
-                                   etype=entity.__regid__, eid=entity.eid,
-                                   changes=self._binary(dumps(changes)))
-        attrs = {'cw_eid': entity.eid}
-        sql = self.sqlgen.delete(SQL_PREFIX + entity.__regid__, attrs)
-        self.doexec(session, sql, attrs)
+        with self._storage_handler(entity, 'deleted'):
+            if session.undoable_action('D', entity.__regid__):
+                attrs = [SQL_PREFIX + r.type
+                         for r in entity.e_schema.subject_relations()
+                         if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
+                changes = self._save_attrs(session, entity, attrs)
+                self._record_tx_action(session, 'tx_entity_actions', 'D',
+                                       etype=entity.__regid__, eid=entity.eid,
+                                       changes=self._binary(dumps(changes)))
+            attrs = {'cw_eid': entity.eid}
+            sql = self.sqlgen.delete(SQL_PREFIX + entity.__regid__, attrs)
+            self.doexec(session, sql, attrs)
 
     def _add_relation(self, session, subject, rtype, object, inlined=False):
         """add a relation to the source"""
--- a/server/sources/storages.py	Fri Mar 19 15:27:45 2010 +0100
+++ b/server/sources/storages.py	Fri Mar 19 19:21:31 2010 +0100
@@ -4,16 +4,11 @@
 from cubicweb import Binary
 from cubicweb.server.hook import Operation
 
-
-ETYPE_ATTR_STORAGE = {}
 def set_attribute_storage(repo, etype, attr, storage):
-    ETYPE_ATTR_STORAGE.setdefault(etype, {})[attr] = storage
-    repo.system_source.map_attribute(etype, attr, storage.sqlgen_callback)
+    repo.system_source.set_storage(etype, attr, storage)
 
 def unset_attribute_storage(repo, etype, attr):
-    ETYPE_ATTR_STORAGE.setdefault(etype, {}).pop(attr, None)
-    repo.system_source.unmap_attribute(etype, attr)
-
+    repo.system_source.unset_storage(etype, attr)
 
 class Storage(object):
     """abstract storage"""