Updating a 3-level nested value in MongoDb - mongodb

From [MongoDb's official documentation about updating] (http://docs.mongodb.org/manual/core/update/#Updating-The%24positionaloperator), I read:
The following operation queries the bios collection for the first document where the _id field equals 6 and the awards array contains a subdocument element with the by field equal to ACM. If found, the update() method updates the by field in the first matching subdocument:
db.bios.update(
{ _id: 6, 'awards.by': 'ACM' } ,
{ $set: { 'awards.$.by': 'Association for Computing Machinery' } }
)
What if I need another nesting level here? That is, what if rather than having:
{ _id: 6, awards: [ { by: 'ACM', prize: 1000}, { by: 'ACS', prize: 2000} ] }
I had something like
{ _id: 6, companies: [ { name: 'yyy', awards: [ { by: 'ACM', prize: 1000}, { by: 'ACS', prize: 2000 } ] } ] }
And I wanted to update the one where id is 6, companies.name is 'yyy', and companies.$.awards.$.by is 'ACM'? Is this even possible in MongoDB?
Merc.

Now you can do it using $[] operator:
db.coll.update({}, {$set: {"a.$[i].b.$[j].c": 5}}, {arrayFilters: [{"i.id": 1},{"j.c":1]});
Input: {a: [{id:1, b: [{c: 0},{c:1}]]}
Output: {a: [{id:1, b: {[c:0},{c:5}]]}
You will specify all needed filters inside arrayFilters.

I am afraid to say that YOU can NOT do it.
There is even open JIRA ticket: https://jira.mongodb.org/browse/SERVER-831.
You have to change the schema design until mongoDB team come up with the solution.

Related

Can't remove object in array using Mongoose

This has been extensively covered here, but none of the solutions seems to be working for me. I'm attempting to remove an object from an array using that object's id. Currently, my Schema is:
const scheduleSchema = new Schema({
//unrelated
_id: ObjectId
shifts: [
{
_id: Types.ObjectId,
name: String,
shift_start: Date,
shift_end: Date,
},
],
});
I've tried almost every variation of something like this:
.findOneAndUpdate(
{ _id: req.params.id },
{
$pull: {
shifts: { _id: new Types.ObjectId(req.params.id) },
},
}
);
Database:
Database Format
Within these variations, the usual response I've gotten has been either an empty array or null.
I was able slightly find a way around this and accomplish the deletion by utilizing the main _id of the Schema (instead of the nested one:
.findOneAndUpdate(
{ _id: <main _id> },
{ $pull: { shifts: { _id: new Types.ObjectId(<nested _id>) } } },
{ new: true }
);
But I was hoping to figure out a way to do this by just using the nested _id. Any suggestions?
The problem you are having currently is you are using the same _id.
Using mongo, update method allows three objects: query, update and options.
query object is the object into collection which will be updated.
update is the action to do into the object (add, change value...).
options different options to add.
Then, assuming you have this collection:
[
{
"_id": 1,
"shifts": [
{
"_id": 2
},
{
"_id": 3
}
]
}
]
If you try to look for a document which _id is 2, obviously response will be empty (example).
Then, if none document has been found, none document will be updated.
What happens if we look for a document using shifts._id:2?
This tells mongo "search a document where shifts field has an object with _id equals to 2". This query works ok (example) but be careful, this returns the WHOLE document, not only the array which match the _id.
This not return:
[
{
"_id": 1,
"shifts": [
{
"_id": 2
}
]
}
]
Using this query mongo returns the ENTIRE document where exists a field called shifts that contains an object with an _id with value 2. This also include the whole array.
So, with tat, you know why find object works. Now adding this to an update query you can create the query:
This one to remove all shifts._id which are equal to 2.
db.collection.update({
"shifts._id": 2
},
{
$pull: {
shifts: {
_id: 2
}
}
})
Example
Or this one to remove shifts._id if parent _id is equal to 1
db.collection.update({
"_id": 1
},
{
$pull: {
shifts: {
_id: 2
}
}
})
Example

MongoDB: Add field to all objects in array, based on other fields on same object?

I am fairly new to MongoDB and cant seem to find a solution to this problem.
I have a database of documents that has this structure:
{
id: 1
elements: [ {elementId: 1, nr1: 1, nr2: 3}, {elementId:2, nr1:5, nr2: 10} ]
}
I am looking for a query that can add a value nr3 which is for example nr2/nr1 to all the objects in the elements array, so that the resulting document would look like this:
{
id: 1
elements: [ {elementId: 1, nr1: 1, nr2: 3, nr3:3}, {elementId:2, nr1:5, nr2: 10, nr3: 2} ]
}
So I imagine a query along the lines of this:
db.collection.updateOne({id:1}, {$set:{"elements.$[].nr3": nr2/nr1}})
But I cant find how to get the value of nr2 and nr1 of the same object in the array.
I found some similar questions on stackoverflow stating this is not possible, but they were 5+ years old, so I thought maybe they have added support for something like this.
I realize I can achieve this with first querying the document and iterate over the elements-array doing updates along the way, but for the purpose of learning I would love to see if its possible to do this in one query.
You can use update with aggregation pipeline starting from MongoDB v4.2,
$map to iterate loop of elements
divide nr2 with nr1 using $divide
merge current object and new field nr3 using $mergeObjects
db.collection.updateOne(
{ id: 1 },
[{
$set: {
elements: {
$map: {
input: "$elements",
in: {
$mergeObjects: [
"$$this",
{ nr3: { $divide: ["$$this.nr2", "$$this.nr1"] } }
]
}
}
}
}
}]
)
Playground
db.collection.update(
{ id:1},
{ "$set": { "elements.$[elem].nr3":elements.$[elem].nr2/elements.$[elem].nr1} },
{ "multi": true }
);
I guess this should work

Update array filtering by 2 fields [duplicate]

This question already has an answer here:
Update array with multiple conditions in mongodb
(1 answer)
Closed 4 years ago.
I don't know if it is possible.
I'm trying to do an automatic process to update all elements of a nested array in some documents. The array hasn't a fixed length.
Below is a simplified example of the collection:
{
"_id" : ObjectId("5ba2e413a4dd01725a658c63"),
"MyOwnID" : "123456789",
"MyArray" : [
{
Field1: 'FooName1',
Field2: 'FooSpec1',
FieldToUpdate: '...'
},
{
Field1: 'FooName1',
Field2: 'FooSpec2',
FieldToUpdate: '...'
},
{
... More elements ...
}
]
},
{
"_id" : ObjectId("5ba2e413a4dd01725a658c63"),
"MyOwnID" : "987654321",
"MyArray" : [
{
Field1: 'FooName1',
Field2: 'FooSpec1',
FieldToUpdate: '...'
},
{
Field1: 'FooName2',
Field2: 'FooSpec2',
FieldToUpdate: '...'
},
]
}
I tried and it worked for the first element:
Query for the second element:
db.getCollection('works').findOneAndUpdate(
{ MyOwnID: '123456789', '$and':[ { 'MyArray.Field1': 'FooName1' },{ 'MyArray.Field2': 'FooSpec1' } ] } ,
{ '$set': { 'MyArray.$.FieldToUpdate': 1234} }
)
But when I try to update the second element only the first is updated.
Query for the second element:
db.getCollection('works').findOneAndUpdate(
{ MyOwnID: '123456789', '$and':[ { 'MyArray.Field1': 'FooName1' },{ 'MyArray.Field2': 'FooSpec2' } ] } ,
{ '$set': { 'MyArray.$.FieldToUpdate': 4321} }
)
I tried with arrayFilters option and $elemMatch, both give me an error.
Any options?
You can try below query using $elemMatch
db.getCollection("works").findOneAndUpdate(
{
"MyOwnID": "123456789",
"MyArray": { "$elemMatch": { "Field1": "FooName1", "Field2": "FooSpec2" }}
},
{ "$set": { "MyArray.$.FieldToUpdate": 4321 }}
)
You tried with arrayFilters, but probably in a wrong way, becuse it's working with it. It's not very clear in mongoDB doc, but $[myRef] acts as a placeholder for arrayFilters. Knowing that, you can do this to achieve your goal :
db['01'].findOneAndUpdate(
{MyOwnID: '123456789'},
{$set:{"MyArray.$[object].FieldToUpdate":1234}},
{arrayFilters:[{ $and:[{'object.Field1': 'FooName1' },{ 'object.Field2': 'FooSpec1' }]}]}
)
Note that the unique document in arrayFilters is needed (with $and operator), because both conditions refer to the placeholder. If you put 2 conditions,
({arrayFilters:[{'object.Field1': 'FooName1' },{ 'object.Field2':
'FooSpec1' }]})
MongoDB will complain about two criteria with same base placeholder.
While the answer given by #Anthony Winzlet is right and works perfectly, it will only update the first array element matching conditions defined in $elemMatch, that's why i avoid to use it like that (unless having a unique index on including MyArray.Field1 and MyArray.Field2, you can't be sure that the matching element is unique in your array)

MongoDB, right projection subfield [duplicate]

Is it possible to rename the name of fields returned in a find query? I would like to use something like $rename, however I wouldn't like to change the documents I'm accessing. I want just to retrieve them differently, something that works like SELECT COORINATES AS COORDS in SQL.
What I do now:
db.tweets.findOne({}, {'level1.level2.coordinates': 1, _id:0})
{'level1': {'level2': {'coordinates': [10, 20]}}}
What I would like to be returned is:
{'coords': [10, 20]}
So basically using .aggregate() instead of .find():
db.tweets.aggregate([
{ "$project": {
"_id": 0,
"coords": "$level1.level2.coordinates"
}}
])
And that gives you the result that you want.
MongoDB 2.6 and above versions return a "cursor" just like find does.
See $project and other aggregation framework operators for more details.
For most cases you should simply rename the fields as returned from .find() when processing the cursor. For JavaScript as an example, you can use .map() to do this.
From the shell:
db.tweets.find({},{'level1.level2.coordinates': 1, _id:0}).map( doc => {
doc.coords = doc['level1']['level2'].coordinates;
delete doc['level1'];
return doc;
})
Or more inline:
db.tweets.find({},{'level1.level2.coordinates': 1, _id:0}).map( doc =>
({ coords: doc['level1']['level2'].coordinates })
)
This avoids any additional overhead on the server and should be used in such cases where the additional processing overhead would outweigh the gain of actual reduction in size of the data retrieved. In this case ( and most ) it would be minimal and therefore better to re-process the cursor result to restructure.
As mentioned by #Neil Lunn this can be achieved with an aggregation pipeline:
And starting Mongo 4.2, the $replaceWith aggregation operator can be used to replace a document by a sub-document:
// { level1: { level2: { coordinates: [10, 20] }, b: 4 }, a: 3 }
db.collection.aggregate(
{ $replaceWith: { coords: "$level1.level2.coordinates" } }
)
// { "coords" : [ 10, 20 ] }
Since you mention findOne, you can also limit the number of resulting documents to 1 as such:
db.collection.aggregate([
{ $replaceWith: { coords: "$level1.level2.coordinates" } },
{ $limit: 1 }
])
Prior to Mongo 4.2 and starting Mongo 3.4, $replaceRoot can be used in place of $replaceWith:
db.collection.aggregate(
{ $replaceRoot: { newRoot: { coords: "$level1.level2.coordinates" } } }
)
As we know, in general, $project stage takes the field names and specifies 1 or 0/true or false to include the fields in the output or not, we also can specify the value against a field instead of true or false to rename the field. Below is the syntax
db.test_collection.aggregate([
{$group: {
_id: '$field_to_group',
totalCount: {$sum: 1}
}},
{$project: {
_id: false,
renamed_field: '$_id', // here assigning a value instead of 0 or 1 / true or false effectively renames the field.
totalCount: true
}}
])
Stages (>= 4.2)
$addFields : {"New": "$Old"}
$unset : {"$Old": 1}

MongoDB query to find property of first element of array

I have the following data in MongoDB (simplified for what is necessary to my question).
{
_id: 0,
actions: [
{
type: "insert",
data: "abc, quite possibly very very large"
}
]
}
{
_id: 1,
actions: [
{
type: "update",
data: "def"
},{
type: "delete",
data: "ghi"
}
]
}
What I would like is to find the first action type for each document, e.g.
{_id:0, first_action_type:"insert"}
{_id:1, first_action_type:"update"}
(It's fine if the data structured differently, but I need those values present, somehow.)
EDIT: I've tried db.collection.find({}, {'actions.action_type':1}), but obviously that returns all elements of the actions array.
NoSQL is quite new to me. Before, I would have stored all this in two tables in a relational database and done something like SELECT id, (SELECT type FROM action WHERE document_id = d.id ORDER BY seq LIMIT 1) action_type FROM document d.
You can use $slice operator in projection. (but for what you do i am not sure that the order of the array remain the same when you update it. Just to keep in mind))
db.collection.find({},{'actions':{$slice:1},'actions.type':1})
You can also use the Aggregation Pipeline introduced in version 2.2:
db.collection.aggregate([
{ $unwind: '$actions' },
{ $group: { _id: "$_id", first_action_type: { $first: "$actions.type" } } }
])
Using the $arrayElemAt operator is actually the most elegant way, although the syntax may be unintuitive:
db.collection.aggregate([
{ $project: {first_action_type: {$arrayElemAt: ["$actions.type", 0]}
])