Multiple update in a document in MongoDB - mongodb

I am trying to update multiple nested documents in a document in mongoDB.
Say my data is:
{
"_id" : "ObjectId(7df78ad8902c)",
"title" : "Test",
"img_url" : "[{s: 1, v:1}, {s: 2, v: 2}, {s: 3, v: 3}]",
"tags" : "['mongodb', 'database', 'NoSQL']",
"likes" : "100"
}
I want to update v to 200 for s = 1 and s= 2 in img_url list.
It is easy to update v for any single s.
Is there any way to update multiple documents satisfying some criteria.
I tried:
db.test.update({ "_id" : ObjectId("7df78ad8902c"), "img_url.s": {$in : ["1", "2"]}}, {$set: { "img_url.$.v" : 200 } });
and
db.test.update({ "_id" : ObjectId("7df78ad8902c"), "img_url.s": {$in : ["1", "2"]}}, {$set: { "img_url.$.v" : 200 } }, {mulit: true});
Some sources are suggesting it is not possible to do so.
Multiple update of embedded documents' properties
https://jira.mongodb.org/browse/SERVER-1243
Am I missing something ?

For the specific case/example you have here. You are specifying an _id which means you are to update only one with that specific _id.
to update img_url try without the _id; something like this:
db.test.update({}, {"$set":{"img_url.0":{s:1, v:400}}}, {multi:true})
db.test.update({}, {"$set":{"img_url.1":{s:2, v:400}}}, {multi:true})
0 and 1 in img_url are the array indexes for s:1 and s:2
in order to update based on specific criteria you need to set the attribute you need on the first argument. say for example, to update all documents that have likes greater than 100 increment by 1 you do (assuming likes type is int...):
db.people.update( { likes: {$gt:100} }, {$inc :{likes: 1}}, {multi: true} )
hope that helps

Related

MongoDB Aggregation - Does $unwind order documents the same way as the nested array order

I am wandering whether using $unwind operator in aggregation pipeline for document with nested array will return the deconstructed documents in the same order as the order of the items in the array.
Example:
Suppose I have the following documents
{ "_id" : 1, "item" : "foo", values: [ "foo", "foo2", "foo3"] }
{ "_id" : 2, "item" : "bar", values: [ "bar", "bar2", "bar3"] }
{ "_id" : 3, "item" : "baz", values: [ "baz", "baz2", "baz3"] }
I would like to use paging for all values in all documents in my application code. So, my idea is to use mongo aggregation framework to:
sort the documents by _id
use $unwind on values attribute to deconstruct the documents
use $skip and $limit to simulate paging
So the question using the example described above is:
Is it guaranteed that the following aggregation pipeline:
[
{$sort: {"_id": 1}},
{$unwind: "$values"}
]
will always result to the following documents with exactly the same order?:
{ "_id" : 1, "item" : "foo", values: "foo" }
{ "_id" : 1, "item" : "foo", values: "foo2" }
{ "_id" : 1, "item" : "foo", values: "foo3" }
{ "_id" : 2, "item" : "bar", values: "bar" }
{ "_id" : 2, "item" : "bar", values: "bar2" }
{ "_id" : 2, "item" : "bar", values: "bar3" }
{ "_id" : 3, "item" : "baz", values: "baz" }
{ "_id" : 3, "item" : "baz", values: "baz2" }
{ "_id" : 3, "item" : "baz", values: "baz3" }
I also asked the same question in the MongoDB community forum . An answer that confirms my assumption was posted from a member of MongoDB stuff.
Briefly:
Yes, the order of the returned documents in the example above will always be the same. It follows the order from the array field.
In the case that you do run into issues with order. You could use includeArrayIndex to guarantee order.
[
{$unwind: {
path: 'values',
includeArrayIndex: 'arrayIndex'
}},
{$sort: {
_id: 1,
arrayIndex: 1
}},
{ $project: {
index: 0
}}
]
From what I see at https://github.com/mongodb/mongo/blob/0cee67ce6909ca653462d4609e47edcc4ac5c1a9/src/mongo/db/pipeline/document_source_unwind.cpp
The cursor iterator uses getNext() method to unwind an array:
DocumentSource::GetNextResult DocumentSourceUnwind::doGetNext() {
auto nextOut = _unwinder->getNext();
while (nextOut.isEOF()) {
.....
// Try to extract an output document from the new input document.
_unwinder->resetDocument(nextInput.releaseDocument());
nextOut = _unwinder->getNext();
}
return nextOut;
}
And the getNext() implemenation relies on array's index:
DocumentSource::GetNextResult DocumentSourceUnwind::Unwinder::getNext() {
....
// Set field to be the next element in the array. If needed, this will automatically
// clone all the documents along the field path so that the end values are not shared
// across documents that have come out of this pipeline operator. This is a partial deep
// clone. Because the value at the end will be replaced, everything along the path
// leading to that will be replaced in order not to share that change with any other
// clones (or the original).
_output.setNestedField(_unwindPathFieldIndexes, _inputArray[_index]);
indexForOutput = _index;
_index++;
_haveNext = _index < length;
.....
return _haveNext ? _output.peek() : _output.freeze();
}
So unless there is anything upstream that messes with document's order the cursor should have unwound docs in the same order as subdocs were stored in the array.
I don't recall how merger works for sharded collections and I imagine there might be a case when documents from other shards are returned from between 2 consecutive unwound documents. What the snippet of the code guarantees is that unwound document with next item from the array will never be returned before unwound document with previous item from the array.
As a side note, having million items in an array is quite an extreme design. Even 20-bytes items in the array will exceed 16Mb doc limit.

