Improve perf of Doc.save by preventing a full to_mongo() call just to get the created variable

This commit is contained in:
Bastien Gérard 2019-05-08 23:45:35 +02:00
parent 1907133f99
commit 9ae8fe7c2d
4 changed files with 89 additions and 34 deletions

View File

@ -7,6 +7,7 @@ Development
- Add support for MongoDB 3.6 and Python3.7 in travis - Add support for MongoDB 3.6 and Python3.7 in travis
- BREAKING CHANGE: Changed the custom field validator (i.e `validation` parameter of Field) so that it now requires: - 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). the callable to raise a ValidationError (i.o return True/False).
- Prevent an expensive call to to_mongo in Document.save() to improve performance #?
- Fix querying on List(EmbeddedDocument) subclasses fields #1961 #1492 - Fix querying on List(EmbeddedDocument) subclasses fields #1961 #1492
- Fix querying on (Generic)EmbeddedDocument subclasses fields #475 - Fix querying on (Generic)EmbeddedDocument subclasses fields #475
- expose `mongoengine.connection.disconnect` and `mongoengine.connection.disconnect_all` - expose `mongoengine.connection.disconnect` and `mongoengine.connection.disconnect_all`

View File

@ -293,8 +293,7 @@ class BaseDocument(object):
""" """
Return as SON data ready for use with MongoDB. Return as SON data ready for use with MongoDB.
""" """
if not fields: fields = fields or []
fields = []
data = SON() data = SON()
data['_id'] = None data['_id'] = None

View File

@ -259,7 +259,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
data = super(Document, self).to_mongo(*args, **kwargs) data = super(Document, self).to_mongo(*args, **kwargs)
# If '_id' is None, try and set it from self._data. If that # 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 data['_id'] is None:
if self._data.get('id') is None: if self._data.get('id') is None:
del data['_id'] del data['_id']
@ -365,10 +365,11 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
.. versionchanged:: 0.10.7 .. versionchanged:: 0.10.7
Add signal_kwargs argument Add signal_kwargs argument
""" """
signal_kwargs = signal_kwargs or {}
if self._meta.get('abstract'): if self._meta.get('abstract'):
raise InvalidDocumentError('Cannot save an abstract document.') raise InvalidDocumentError('Cannot save an abstract document.')
signal_kwargs = signal_kwargs or {}
signals.pre_save.send(self.__class__, document=self, **signal_kwargs) signals.pre_save.send(self.__class__, document=self, **signal_kwargs)
if validate: if validate:
@ -377,9 +378,8 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
if write_concern is None: if write_concern is None:
write_concern = {} write_concern = {}
doc = self.to_mongo() doc_id = self.to_mongo(fields=['id'])
created = ('_id' not in doc_id or self._created or force_insert)
created = ('_id' not in doc or self._created or force_insert)
signals.pre_save_post_validation.send(self.__class__, document=self, signals.pre_save_post_validation.send(self.__class__, document=self,
created=created, **signal_kwargs) created=created, **signal_kwargs)

View File

@ -16,7 +16,7 @@ from mongoengine.pymongo_support import list_collection_names
from tests import fixtures from tests import fixtures
from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest, from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest,
PickleDynamicEmbedded, PickleDynamicTest) PickleDynamicEmbedded, PickleDynamicTest)
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase, get_as_pymongo
from mongoengine import * from mongoengine import *
from mongoengine.base import get_document, _document_registry from mongoengine.base import get_document, _document_registry
@ -715,39 +715,74 @@ class InstanceTest(MongoDBTestCase):
acc1 = Account.objects.first() acc1 = Account.objects.first()
self.assertHasInstance(acc1._data["emails"][0], acc1) 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
})
def test_document_clean(self): def test_document_clean(self):
class TestDocument(Document): class TestDocument(Document):
status = StringField() status = StringField()
pub_date = DateTimeField() cleaned = BooleanField(default=False)
def clean(self): def clean(self):
if self.status == 'draft' and self.pub_date is not None: self.cleaned = True
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()
TestDocument.drop_collection() TestDocument.drop_collection()
t = TestDocument(status="draft", pub_date=datetime.now()) t = TestDocument(status="draft")
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})
# Ensure clean=False prevent call to clean
t = TestDocument(status="published") t = TestDocument(status="published")
t.save(clean=False) t.save(clean=False)
self.assertEqual(t.status, "published")
self.assertEqual(t.pub_date, None) self.assertEqual(t.cleaned, False)
t = TestDocument(status="published") t = TestDocument(status="published")
self.assertEqual(t.cleaned, False)
t.save(clean=True) t.save(clean=True)
self.assertEqual(t.status, "published")
self.assertEqual(type(t.pub_date), datetime) 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): def test_document_embedded_clean(self):
class TestEmbeddedDocument(EmbeddedDocument): class TestEmbeddedDocument(EmbeddedDocument):
@ -887,19 +922,39 @@ class InstanceTest(MongoDBTestCase):
person.save() person.save()
# Ensure that the object is in the database # Ensure that the object is in the database
collection = self.db[self.Person._get_collection_name()] raw_doc = get_as_pymongo(person)
person_obj = collection.find_one({'name': 'Test User'}) self.assertEqual(
self.assertEqual(person_obj['name'], 'Test User') raw_doc,
self.assertEqual(person_obj['age'], 30) {
self.assertEqual(person_obj['_id'], person.id) '_cls': 'Person',
'name': 'Test User',
'age': 30,
'_id': person.id
})
# Test skipping validation on save def test_save_skip_validation(self):
class Recipient(Document): class Recipient(Document):
email = EmailField(required=True) email = EmailField(required=True)
recipient = Recipient(email='not-an-email') recipient = Recipient(email='not-an-email')
self.assertRaises(ValidationError, recipient.save) with self.assertRaises(ValidationError):
recipient.save()
recipient.save(validate=False) 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): def test_save_to_a_value_that_equates_to_false(self):
class Thing(EmbeddedDocument): class Thing(EmbeddedDocument):