Merge pull request #2049 from bagerard/save_to_mongo_call_in_save
Improve perf of Document.save
This commit is contained in:
		@@ -4,9 +4,10 @@ Changelog
 | 
			
		||||
 | 
			
		||||
Development
 | 
			
		||||
===========
 | 
			
		||||
- Add support for MongoDB 3.6 and Python3.7 in travis
 | 
			
		||||
- Add support for MongoDB 3.6 in travis
 | 
			
		||||
- BREAKING CHANGE: Changed the custom field validator (i.e `validation` parameter of Field) so that it now requires:
 | 
			
		||||
    the callable to raise a ValidationError (i.o return True/False).
 | 
			
		||||
- Improve perf of .save by avoiding a call to to_mongo in Document.save() #2049
 | 
			
		||||
- Fix querying on List(EmbeddedDocument) subclasses fields #1961 #1492
 | 
			
		||||
- Fix querying on (Generic)EmbeddedDocument subclasses fields #475
 | 
			
		||||
- expose `mongoengine.connection.disconnect` and `mongoengine.connection.disconnect_all`
 | 
			
		||||
 
 | 
			
		||||
@@ -293,8 +293,7 @@ class BaseDocument(object):
 | 
			
		||||
        """
 | 
			
		||||
        Return as SON data ready for use with MongoDB.
 | 
			
		||||
        """
 | 
			
		||||
        if not fields:
 | 
			
		||||
            fields = []
 | 
			
		||||
        fields = fields or []
 | 
			
		||||
 | 
			
		||||
        data = SON()
 | 
			
		||||
        data['_id'] = None
 | 
			
		||||
 
 | 
			
		||||