incrementing version number in a subdocument and the parent document

I am trying to add versioning levels in a document as well as its subdocuments. Heres a schema example
{
"_id" : ObjectId("59d1312a8ee6de1858933950"),
"synonyms" : [
{
"_id" : ObjectId("59d1312a8ee6de1858933954"),
"text" : [
1.0,
2.0,
3.0
],
"__v" : 1.0
},
{
"_id" : ObjectId("59d1312a8ee6de1858933953"),
"text" : [
"Foo ",
"bar ",
"Baz"
],
"__v" : 0
},
{
"_id" : ObjectId("59d1312a8ee6de1858933951"),
"text" : [
"fizz",
"bazz",
"bizz"
],
"__v" : 0
}
],
"__v" : 3.0
}
As you can see , the parent document has its own __v while each subdocument (part of the synonyms array) also has its __v . What i'm trying to accomplish is this
When updating a subdocument array - increment the version of the subdocument as well as its parent version
to that effect , i've tried the below code
db.collection.update({
'_id': ObjectId("59d1312a8ee6de1858933950"),
"synonyms._id": ObjectId("59d1312a8ee6de1858933954")
},
{$set: {'synonyms.$.text': [1,2,3]}, $inc: {'synonyms.$.__v': 1}, $inc: {"__v": 1}}
)
My parent __v is getting increment on every update but the subdocument seems to be stuck at 1.0 no matter how many updates i go through. Is there a better way?
If you think about the parameters that you pass to a MongoDB command as a JSON document rather than a string this makes perfect sense: Passing in the same operator twice (as in $inc at the start and later another $inc again in your example) will create a JSON document that only contains the last parameter. This would be different if you were using a string here which would actually represent a JSON document with two $inc fields.
So here's how to get it right (basically by $incing two fields as part of a single operation):
db.collection.update({
'_id': ObjectId("59d1312a8ee6de1858933950"),
"synonyms._id": ObjectId("59d1312a8ee6de1858933954")
},
{$set: {'synonyms.$.text': [1,2,3]}, $inc: {'synonyms.$.__v': 1, "__v": 1}}
)

How to query nested document in mongodb?

I have document with nested document reviews:
{
"_id" : ObjectId("53a5753937c2f0ef6dcd9006"),
"product" : "Super Duper-o-phonic",
"price" : 11000000000,
"reviews" : [
{
"user" : "fred",
"comment" : "Great!",
"rating" : 5
},
{
"user" : "Tom",
"comment" : "Great again!",
"rating" : 5
},
{
"user" : "Tom",
"comment" : "I agree with fred somewhat",
"rating" : 4
}
]
}
I want to find only those reviews whose rating is 5.
Final query should select product price and two documents from reviews whose rating is 5.
The last query I tried is :
db.testData.find({'reviews':{$elemMatch:{'rating':{$gte:5}}}}).pretty()
It's strange but it doesn't work.
How to do this in mongodb?
If you only want a single sub-doc from reviews whose rating is 5, you can use the $ positional projection operator to include the first element of reviews that matches your query:
db.test.find({'reviews.rating': 5}, {product: 1, price: 1, 'reviews.$': 1})
If you want all reviews elements with a rating of 5 (instead of just the first) you can use aggregate instead of find:
db.test.aggregate([
// Only include docs with at least one 5 rating review
{$match: {'reviews.rating': 5}},
// Duplicate the docs, one per reviews element
{$unwind: '$reviews'},
// Only include the ones where rating = 5
{$match: {'reviews.rating': 5}},
// Only include the following fields in the output
{$project: {product: 1, price: 1, reviews: 1}}])
Take a look up here: MongoDB - how to query for a nested item inside a collection?
Just in case you thought about this:
If you try to accomplish this with $elemMatchit will jsut return the first matching review.
http://docs.mongodb.org/manual/reference/operator/projection/elemMatch/

