[Checkins] SVN: mongopersist/trunk/ - Feature: The new ``IdNamesMongoContainer`` class uses the natural Mongo

Stephen Richter cvs-admin at zope.org
Mon Apr 2 15:10:18 UTC 2012


Log message for revision 124891:
  - Feature: The new ``IdNamesMongoContainer`` class uses the natural Mongo
    ObjectId as the name/key for the items in the container. No more messing
    around with coming up or generating a name. Of course, if you specified
    ``None`` as a key in the past, it already used the object id, but it was
    sotred again in the mapping key field. Now the object id is used directly
    everywhere.
  

Changed:
  U   mongopersist/trunk/CHANGES.txt
  U   mongopersist/trunk/src/mongopersist/testing.py
  U   mongopersist/trunk/src/mongopersist/tests/test_datamanager.py
  U   mongopersist/trunk/src/mongopersist/zope/container.py
  U   mongopersist/trunk/src/mongopersist/zope/tests/test_container.py

-=-
Modified: mongopersist/trunk/CHANGES.txt
===================================================================
--- mongopersist/trunk/CHANGES.txt	2012-04-02 14:59:09 UTC (rev 124890)
+++ mongopersist/trunk/CHANGES.txt	2012-04-02 15:10:15 UTC (rev 124891)
@@ -27,6 +27,13 @@
   ``conflict_handler_factory`` to the data manager constructor. The factory
   needs to expect on argument, the data manager.
 
+- Feature: The new ``IdNamesMongoContainer`` class uses the natural Mongo
+  ObjectId as the name/key for the items in the container. No more messing
+  around with coming up or generating a name. Of course, if you specified
+  ``None`` as a key in the past, it already used the object id, but it was
+  sotred again in the mapping key field. Now the object id is used directly
+  everywhere.
+
 - Feature: Whenever ``setattr()`` is called on a persistent object, it is
   marked as changed even if the new value equals the old one. To minimize
   writes to MongoDB, the latest database state is compared to the new state
@@ -54,7 +61,7 @@
   accomplished by removing unnecessary database accesses, better caching and
   more efficient algorithms. This results in speedups between 4-25 times.
 
-  - When resolving the path to a klass, the result is now cached. More
+  - When resolving the path to a class, the result is now cached. More
     importantly, lookup failures are also cached mapping path ->
     ``None``. This is important, since an optimization the ``resolve()``
     method causes a lot of failing lookups.

