Save on condition (Optimistic concurrency)
See original GitHub issueBackground
This is a proposal for optimistic concurrency
for mongoose.
We sometimes use the following code to modify the document instead of update
because validation is not ok (runValidator
doesn’t support everything)
let doc = yield Model.findById(id);
doc.prop = 123;
doc.val += 5;
yield doc.save();
However this is troublesome when another request is also modifying this document, it may cause the doc overwritten without any errors. I don’t know what to call this phenomenon, maybe race condition
Optimistic Concurrency Control
A better solution is that, when reading the model, store some important values, e.g. updatedAt
or __v
version key.
and when updating, add a condition that the timestamp should be the same.
This mechanism is called Optimistic concurrency control
, or OCC
or update-if-current
.
Reference: Mongodb Official document: Update Document if Current
let doc = yield Model.findById(id);
let timestamp = doc.updatedAt;
let updateProps = {
prop: 123,
val: doc.val + 5
};
// update only when timestamp match the original one
let updater = yield Model.update({ _id: doc._id, updatedAt: timestamp }, { $set: updateProps });
if (updater.result.nMatched === 0) {
// if update operation doesn't match, it means this document is modified by others or is removed.
throw new OCCError('This document is modified by others at this moment. Please try again');
}
//successful!
However, this is very ugly. Hence I propose an elegant way in mongoose.
Proposal 1 (General)
let doc = yield Model.findById(id);
doc.prop = 123;
doc.val += 5;
yield doc.save({cond: {updatedAt: doc.updatedAt }}); // if condition doesn't match, throw an error.
Proposal 2 (Native Support)
The previous proposal is a general way. Maybe we can include occ during find:
let doc = yield Model.findById(id).occ('updatedAt');
doc.prop = 123;
doc.val += 5;
yield doc.save(); //an OCCError is thrown if `updatedAt` doesn't match the first one
If there are multiple values needed to be checked, use this way
let doc = yield Model.findById(id).occ('firstProp secondProp')
// or
let doc = yield Model.findById(id).occ('firstProp').occ('secondProp')
// prop in subdocument
let doc = yield Model.findById(id).occ('parent.child')
Proposal 3 (Schema options)
new Schema({..}, { saveIfCurrent: '__v' });
new Schema({..}, { saveIfCurrent: 'updatedAt parent.child' });
schema.set('saveIfCurrent', 'updatedAt parent.child');
update-if-current
is the formal name.
Here, we emphasize that this behavior is working on save-if-current
.
Issue Analytics
- State:
- Created 8 years ago
- Reactions:3
- Comments:16 (2 by maintainers)
@lonix1 we added the ability to implement OCC plugins, and it looks like
mongoose-update-if-current
does exactly what we suggest. It looks like a solid plugin 👍 Caveat is that it only handlessave()
, notupdateOne()
, etc.OCC hasn’t really been a concern because for most apps I’ve worked on, Mongoose’s ability to only update the paths that have actually changed is good enough to prevent accidentally overwriting.
https://github.com/Automattic/mongoose/commit/8b4870c18c49c3bd1581c6a470ccf107addfb5f3 should give you the general direction of how one would write a plugin for this