MongoDB Why this error : can't append to array using string field name: comments

I have a DB structure like below:
{
"_id" : 1,
"comments" : [
{
"_id" : 2,
"content" : "xxx"
}
]
}
I update a new subdocument in the comments feild. It is OK.
db.test.update(
{"_id" : 1, "comments._id" : 2},
{$push : {"comments.$.comments" : {_id : 3, content:"xxx"}}}
)
after that the DB structure:
{
"_id" : 1,
"comments" : [
{
"_id" : 2,
"comments" : [
{
"id" : 3,
"content" : "xxx"
}
],
"content" : "xxx"
}
]
}
But when I update a new subdocument in the comment field that _id is 3, There is a error:
db.test.update(
{"_id" : 1, "comments.comments.id" : 3},
{$push : {"comments.comments.$.comments" : {id : 4, content:"xxx"}}}
)
error message:
can't append to array using string field name: comments
Well, it makes total sense if you think about it. MongoDb has the advantage and the disadvantage of solving magically certain things.
When you query the database for a specific regular field like this:
{ field : "value" }
The query {field:"value"} makes total sense, it wouldn't in case value is part of an array but Mongo solves it for you, so in case the structure is:
{ field : ["value", "anothervalue"] }
Mongo iterates through all of them and matches "value" into the field and you don't have to think about it. It works perfectly.. at only one level, because it's impossible to guess what you want to do if you have multiple levels
In your case the first query works because it's the case in this example:
db.test.update(
{"_id" : 1, "comments._id" : 2},
{$push : {"comments.$.comments" : {_id : 3, content:"xxx"}}}
)
Matches _id in the first level, and comments._id at the second level, it gets an array as a result but Mongo is able to solve it.
But in the second case, think what you need, let's isolate the where clause:
{"_id" : 1, "comments.comments.id" : 3},
"Give me from the main collection records with _id:1" (one doc)
"And comments which comments inside have and id=3" (array * array)
The first level is solved easily, comments.id, the second is not possible due comments returns an array, but one more level is an array of arrays and Mongo gets an array of arrays as a result and it's not possible to push a document into all the records of the array.
The solution is to narrow your where clause to obtain an unique document in comments (could be the first one) but it's not a good solution because you never know what is the position of the document you're looking for, using the shell I think the only option to be accurate is to do it in two steps. Check this query that works (not the solution anyway) but "solves" the multiple array part fixing it to the first record:
db.test.update(
{"_id" : 1, "comments.0.comments._id" : 3},
{$push : {"comments.0.comments.$.comments" : {id : 4, content:"xxx"}}}
)

mongodb upsert in updating an array element

Want to upsert in object properties in a array of a document
Consider a document in collection m
{ "_id" : ObjectId("524bfc39e6bed5cc5a9f3a33"),
"x" : [
{ "id":0.0, "name":"aaa"},{ "id":1.0, "name":"bbb"}
]
}
Want to add age:100 to { "id":0.0, "name":"aaa"} .
Not just age .. But but provision for upsert in the array element {}. So it can contain {age:100,"city":"amd"} (since i am getting this from the application service)
Was trying this... But did not worked as it replaced the entire array element
db.m.update({_id:ObjectId("524bfc39e6bed5cc5a9f3a33"),
"x" : {
"$elemMatch" : {
"id" : 0.0
}
}},
{
$set : {
"x.$" : {
"age": 100
}
}
},
{upsert: true}
)
Changed the document to (which i did not wanted)
{ "_id" : ObjectId("524bfc39e6bed5cc5a9f3a33"),
"x" : [
{ "age":100},{ "id":1.0, "name":"bbb"}
]
}
Is this possible without changing schema.
$set : {"x.$" : {"age": 100}}
x.$ sets the entire matched array element to {age: 100}
This should work:
db.m.update({_id:ObjectId("524bfc39e6bed5cc5a9f3a33"),
"x.id": 0.0}, {$set: {"x.$.age": 100 }});
Using elemMatch:
db.test.update({x: {$elemMatch: {id: 1}}},{$set: {"x.$.age": 44}})
Note that the upsert option here, is redundant and wouldn't work if the id isn't present in x because the positional operator $ doesn't support upserting.
This is not possible without changing schema. If you can change schema to use an object to store your items (rather than an array), you can follow the approach I outlined in this answer.