Commits

Chad Dombrova  committed 048ae7b

Refactor CollectionListener to use primary key values instead of ORM model instances.

Primary key values are the lowest-common-denominator and thus open up the base
CollectionListener to be used in more contexts, such as from external triggers.

  • Participants
  • Parent commits 7d1e625
  • Branches external_triggers

Comments (0)

Files changed (3)

File denormalize/orms/base.py

+from collections import defaultdict
 import logging
 
 log = logging.getLogger(__name__)
         self.model = model
         self.filter_path = filter_path
         self.relation = relation
+        self._cache = {'update': defaultdict(set),
+                       'delete': defaultdict(set)}
 
     def connect(self):
         """connect the listener to the necessary signals/events"""
         `connect()`"""
         raise NotImplementedError()
 
-    def _get_affected_instance_ids(self, instance):
+    def _get_affected_instance_ids(self, primary_key):
         """"Find all affected root model instance ids.
         These are the objects that reference the changed instance
         """
             # since it is not cross-orm compatible
             # Submodel
             affected = self.collection.model.get_objects_affected_by(
-                self.filter_path, instance)
+                self.filter_path, self.model, primary_key)
         else:
-            # FIXME: use inspector to get primary key
             # This is the root model
-            affected = [self.model.get_primary_key_value(instance)]
+            affected = [primary_key]
         return set(affected)
 
-    def _set_affected(self, ns, instance, affected_set):
+    def _set_affected(self, ns, primary_key, affected_set):
         """Used in pre_* handlers. Annotates an instance with
         the ids of the root objects it affects, so that we can include
         these in the final updates in the post_* handlers.
         """
-        attname = '_denormalize_extra_affected_{0}_{1}'.format(self.collection.name, ns)
-        current = getattr(instance, attname, set())
-        new = current | affected_set
-        setattr(instance, attname, new)
+        self._cache[ns][primary_key].update(affected_set)
 
-    def _get_affected(self, ns, instance):
+    def _get_affected(self, ns, primary_key):
         """See _set_affected for an explanation."""
-        attname = '_denormalize_extra_affected_{0}_{1}'.format(self.collection.name, ns)
-        affected_set = getattr(instance, attname, set())
-        return affected_set
+        return self._cache[ns].pop(primary_key, set())
 
     # -- callbacks
 
-    def pre_update(self, instance):
+    def pre_update(self, primary_key):
         log.debug('pre_update: collection %s, %s (%s) with id %s',
                   self.collection.name, self.model.name,
-                  self.filter_path or '^', self.model.get_primary_key_value(instance))
+                  self.filter_path or '^', primary_key)
 
         if self.filter_path:
-            affected = self._get_affected_instance_ids(instance)
+            affected = self._get_affected_instance_ids(primary_key)
             log.debug("pre_update:   affected documents: %r", affected)
 
             # Instead of calling the update functions directly, we make
             # sure it is queued for after the operation. Otherwise
             # the changes are not reflected in the database yet.
-            self._set_affected('update', instance, affected)
+            self._set_affected('update', primary_key, affected)
 
-    def post_update(self, instance):
+    def post_update(self, primary_key):
         log.debug('post_update: collection %s, %s (%s) with id %s',
                   self.collection.name, self.model.name,
-                  self.filter_path or '^', self.model.get_primary_key_value(instance))
+                  self.filter_path or '^', primary_key)
 
-        affected = self._get_affected_instance_ids(instance)
+        affected = self._get_affected_instance_ids(primary_key)
         log.debug("post_update:   affected documents: %r", affected)
 
         # This can be passed by the pre_save handler
-        extra_affected = self._get_affected('update', instance)
+        extra_affected = self._get_affected('update', primary_key)
         if extra_affected:
             affected.update(extra_affected)
 
         for doc_id in self.collection.map_affected(affected):
             self.backend._queue_changed(self.collection, doc_id)
 
-    def pre_insert(self, instance):
+    def pre_insert(self, primary_key):
         # Not used, does not need to be called/connected
         pass
 
-    def post_insert(self, instance):
+    def post_insert(self, primary_key):
         log.debug('post_insert: collection %s, %s (%s) with id %s',
                   self.collection.name, self.model.name,
-                  self.filter_path or '^', self.model.get_primary_key_value(instance))
+                  self.filter_path or '^', primary_key)
 
-        affected = self._get_affected_instance_ids(instance)
+        affected = self._get_affected_instance_ids(primary_key)
         for doc_id in self.collection.map_affected(affected):
             self.backend._queue_added(self.collection, doc_id)
 
-    def pre_delete(self, instance):
+    def pre_delete(self, primary_key):
         log.debug('pre_delete: collection %s, %s (%s) with id %s being deleted',
                   self.collection.name, self.model.name,
-                  self.filter_path or '^', self.model.get_primary_key_value(instance))
+                  self.filter_path or '^', primary_key)
 
-        affected = self._get_affected_instance_ids(instance)
+        affected = self._get_affected_instance_ids(primary_key)
         log.debug("pre_delete:   affected documents: %r", affected)
 
         # Instead of calling the update functions directly, we make
         # sure it is queued for after the operation. Otherwise
         # the changes are not reflected in the database yet.
-        self._set_affected('delete', instance, affected)
+        self._set_affected('delete', primary_key, affected)
 
-    def post_delete(self, instance):
+    def post_delete(self, primary_key):
         log.debug('post_delete: collection %s, %s (%s) with id %s was deleted',
                   self.collection.name, self.model.name,
-                  self.filter_path or '^', self.model.get_primary_key_value(instance))
+                  self.filter_path or '^', primary_key)
 
-        affected = self._get_affected('delete', instance)
+        affected = self._get_affected('delete', primary_key)
         if self.filter_path:
             queue = self.backend._queue_changed
         else:

File denormalize/orms/django.py

     # FIXME: move to CollectionModel?
     # FIXME: if it does not affect performance, return instances here, and get
     # the ids from them in the calling function
-    def get_objects_affected_by(self, join_path, instance):
-        filt = {join_path: instance}
+    def get_objects_affected_by(self, join_path, model, primary_key):
+        filt = {join_path: primary_key}
         # affected = self.collection.queryset(prefetch=False).filter(
         #     **filt).values_list('id', flat=True)
 
             # self.post_insert()
             return
 
-        self.pre_update(instance)
+        self.pre_update(instance.pk)
 
     def _post_save(self, sender, instance, created, raw, **kwargs):
         # From the Django docs: "True if the model is saved exactly as
             return
 
         if created:
-            self.post_insert(instance)
+            self.post_insert(instance.pk)
         else:
-            self.post_update(instance)
+            self.post_update(instance.pk)
 
     def _pre_delete(self, sender, instance, **kwargs):
-        self.pre_delete(instance)
+        self.pre_delete(instance.pk)
 
     def _post_delete(self, sender, instance, **kwargs):
-        self.post_delete(instance)
+        self.post_delete(instance.pk)
 
     def _m2m_changed(self, sender, instance, action, reverse, model, pk_set, **kwargs):
         """