@@ -259,7 +259,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
 | 
			
		||||
        data = super(Document, self).to_mongo(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # If '_id' is None, try and set it from self._data. If that
 | 
			
		||||
        # doesn't exist either, remote '_id' from the SON completely.
 | 
			
		||||
        # doesn't exist either, remove '_id' from the SON completely.
 | 
			
		||||
        if data['_id'] is None:
 | 
			
		||||
            if self._data.get('id') is None:
 | 
			
		||||
                del data['_id']
 | 
			
		||||
@@ -365,10 +365,11 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
 | 
			
		||||
        .. versionchanged:: 0.10.7
 | 
			
		||||
            Add signal_kwargs argument
 | 
			
		||||
        """
 | 
			
		||||
        signal_kwargs = signal_kwargs or {}
 | 
			
		||||
 | 
			
		||||
        if self._meta.get('abstract'):
 | 
			
		||||
            raise InvalidDocumentError('Cannot save an abstract document.')
 | 
			
		||||
 | 
			
		||||
        signal_kwargs = signal_kwargs or {}
 | 
			
		||||
        signals.pre_save.send(self.__class__, document=self, **signal_kwargs)
 | 
			
		||||
 | 
			
		||||
        if validate:
 | 
			
		||||
@@ -377,9 +378,8 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
 | 
			
		||||
        if write_concern is None:
 | 
			
		||||
            write_concern = {}
 | 
			
		||||
 | 
			
		||||
        doc = self.to_mongo()
 | 
			
		||||
 | 
			
		||||
        created = ('_id' not in doc or self._created or force_insert)
 | 
			
		||||
        doc_id = self.to_mongo(fields=['id'])
 | 
			
		||||
        created = ('_id' not in doc_id or self._created or force_insert)
 | 
			
		||||
 | 
			
		||||
        signals.pre_save_post_validation.send(self.__class__, document=self,
 | 
			
		||||
                                              created=created, **signal_kwargs)
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ from mongoengine.pymongo_support import list_collection_names
 | 
			
		||||
from tests import fixtures
 | 
			
		||||
from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest,
 | 
			
		||||
                            PickleDynamicEmbedded, PickleDynamicTest)
 | 
			
		||||
from tests.utils import MongoDBTestCase
 | 
			
		||||
from tests.utils import MongoDBTestCase, get_as_pymongo
 | 
			
		||||
 | 
			
		||||
from mongoengine import *
 | 
			
		||||
from mongoengine.base import get_document, _document_registry
 | 
			
		||||
@@ -715,39 +715,78 @@ class InstanceTest(MongoDBTestCase):
 | 
			
		||||
        acc1 = Account.objects.first()
 | 
			
		||||
        self.assertHasInstance(acc1._data["emails"][0], acc1)
 | 
			
		||||
 | 
			
		||||
    def test_save_checks_that_clean_is_called(self):
 | 
			
		||||
        class CustomError(Exception):
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        class TestDocument(Document):
 | 
			
		||||
            def clean(self):
 | 
			
		||||
                raise CustomError()
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(CustomError):
 | 
			
		||||
            TestDocument().save()
 | 
			
		||||
 | 
			
		||||
        TestDocument().save(clean=False)
 | 
			
		||||
 | 
			
		||||
    def test_save_signal_pre_save_post_validation_makes_change_to_doc(self):
 | 
			
		||||
        class BlogPost(Document):
 | 
			
		||||
            content = StringField()
 | 
			
		||||
 | 
			
		||||
            @classmethod
 | 
			
		||||
            def pre_save_post_validation(cls, sender, document, **kwargs):
 | 
			
		||||
                document.content = 'checked'
 | 
			
		||||
 | 
			
		||||
        signals.pre_save_post_validation.connect(BlogPost.pre_save_post_validation, sender=BlogPost)
 | 
			
		||||
 | 
			
		||||
        BlogPost.drop_collection()
 | 
			
		||||
 | 
			
		||||
        post = BlogPost(content='unchecked').save()
 | 
			
		||||
        self.assertEqual(post.content, 'checked')
 | 
			
		||||
        # Make sure pre_save_post_validation changes makes it to the db
 | 
			
		||||
        raw_doc = get_as_pymongo(post)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            raw_doc,
 | 
			
		||||
            {
 | 
			
		||||
                'content': 'checked',
 | 
			
		||||
                '_id': post.id
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        # Important to disconnect as it could cause some assertions in test_signals
 | 
			
		||||
        # to fail (due to the garbage collection timing of this signal)
 | 
			
		||||
        signals.pre_save_post_validation.disconnect(BlogPost.pre_save_post_validation)
 | 
			
		||||
 | 
			
		||||
    def test_document_clean(self):
 | 
			
		||||
        class TestDocument(Document):
 | 
			
		||||
            status = StringField()
 | 
			
		||||
            pub_date = DateTimeField()
 | 
			
		||||
            cleaned = BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
            def clean(self):
 | 
			
		||||
                if self.status == 'draft' and self.pub_date is not None:
 | 
			
		||||
                    msg = 'Draft entries may not have a publication date.'
 | 
			
		||||
                    raise ValidationError(msg)
 | 
			
		||||
                # Set the pub_date for published items if not set.
 | 
			
		||||
                if self.status == 'published' and self.pub_date is None:
 | 
			
		||||
                    self.pub_date = datetime.now()
 | 
			
		||||
                self.cleaned = True
 | 
			
		||||
 | 
			
		||||
        TestDocument.drop_collection()
 | 
			
		||||
 | 
			
		||||
        t = TestDocument(status="draft", pub_date=datetime.now())
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(ValidationError) as cm:
 | 
			
		||||
            t.save()
 | 
			
		||||
 | 
			
		||||
        expected_msg = "Draft entries may not have a publication date."
 | 
			
		||||
        self.assertIn(expected_msg, cm.exception.message)
 | 
			
		||||
        self.assertEqual(cm.exception.to_dict(), {'__all__': expected_msg})
 | 
			
		||||
        t = TestDocument(status="draft")
 | 
			
		||||
 | 
			
		||||
        # Ensure clean=False prevent call to clean
 | 
			
		||||
        t = TestDocument(status="published")
 | 
			
		||||
        t.save(clean=False)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(t.pub_date, None)
 | 
			
		||||
        self.assertEqual(t.status, "published")
 | 
			
		||||
        self.assertEqual(t.cleaned, False)
 | 
			
		||||
 | 
			
		||||
        t = TestDocument(status="published")
 | 
			
		||||
        self.assertEqual(t.cleaned, False)
 | 
			
		||||
        t.save(clean=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(type(t.pub_date), datetime)
 | 
			
		||||
        self.assertEqual(t.status, "published")
 | 
			
		||||
        self.assertEqual(t.cleaned, True)
 | 
			
		||||
        raw_doc = get_as_pymongo(t)
 | 
			
		||||
        # Make sure clean changes makes it to the db
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            raw_doc,
 | 
			
		||||
            {
 | 
			
		||||
                'status': 'published',
 | 
			
		||||
                'cleaned': True,
 | 
			
		||||
                '_id': t.id
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
    def test_document_embedded_clean(self):
 | 
			
		||||
        class TestEmbeddedDocument(EmbeddedDocument):
 | 
			
		||||
@@ -887,19 +926,39 @@ class InstanceTest(MongoDBTestCase):
 | 
			
		||||
        person.save()
 | 
			
		||||
 | 
			
		||||
        # Ensure that the object is in the database
 | 
			
		||||
        collection = self.db[self.Person._get_collection_name()]
 | 
			
		||||
        person_obj = collection.find_one({'name': 'Test User'})
 | 
			
		||||
        self.assertEqual(person_obj['name'], 'Test User')
 | 
			
		||||
        self.assertEqual(person_obj['age'], 30)
 | 
			
		||||
        self.assertEqual(person_obj['_id'], person.id)
 | 
			
		||||
        raw_doc = get_as_pymongo(person)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            raw_doc,
 | 
			
		||||
            {
 | 
			
		||||
                '_cls': 'Person',
 | 
			
		||||
                'name': 'Test User',
 | 
			
		||||
                'age': 30,
 | 
			
		||||
                '_id': person.id
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        # Test skipping validation on save
 | 
			
		||||
    def test_save_skip_validation(self):
 | 
			
		||||
        class Recipient(Document):
 | 
			
		||||
            email = EmailField(required=True)
 | 
			
		||||
 | 
			
		||||
        recipient = Recipient(email='not-an-email')
 | 
			
		||||
        self.assertRaises(ValidationError, recipient.save)
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            recipient.save()
 | 
			
		||||
 | 
			
		||||
        recipient.save(validate=False)
 | 
			
		||||
        raw_doc = get_as_pymongo(recipient)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            raw_doc,
 | 
			
		||||
            {
 | 
			
		||||
                'email': 'not-an-email',
 | 
			
		||||
                '_id': recipient.id
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
    def test_save_with_bad_id(self):
 | 
			
		||||
        class Clown(Document):
 | 
			
		||||
            id = IntField(primary_key=True)
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            Clown(id="not_an_int").save()
 | 
			
		||||
 | 
			
		||||
    def test_save_to_a_value_that_equates_to_false(self):
 | 
			
		||||
        class Thing(EmbeddedDocument):
 | 
			
		||||
 
 | 
			
		||||
@@ -227,6 +227,9 @@ class SignalTests(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        self.ExplicitId.objects.delete()
 | 
			
		||||
 | 
			
		||||
        # Note that there is a chance that the following assert fails in case
 | 
			
		||||
        # some receivers (eventually created in other tests)
 | 
			
		||||
        # gets garbage collected (https://pythonhosted.org/blinker/#blinker.base.Signal.connect)
 | 
			
		||||
        self.assertEqual(self.pre_signals, post_signals)
 | 
			
		||||
 | 
			
		||||
    def test_model_signals(self):
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user