Is mongodb update with query atomic? - multithreading

mongodb write and update opertaions are atomic, as stated in their docs.
But does it also atomic when using query?
For example:
db.collection.update( { id : 1 , count : 0 } , { $inc : { count : 1 } } )
If I execute this operation in a multi threaded environment, is it possible that at some point the value of count in the document with id equal to 1 will be greater than 1?

Any modification to a single document is atomic.
Using your example, let's say there are two threads trying to update that document using the same query:
Thread A: db.collection.update({_id: 1, count: 0}, {$inc: {count: 1}})
Thread B: db.collection.update({_id: 1, count: 0}, {$inc: {count: 1}})
with the collection containing the document:
collection: {_id: 1, count: 0}
If thread A managed to update the document before thread B, the collection would then contain:
collection: {_id: 1, count: 1}
Thread B will search for a document matching _id:1, count:0 but since that document was already modified by thread A, the update in thread B will not proceed (since the document no longer "exists" as thread B is concerned).
In other words, thread A will return nMatched: 1, nModified: 1, and thread B will return nMatched: 0, nModified: 0.
To specifically answer your question, there will not be a case where a document {_id: 1, count: 2} will exist.

Related

Mongo - update a field with result of another field when a document is updated

I'm working with mongoose to manage my mongoDB collections.
I have one collection that contains two numeric fields. I want to set another field with the result of dividing these two fields every time that one of them is updated.
Let's sat this is my mongo collection:
{
id: 1,
num1: 100,
num2: 50,
result: 2
},
{
id: 2,
num1: 200,
num2: 10,
result: 20
},
{
id: 1,
num1: 40,
num2: 5,
result: 8
}
Now, the item with id 2 is updated, and num1 is set to 50. I want to recalculate and set result field with the new correct value (10).
(for updating items, I use the bulk.find().upsert().updateOne() funciton).
How can I do it with the less performance impact?
You can use a pre-save hook to do the calculation:
schema.pre('save', function(next) {
this.result = this.num1 / this.num2;
next();
});
...where 'schema' is your Mongoose schema.
Then when you update num1 or num2, the result will be calculated and saved.

How to find a document by its position/index in the array?

I need to retrieve let's say the documents at position 1,5 and 8 in a MongoDB database using Mongoose.
Is it possible at all to get a document by its position in a collection? If so, could you show how to do that?
I need something like this:
var someDocs = MyModel.find({<collectionIndex>: [1, 5, 8]}, function(err, docs) {
//do something with the three documents
})
I tried to do the following to see what indexes are used in collection but I get the 'getIndexes is not a function' error:
var indexes = MyModel.getIndexes();
Appreciate any help.
If by position 5 for example, you mean the literal 5th element in your collection, that isn't the best way to go. A Mongo collection is usually in the order in which you inserted the elements, but it may not always be. See the answer here and check the docs on natural order: https://stackoverflow.com/a/33018164/7531267.
In your case, you might have a unique id on each record that you can query by.
Assuming the [1, 5, 8] you mentioned are the ids, something like this should do it:
var someDocs = MyModel.find({ $or: [{ id: 1 }, { id: 5 }, { id: 8 }]}}, function(err, cb) {
//do something with the three documents
})
You can also read about $in to replace $or and clean up the query a bit.
Assume you have this document in users collections:
{
_id: ObjectId('...'),
name: 'wrq',
friends: ['A', 'B', 'C']
}
Code below to search first and thrid friend of user 'wrq':
db.users.aggregate(
[
{
$match: {
name: 'wrq'
}
},
{
$project:{
friend1: {
$arrayElemAt: ["$friends", 0]
},
friend3: {
$arrayElemAt: ["$friends", 2]
}
}
}
]
)

Insert an array item after a specific value in mongodb

I have db like;
{"_id" : 1 , children: ["2", "123", "5"]}
I want to insert an element into children array. It is easy when you know the index. $position can be used for this purpose. Like;
db.module_menu.update({"_id" : "1"} ,
{$push : {"children" : {
$each : ["12"] , $position : 1}}});
But lets say I don't know the position index, I know the value of element that I want to insert after.
I have value: 12 and value that I want to insert after. insertAfter: 2
As a result it should be;
{"_id" : 1, text: "" , children: ["2","12", "123", "5"]}
How can I do it?
Go through each element in the children array find the index of value and by using $position, insert the value. Is it the only way to do it?
I'm afraid it's not possible. There's an ancient issue to use $function in update but you're only way is to read, modify and persist the array.
On the other hand if you an array of embedded documents then you can use the $ positional operator.
The $ can be used to $set a value in you simple array but to $push.
> db.test.find()
{ "_id" : ObjectId("56d9570d09e01f20e254d0f3"), "a" : [ 1, 2, 3 ] }
> db.test.update({"_id": ObjectId("56d9570d09e01f20e254d0f3"), "a": 2},
{$push: {a: {$each: [2], $position: "$"}}})
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 2,
"errmsg" : "The value for $position must be a positive numeric value not a String"
}
})
There is no function to get position and update at one go,
so in you problem you need to get position before update and pass it to query - so that means we are operating on 1:1 bassis.
Other approach is to retrieve data - perform in-place update and push full array set back to mongo

