Zephyr
Zephyr

Reputation: 2412

MongoDB/Mongoose atomic read & write on single Document

I need to update a Document based on certain criteria with mongoDB/mongoose.
So I search for the Document in the Collection based on ID. Once I have the document I check if it satisfies the condition I'm looking for (values of one the nested properties). Once I've confirmed the Document satisfies the criteria, I perform certain actions on the document and then Save the document.

The whole process of finding the Document, checking for criteria, making adjustments and updating, takes some time to complete.

Problem is I can trigger the api that runs this process multiple times.

While the entire process is running for the first api call, I can call the api multiple times (can't control api call) and have the entire process running again for the same document. So now I end up making the same updates twice on the Document. If the first call runs through successfully, the next call will not cos the first update will ensure the criteria is no longer met. But since they are being called while the first one hasn't finished updating it ends up going through successfully.

Any way I can perform all the steps as one atomic action?

Upvotes: 3

Views: 1245

Answers (2)

JCollier
JCollier

Reputation: 1179

Well, I'm a year or two late to this party, and this answer I'm about to write has some issues depending on your use case, but for my situation, it has worked pretty well.

Here's the solution with Mongoose / JavaScript:

const findCriteria1 = { _id: theId };

myModel.findOne(findCriteria1, function(error, dataSet) {
  if (dataSet.fieldInQuestion === valueIWantFieldToBe) {

    const findCriteria2 = { _id: theId, fieldInQuestion: dataSet.fieldInQuestion };
    const updateObject = { fieldInQuestion: updateValue };

    myModel.findOneAndUpdate(findCriteria2, updateObject, function(error, dataSet) {
      if (!error) {
        console.log('success')
      }
    });
  }
}

So basically, you find() the document with the value you want, and if it meets conditions, you do a findOneAndUpdate(), making sure that the document value did not change from what it was when you found it (as seen in findCriteria2).

One major issue with this solution is that it is possible for this operation to fail because the document value was updated by another user in between this operation's DB calls. This is unlikely, but may not be acceptable, especially if your DB is being pounded with frequent calls to the same document. A much better solution, if it exists, would be for a document lock and update queue, much like most SQL databases can do.

One way to help with that issue would be to wrap the whole solution I gave in a loop, and if the findOneAndUpdate fails, to try the loop again until it doesn't fail. You could set how many times you tried the loop... and this seems like a really bad idea, but you could do an infinite loop... of course, yeah, that could be really dangerous because it has the potential to totally disable the DB.

Another issue that my loop idea doesn't solve is that if you need a "first come, first served" model, that might not always be the case, as a DB request that thwarts the request before it may get to be the "first served".

And, a better idea altogether might just be to change how you model your data. Of course, this depends on what checks you need to run on your data, but you mentioned "values of nested properties" in your answer... what if those values were just in a seperate document and you could simply check what you needed to on the findOneAndUpdate() criteria parameter?

Upvotes: 1

D. SM
D. SM

Reputation: 14520

To operate on a consistent snapshot of the entire database, use a transaction with read concern snapshot.

Transactions do not magically prevent concurrency. withTransaction helper handles the mechanics of many cases of concurrent modifications transparently to the application, but you still need to understand concurrent operations on databases in general to write working/correct code.

Upvotes: 0

Related Questions