Merge pull request #2049 from bagerard/save_to_mongo_call_in_save

Improve perf of Document.save
This commit is contained in:
Bastien Gérard 2019-06-01 15:00:44 +02:00 committed by GitHub
commit a18c8c0eb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 97 additions and 35 deletions

View File

@ -4,9 +4,10 @@ Changelog
Development 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: - 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).
- 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 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,78 @@ 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
})
# 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): 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 +926,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):

View File

@ -227,6 +227,9 @@ class SignalTests(unittest.TestCase):
self.ExplicitId.objects.delete() 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) self.assertEqual(self.pre_signals, post_signals)
def test_model_signals(self): def test_model_signals(self):