In Mongo, Increment and return value -- possible in 1 call?

In Mongo, is it possible to increase and get the result of the increment?
collection.update({id: doc_id}, {$inc: {view_count: 1}});
I tried to output the result of that statement (in node) and I got the following:
{ _id: 1,
_state: undefined,
_result: undefined,
_subscribers: [] }
You can use findAndModify. Add the new:true option.
According to the docs:
The findAndModify command modifies and returns a single document. By default, the returned document does not include the modifications made on the update. To return the document with the modifications made on the update, use the new option.
You could do the following:
db.collection.findAndModify(
query: {_id: doc_id},
update: { $inc: { view_count :1 } },
new: true,
)
If you canĀ“t find the findAndModify method to use on your collection,you can use the findOneAndUpdate method.
Here is how to use:
The following code finds the first document where name : R. Stiles and increments the score by 5:
const result = await db.grades.findOneAndUpdate(
{ "name" : "R. Stiles" }, //also you can search for id
{ $inc: { "points" : 5 } }
)
The code returns the original document before the update inside the "value" propety:
{ _id: 6319, name: "R. Stiles", "points" : 0,... } // result.value returns document before update, but in the db it changued
If you want get the document uploaded, you has to set "returnNewDocument" to true, so the operation would return the updated document instead.
I hope it works for you.
source: https://www.mongodb.com/docs/manual/reference/method/db.collection.findOneAndUpdate/

Does $elemMatch not work with a new mongodb 2.2?

In a new version on MongoDB we can use an $elemMatch projection operator to limit the response of a query to a single matching element of an array. http://docs.mongodb.org/manual/reference/projection/elemMatch/
But it seems doesn't work yet in mongoose 3 here is the example:
{
_id: ObjectId(5),
items: [1,2,3,45,4,67,9,4]
}
Folder.findOne({_id: Object(5)}, {$elemMatch: {$in: [1,67,9]}})
.exec(function (err, doc) {
});
I'm expected to get the follows doc:
{
_id: ObjectId(5),
items: [1,67,9]
}
But unfortunately what I'm getting is document with all items:
{
_id: ObjectId(5),
items: [1,2,3,45,4,67,9,4]
}
The mongodb docs here are misleading, we'll get them updated.
What its saying is that you can now use $elemMatch in your projection, that is, your field selection:
https://gist.github.com/3640687
See also: https://github.com/learnboost/mongoose/issues/1085
[Edit] pull request for docs sent: https://github.com/mongodb/docs/pull/185
Firstly, you are missing the items field name in front of the $elemMatch operator. Your query should read
Folder.findOne({_id: Object(5)}, {items: {$elemMatch: {$in: [1,67,9]}}})
.exec(function (err, doc) { });
But this would still not return the desired result, because as stated in the documentation:
The $elemMatch projection will only match one array element per source
document.
So you would only get back something like:
{
_id: ObjectId(5),
items: [1]
}
I haven't got mongoose set up to do this with node, but you can also get the result you want using the new aggregation framework in 2.2 - here's an example that gets you the result you wanted. First, my sample doc looks like this:
> db.foo.findOne()
{
"_id" : ObjectId("50472eb566caf6af6108de02"),
"items" : [
1,
2,
3,
45,
4,
67,
9,
4
]
}
To get to what you want I did this:
> db.foo.aggregate(
{$match : {"_id": ObjectId("50472eb566caf6af6108de02")}},
{$unwind : "$items"},
{$match : {"items": {$in : [1, 67, 9]}}},
{$group : {_id : "$_id", items : { $push : "$items"}}},
{$project : {_id : 0, items : 1}}
)
{
"result" : [
{
"_id" : ObjectId("50472eb566caf6af6108de02"),
"items" : [
1,
67,
9
]
}
],
"ok" : 1
}
To explain, in detail I will take it line by line:
{$match : {"_id": ObjectId("50472eb566caf6af6108de02")}}
This is fairly obvious - it is basically the equivalent to the find criteria on a regular query, the results are passed to the next step in the pipeline to be processed. This is the piece that can use indexes etc.
{$unwind : "$items"}
This will explode the array, creating a stream of documents, one for each element of the array.
{$match : {"items": {$in : [1, 67, 9]}}}
This second match will return only the documents in the list, basically reducing the stream of docs to a result set of three.
{$group : {_id : "$_id", items : { $push : "$items"}}}
We want our output to be an array, so we have to undo the unwind above now that we have selected the items we want, using the _id as the key to group. Note: this will have repeating values if there is more than one match, if you wanted a unique list you would use $addToSet instead of $push
{$project : {_id : 1, items : 1}}
Then finally, this projection is not really needed, but I included it to illustrate the functionality - you could choose to not return the _id if you wished etc.
Neither $elemMatch nor MongoDB in general will filter data from an array. $elemMatch can be used to match a document but it won't affect the data to be returned. You can only include/exclude fields from a documented by using the filter parameter (second parameter of a find() findOne() call) but you can not filter the result based on some query input.

Resources