File denormalize/orms/sqlalchemy.py

     # FIXME: move to CollectionModel?
     # FIXME: if it does not affect performance, return instances here, and get
     # the ids from them in the calling function
-    def get_objects_affected_by(self, join_path, instance):
+    def get_objects_affected_by(self, join_path, model, primary_key):
+        if not isinstance(primary_key, (list, tuple)):
+            primary_key = [primary_key]
+
         # FIXME: look into using the session from the instance:
         # session = sqlalchemy.inspect(inspect).session
         join_paths = join_path.split('__')
     # -- callbacks
 
     def _before_update(self, mapper, connection, instance):
-        self.pre_update(instance)
+        self.pre_update(self.model.get_primary_key_value(instance))
 
     def _after_update(self, mapper, connection, instance):
-        self.post_update(instance)
+        self.post_update(self.model.get_primary_key_value(instance))
 
     def _after_insert(self, mapper, connection, instance):
-        self.post_insert(instance)
+        self.post_insert(self.model.get_primary_key_value(instance))
 
     def _before_delete(self, mapper, connection, instance):
-        self.pre_delete(instance)
+        self.pre_delete(self.model.get_primary_key_value(instance))
 
     def _after_delete(self, mapper, connection, instance):
-        self.post_delete(instance)
+        self.post_delete(self.model.get_primary_key_value(instance))
 
     def _m2m_changed(self, mapper, connection, instance):
         print "sql m2m", mapper, instance