Document serialization uses field order to ensure a strict order is set (#296)

This commit is contained in:
Ross Lawley 2013-04-26 11:38:45 +00:00
parent 2447349383
commit 36993097b4
8 changed files with 94 additions and 23 deletions

View File

@ -4,6 +4,7 @@ Changelog
Changes in 0.8.X Changes in 0.8.X
================ ================
- Document serialization uses field order to ensure a strict order is set (#296)
- DecimalField now stores as float not string (#289) - DecimalField now stores as float not string (#289)
- UUIDField now stores as a binary by default (#292) - UUIDField now stores as a binary by default (#292)
- Added Custom User Model for Django 1.5 (#285) - Added Custom User Model for Django 1.5 (#285)

View File

@ -24,6 +24,9 @@ objects** as class attributes to the document class::
title = StringField(max_length=200, required=True) title = StringField(max_length=200, required=True)
date_modified = DateTimeField(default=datetime.datetime.now) date_modified = DateTimeField(default=datetime.datetime.now)
As BSON (the binary format for storing data in mongodb) is order dependent,
documents are serialized based on their field order.
Dynamic document schemas Dynamic document schemas
======================== ========================
One of the benefits of MongoDb is dynamic schemas for a collection, whilst data One of the benefits of MongoDb is dynamic schemas for a collection, whilst data
@ -51,6 +54,7 @@ be saved ::
There is one caveat on Dynamic Documents: fields cannot start with `_` There is one caveat on Dynamic Documents: fields cannot start with `_`
Dynamic fields are stored in alphabetical order *after* any declared fields.
Fields Fields
====== ======

View File

@ -30,11 +30,14 @@ already exist, then any changes will be updated atomically. For example::
.. note:: .. note::
Changes to documents are tracked and on the whole perform `set` operations. Changes to documents are tracked and on the whole perform ``set`` operations.
* ``list_field.pop(0)`` - *sets* the resulting list * ``list_field.push(0)`` - *sets* the resulting list
* ``del(list_field)`` - *unsets* whole list * ``del(list_field)`` - *unsets* whole list
With lists its preferable to use ``Doc.update(push__list_field=0)`` as
this stops the whole list being updated - stopping any race conditions.
.. seealso:: .. seealso::
:ref:`guide-atomic-updates` :ref:`guide-atomic-updates`
@ -70,9 +73,10 @@ Cascading Saves
--------------- ---------------
If your document contains :class:`~mongoengine.fields.ReferenceField` or If your document contains :class:`~mongoengine.fields.ReferenceField` or
:class:`~mongoengine.fields.GenericReferenceField` objects, then by default the :class:`~mongoengine.fields.GenericReferenceField` objects, then by default the
:meth:`~mongoengine.Document.save` method will automatically save any changes to :meth:`~mongoengine.Document.save` method will not save any changes to
those objects as well. If this is not desired passing :attr:`cascade` as False those objects. If you want all references to also be saved also, noting each
to the save method turns this feature off. save is a separate query, then passing :attr:`cascade` as True
to the save method will cascade any saves.
Deleting documents Deleting documents
------------------ ------------------

View File

@ -120,7 +120,7 @@ eg::
p._mark_as_dirty('friends') p._mark_as_dirty('friends')
p.save() p.save()
`An example test migration is available on github `An example test migration for ReferenceFields is available on github
<https://github.com/MongoEngine/mongoengine/blob/master/tests/migration/refrencefield_dbref_to_object_id.py>`_. <https://github.com/MongoEngine/mongoengine/blob/master/tests/migration/refrencefield_dbref_to_object_id.py>`_.
UUIDField UUIDField
@ -148,7 +148,7 @@ eg::
a._mark_as_dirty('uuid') a._mark_as_dirty('uuid')
a.save() a.save()
`An example test migration is available on github `An example test migration for UUIDFields is available on github
<https://github.com/MongoEngine/mongoengine/blob/master/tests/migration/uuidfield_to_binary.py>`_. <https://github.com/MongoEngine/mongoengine/blob/master/tests/migration/uuidfield_to_binary.py>`_.
DecimalField DecimalField
@ -180,7 +180,7 @@ eg::
.. note:: DecimalField's have also been improved with the addition of precision .. note:: DecimalField's have also been improved with the addition of precision
and rounding. See :class:`~mongoengine.fields.DecimalField` for more information. and rounding. See :class:`~mongoengine.fields.DecimalField` for more information.
`An example test migration is available on github `An example test migration for DecimalFields is available on github
<https://github.com/MongoEngine/mongoengine/blob/master/tests/migration/decimalfield_as_float.py>`_. <https://github.com/MongoEngine/mongoengine/blob/master/tests/migration/decimalfield_as_float.py>`_.
Cascading Saves Cascading Saves
@ -196,6 +196,19 @@ you will have to explicitly tell it to cascade on save::
# Or on save: # Or on save:
my_document.save(cascade=True) my_document.save(cascade=True)
Storage
-------
Document and Embedded Documents are now serialized based on declared field order.
Previously, the data was passed to mongodb as a dictionary and which meant that
order wasn't guaranteed - so things like ``$addToSet`` operations on
:class:`~mongoengine.EmbeddedDocument` could potentially fail in unexpected
ways.
If this impacts you, you may want to rewrite the objects using the
``doc.mark_as_dirty('field')`` pattern described above. If you are using a
compound primary key then you will need to ensure the order is fixed and match
your EmbeddedDocument to that order.
Querysets Querysets
========= =========

View File

@ -6,6 +6,7 @@ from functools import partial
import pymongo import pymongo
from bson import json_util from bson import json_util
from bson.dbref import DBRef from bson.dbref import DBRef
from bson.son import SON
from mongoengine import signals from mongoengine import signals
from mongoengine.common import _import_class from mongoengine.common import _import_class
@ -228,11 +229,16 @@ class BaseDocument(object):
pass pass
def to_mongo(self): def to_mongo(self):
"""Return data dictionary ready for use with MongoDB. """Return as SON data ready for use with MongoDB.
""" """
data = {} data = SON()
for field_name, field in self._fields.iteritems(): data["_id"] = None
data['_cls'] = self._class_name
for field_name in self:
value = self._data.get(field_name, None) value = self._data.get(field_name, None)
field = self._fields.get(field_name)
if value is not None: if value is not None:
value = field.to_mongo(value) value = field.to_mongo(value)
@ -244,19 +250,27 @@ class BaseDocument(object):
if value is not None: if value is not None:
data[field.db_field] = value data[field.db_field] = value
# Only add _cls if allow_inheritance is True # If "_id" has not been set, then try and set it
if (hasattr(self, '_meta') and if data["_id"] is None:
self._meta.get('allow_inheritance', ALLOW_INHERITANCE) == True): data["_id"] = self._data.get("id", None)
data['_cls'] = self._class_name
if '_id' in data and data['_id'] is None: if data['_id'] is None:
del data['_id'] data.pop('_id')
# Only add _cls if allow_inheritance is True
if (not hasattr(self, '_meta') or
not self._meta.get('allow_inheritance', ALLOW_INHERITANCE)):
data.pop('_cls')
if not self._dynamic: if not self._dynamic:
return data return data
for name, field in self._dynamic_fields.items(): # Sort dynamic fields by key
dynamic_fields = sorted(self._dynamic_fields.iteritems(),
key=operator.itemgetter(0))
for name, field in dynamic_fields:
data[name] = field.to_mongo(self._data.get(name, None)) data[name] = field.to_mongo(self._data.get(name, None))
return data return data
def validate(self, clean=True): def validate(self, clean=True):

View File

@ -31,8 +31,9 @@ class DynamicTest(unittest.TestCase):
self.assertEqual(p.to_mongo(), {"_cls": "Person", "name": "James", self.assertEqual(p.to_mongo(), {"_cls": "Person", "name": "James",
"age": 34}) "age": 34})
self.assertEqual(p.to_mongo().keys(), ["_cls", "name", "age"])
p.save() p.save()
self.assertEqual(p.to_mongo().keys(), ["_id", "_cls", "name", "age"])
self.assertEqual(self.Person.objects.first().age, 34) self.assertEqual(self.Person.objects.first().age, 34)

View File

@ -168,6 +168,26 @@ class InheritanceTest(unittest.TestCase):
self.assertEqual(Employee._get_collection_name(), self.assertEqual(Employee._get_collection_name(),
Person._get_collection_name()) Person._get_collection_name())
def test_inheritance_to_mongo_keys(self):
"""Ensure that document may inherit fields from a superclass document.
"""
class Person(Document):
name = StringField()
age = IntField()
meta = {'allow_inheritance': True}
class Employee(Person):
salary = IntField()
self.assertEqual(['age', 'id', 'name', 'salary'],
sorted(Employee._fields.keys()))
self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(),
['_cls', 'name', 'age'])
self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(),
['_cls', 'name', 'age', 'salary'])
self.assertEqual(Employee._get_collection_name(),
Person._get_collection_name())
def test_polymorphic_queries(self): def test_polymorphic_queries(self):
"""Ensure that the correct subclasses are returned from a query """Ensure that the correct subclasses are returned from a query
@ -197,7 +217,6 @@ class InheritanceTest(unittest.TestCase):
classes = [obj.__class__ for obj in Human.objects] classes = [obj.__class__ for obj in Human.objects]
self.assertEqual(classes, [Human]) self.assertEqual(classes, [Human])
def test_allow_inheritance(self): def test_allow_inheritance(self):
"""Ensure that inheritance may be disabled on simple classes and that """Ensure that inheritance may be disabled on simple classes and that
_cls and _subclasses will not be used. _cls and _subclasses will not be used.
@ -213,8 +232,8 @@ class InheritanceTest(unittest.TestCase):
self.assertRaises(ValueError, create_dog_class) self.assertRaises(ValueError, create_dog_class)
# Check that _cls etc aren't present on simple documents # Check that _cls etc aren't present on simple documents
dog = Animal(name='dog') dog = Animal(name='dog').save()
dog.save() self.assertEqual(dog.to_mongo().keys(), ['_id', 'name'])
collection = self.db[Animal._get_collection_name()] collection = self.db[Animal._get_collection_name()]
obj = collection.find_one() obj = collection.find_one()

View File

@ -428,6 +428,21 @@ class InstanceTest(unittest.TestCase):
self.assertFalse('age' in person) self.assertFalse('age' in person)
self.assertFalse('nationality' in person) self.assertFalse('nationality' in person)
def test_embedded_document_to_mongo(self):
class Person(EmbeddedDocument):
name = StringField()
age = IntField()
meta = {"allow_inheritance": True}
class Employee(Person):
salary = IntField()
self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(),
['_cls', 'name', 'age'])
self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(),
['_cls', 'name', 'age', 'salary'])
def test_embedded_document(self): def test_embedded_document(self):
"""Ensure that embedded documents are set up correctly. """Ensure that embedded documents are set up correctly.
""" """