Modified: mongopersist/trunk/src/mongopersist/testing.py
===================================================================
--- mongopersist/trunk/src/mongopersist/testing.py	2012-04-02 14:59:09 UTC (rev 124890)
+++ mongopersist/trunk/src/mongopersist/testing.py	2012-04-02 15:10:15 UTC (rev 124891)
@@ -24,8 +24,10 @@
 checker = renormalizing.RENormalizing([
     (re.compile(r'datetime.datetime(.*)'),
      'datetime.datetime(2011, 10, 1, 9, 45)'),
-    (re.compile(r"ObjectId\('[0-9a-f]*'\)"),
+    (re.compile(r"ObjectId\('[0-9a-f]{24}'\)"),
      "ObjectId('4e7ddf12e138237403000000')"),
+    (re.compile(r"u'[0-9a-f]{24}'"),
+     "u'4e7ddf12e138237403000000'"),
     (re.compile(r"object at 0x[0-9a-f]*>"),
      "object at 0x001122>"),
     ])

Modified: mongopersist/trunk/src/mongopersist/tests/test_datamanager.py
===================================================================
--- mongopersist/trunk/src/mongopersist/tests/test_datamanager.py	2012-04-02 14:59:09 UTC (rev 124890)
+++ mongopersist/trunk/src/mongopersist/tests/test_datamanager.py	2012-04-02 15:10:15 UTC (rev 124891)
@@ -671,7 +671,8 @@
        >>> foo_A = dm.load(foo_ref)
        >>> foo_A.name = u'1'
        >>> coll.find_one({})
-       {u'_id': ObjectId('4e7dd'), u'_py_serial': 1, u'name': u'one'}
+       {u'_id': ObjectId('4e7ddf12e138237403000000'),
+        u'_py_serial': 1, u'name': u'one'}
 
     2. Transaction B comes along and modifies the object as well and commits:
 
@@ -684,14 +685,16 @@
        >>> foo_B.name = 'Eins'
        >>> dm_B.tpc_finish(None)
        >>> coll.find_one({})
-       {u'_id': ObjectId('4e7dd'), u'_py_serial': 2, u'name': u'Eins'}
+       {u'_id': ObjectId('4e7ddf12e138237403000000'), u'_py_serial': 2,
+        u'name': u'Eins'}
 
     3. If transcation A is later aborted, it does not reset the state, since
        it changed:
 
        >>> dm.abort(None)
        >>> coll.find_one({})
-       {u'_id': ObjectId('4e7dd'), u'_py_serial': 2, u'name': u'Eins'}
+       {u'_id': ObjectId('4e7ddf12e138237403000000'), u'_py_serial': 2,
+        u'name': u'Eins'}
 
     """
 

Modified: mongopersist/trunk/src/mongopersist/zope/container.py
===================================================================
--- mongopersist/trunk/src/mongopersist/zope/container.py	2012-04-02 14:59:09 UTC (rev 124890)
+++ mongopersist/trunk/src/mongopersist/zope/container.py	2012-04-02 15:10:15 UTC (rev 124891)
@@ -15,7 +15,9 @@
 import UserDict
 import persistent
 import pymongo.dbref
+import pymongo.objectid
 import zope.component
+from bson.errors import InvalidId
 from rwproperty import getproperty, setproperty
 from zope.container import contained, sample
 from zope.container.interfaces import IContainer
@@ -154,6 +156,10 @@
             if key not in filter:
                 filter[key] = value
 
+    def _locate(self, obj, doc):
+        obj._v_key = doc[self._m_mapping_key]
+        obj._v_parent = self
+
     def _load_one(self, doc):
         # Create a DBRef object and then load the full state of the object.
         dbref = pymongo.dbref.DBRef(
@@ -162,22 +168,21 @@
         # Stick the doc into the _latest_states:
         self._m_jar._latest_states[dbref] = doc
         obj = self._m_jar.load(dbref)
-        obj._v_key = doc[self._m_mapping_key]
-        obj._v_parent = self
+        self._locate(obj, doc)
         return obj
 
     def __getitem__(self, key):
         filter = self._m_get_items_filter()
         filter[self._m_mapping_key] = key
-        coll = self.get_collection()
-        doc = coll.find_one(filter)
-        if doc is None:
+        obj = self.find_one(filter)
+        if obj is None:
             raise KeyError(key)
-        return self._load_one(doc)
+        return obj
 
     def _real_setitem(self, key, value):
         # This call by iteself caues the state to change _p_changed to True.
-        setattr(value, self._m_mapping_key, key)
+        if self._m_mapping_key is not None:
+            setattr(value, self._m_mapping_key, key)
         if self._m_parent_key is not None:
             setattr(value, self._m_parent_key, self._m_get_parent_key_value())
 
@@ -259,6 +264,53 @@
             return None
         return self._load_one(doc)
 
+
+class IdNamesMongoContainer(MongoContainer):
+    """A container that uses the Mongo ObjectId as the name/key."""
+    _m_mapping_key = None
+
+
+    @property
+    def _m_remove_documents(self):
+        # Objects must be removed, since removing the _id of a document is not
+        # allowed.
+        return True
+
+    def _locate(self, obj, doc):
+        obj._v_key = unicode(doc['_id'])
+        obj._v_parent = self
+
+    def __getitem__(self, key):
+        try:
+            id = pymongo.objectid.ObjectId(key)
+        except InvalidId:
+            raise KeyError(key)
+        filter = self._m_get_items_filter()
+        filter['_id'] = id
+        obj = self.find_one(filter)
+        if obj is None:
+            raise KeyError(key)
+        return obj
+
+    def __contains__(self, key):
+        try:
+            id = pymongo.objectid.ObjectId(key)
+        except InvalidId:
+            return False
+        return self.raw_find_one({'_id': id}, fields=()) is not None
+
+    def __iter__(self):
+        result = self.raw_find(fields=None)
+        for doc in result:
+            yield unicode(doc['_id'])
+
+    def iteritems(self):
+        result = self.raw_find()
+        for doc in result:
+            obj = self._load_one(doc)
+            yield unicode(doc['_id']), obj
+
+
 class AllItemsMongoContainer(MongoContainer):
     _m_parent_key = None
 

Modified: mongopersist/trunk/src/mongopersist/zope/tests/test_container.py
===================================================================
--- mongopersist/trunk/src/mongopersist/zope/tests/test_container.py	2012-04-02 14:59:09 UTC (rev 124890)
+++ mongopersist/trunk/src/mongopersist/zope/tests/test_container.py	2012-04-02 15:10:15 UTC (rev 124891)
@@ -447,6 +447,89 @@
       <Person Stephan>
     """
 
+def doctest_IdNamesMongoContainer_basic():
+    """IdNamesMongoContainer: basic
+
+    This container uses the Mongo ObjectId as the name for each object. Since
+    ObjectIds are required to be unique within a collection, this is actually
+    a nice and cheap scenario.
+
+    Let's add a container to the root:
+
+      >>> transaction.commit()
+      >>> dm.root['c'] = container.IdNamesMongoContainer('person')
+
+    Let's now add a new person:
+
+      >>> dm.root['c'].add(Person(u'Stephan'))
+      >>> keys = dm.root['c'].keys()
+      >>> keys
+      [u'4e7ddf12e138237403000003']
+      >>> name = keys[0]
+      >>> dm.root['c'][name]
+      <Person Stephan>
+
+      >>> dm.root['c'].values()
+      [<Person Stephan>]
+
+      >>> dm.root['c'][name].__parent__
+      <mongopersist.zope.container.IdNamesMongoContainer object at 0x7fec50f86500>
+      >>> dm.root['c'][name].__name__
+      u'4e7ddf12e138237403000003'
+
+    It is a feature of the container that the item is immediately available
+    after assignment, but before the data is stored in the database. Let's
+    commit and access the data again:
+
+      >>> transaction.commit()
+
+      >>> db = dm._conn[DBNAME]
+      >>> pprint(list(db['person'].find()))
+      [{u'_id': ObjectId('4e7e9d3ae138232d7b000003'),
+        u'name': u'Stephan',
+        u'parent': DBRef(u'mongopersist.zope.container.IdNamesMongoContainer',
+                         ObjectId('4e7e9d3ae138232d7b000000'),
+                         u'mongopersist_container_test')}]
+
+    Notice how there is no "key" entry in the document. We get a usual key
+    error, if an object does not exist:
+
+      >>> dm.root['c']['4e7e9d3ae138232d7b000fff']
+      Traceback (most recent call last):
+      ...
+      KeyError: '4e7e9d3ae138232d7b000fff'
+
+      >>> '4e7e9d3ae138232d7b000fff' in dm.root['c']
+      False
+
+      >>> dm.root['c']['roy']
+      Traceback (most recent call last):
+      ...
+      KeyError: 'roy'
+
+      >>> 'roy' in dm.root['c']
+      False
+
+    Now remove the item:
+
+      >>> del dm.root['c'][name]
+
+    The changes are immediately visible.
+
+      >>> dm.root['c'].keys()
+      []
+      >>> dm.root['c'][name]
+      Traceback (most recent call last):
+      ...
+      KeyError: u'4e7e9d3ae138232d7b000003'
+
+    Make sure it is really gone after committing:
+
+      >>> transaction.commit()
+      >>> dm.root['c'].keys()
+      []
+    """
+
 def doctest_AllItemsMongoContainer_basic():
     """AllItemsMongoContainer: basic
 
@@ -601,8 +684,10 @@
 checker = renormalizing.RENormalizing([
     (re.compile(r'datetime.datetime(.*)'),
      'datetime.datetime(2011, 10, 1, 9, 45)'),
-    (re.compile(r"ObjectId\('[0-9a-f]*'\)"),
+    (re.compile(r"ObjectId\('[0-9a-f]{24}'\)"),
      "ObjectId('4e7ddf12e138237403000000')"),
+    (re.compile(r"u'[0-9a-f]{24}'"),
+     "u'4e7ddf12e138237403000000'"),
     (re.compile(r"object at 0x[0-9a-f]*>"),
      "object at 0x001122>"),
     (re.compile(r"zodb-[0-9a-f].*"),



More information about the checkins mailing list