Skip to content

Could really use doc_instance.save() with a precondition #460

@michaelbartnett

Description

@michaelbartnett

In my current project I find myself needing to update a complex document. Calculating the update deltas myself would be a pain and I like to rely on MongoEngine's for knowing how/when to pass the shard key along, so the Document instance interface is proving to be helpful for this. But when I save this document instance I need to include my own selection query to ensure that a last_changed timestamp hasn't changed between the read and the update.

What do you think about adding a precondition kwarg to Document.save()? Or even a different save_with_precondition function. Or better yet, is there already a straightforward and easy way to do this sort of save/update?

I'm experimenting with my own conditional_save function that is a direct copy of the save function but with the following changes:

    # Line 180 in mongoengine/document.py
    def conditional_save(self, force_insert=False, validate=True, clean=True,
             write_concern=None,  cascade=None, cascade_kwargs=None,
             _refs=None, precondition=None, **kwargs):
    # ...
    # Line 234
        try:
            collection = self._get_collection()
            if created:
                if force_insert:
                    object_id = collection.insert(doc, **write_concern)
                else:
                    object_id = collection.save(doc, **write_concern)
            else:
                object_id = doc['_id']
                updates, removals = self._delta()
                select_dict = precondition or {}
                # Need to add shard key to query, or you get an error
                select_dict.update(_id=object_id)
    # ...
    # Line 288
                if updates or removals:
                    do_upsert = precondition is None
                    last_error = collection.update(select_dict, update_query,
                                                   upsert=do_upsert, **write_concern)
                    created = is_new_object(last_error)
                    if last_error.get('n', None) != 1:
                        raise self.__class__.DoesNotExist()

Usage would generally look like this:

class MyDocument(Document):
    value = IntField(default=0)
    last_changed = DateTimeField(default=utcnow)

doc_inst = MyDocument.objects.limit(1).get()
doc_inst.value += 1
check = {'last_changed': doc_inst.last_changed}
try:
    doc_inst.conditional_save(precondition=check)
except MyDocument.DoesNotExist:
    print('Failed the precondition')
else:
    print('Successfully saved')

I'm raising the DoesNotExist exception since that is technically what is happening and to avoid breaking the chaining behavior of save(). This doesn't feel quite right, API design-wise, but aside from adding another more specific exception I'm not sure how else to report the precondition failing. There's also the fact that the upsert kwarg now depends on precondition's value.

Does this make sense as a feature and as an approach for implementing it? I'm guessing it would make more sense as an extended Document.update() function, but that doesn't immediately have the delta calculation working without interference from me.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions