I'm trying to figure out how can I push a new item into a deeply nested array in mongodb.
Take this structure for example:
{
name : "high school",
departments : [
{
name : "math",
courses : [
{
name : "algebra",
lessons: [
{
name: "algebra 1a",
students: [
{
name: "Dave",
id : "123456"
},
{
name: "alex",
id: "987654"
}
]
}
]
}
]
}
]
}
I know that I should use $ when pushing into a nested array, like this:
db.schools.update({"departments.name":"math"},{$push:{"departments.$.courses":x}})
But when trying to access a deeper array it throws an error.
How can I add a new lesson? or a new student?
Based on my research and understanding, this is not currently possible to do within mongodb in a single operation. According to this JIRA ticket, it is desired for an upcoming version of mongodb.
Here is a tedious way of doing in the shell what you are attempting to accomplish:
var school = db.schools.findOne({name: "high school"}) //assuming unique
school.departments.forEach(function(department) {
if(department.name === "math") {
department.courses.forEach(function(course) {
if(course.name === "algebra") {
course.lessons.forEach(function(lesson) {
if(lesson.name === "algebra 1a") {
lesson.students.push({name: "Jon", id: "43953"});
}
});
}
});
}
})
db.schools.save(school)
Related
I'm struggling to write a Mongo UpdateMany statement that can reference and update an object within an array.
Here I create 3 documents. Each document has an array called innerArray always containing a single object, with a single date field.
use test;
db.innerArrayExample.insertOne({ _id: 1, "innerArray": [ { "originalDateTime" : ISODate("2022-01-01T01:01:01Z") } ]});
db.innerArrayExample.insertOne({ _id: 2, "innerArray": [ { "originalDateTime" : ISODate("2022-01-02T01:01:01Z") } ]});
db.innerArrayExample.insertOne({ _id: 3, "innerArray": [ { "originalDateTime" : ISODate("2022-01-03T01:01:01Z") } ]});
I want to add a new date field, based on the original date field, to end up with this:
{ _id: 1, "innerArray": [ { "originalDateTime" : ISODate("2022-01-01T01:01:01Z"), "copiedDateTime" : ISODate("2022-01-01T12:01:01Z") } ]}
{ _id: 2, "innerArray": [ { "originalDateTime" : ISODate("2022-01-02T01:01:01Z"), "copiedDateTime" : ISODate("2022-01-02T12:01:01Z") } ]}
{ _id: 3, "innerArray": [ { "originalDateTime" : ISODate("2022-01-03T01:01:01Z"), "copiedDateTime" : ISODate("2022-01-03T12:01:01Z") } ]}
In pseudo code I am saying take the originalDateTime, run it through a function and add a related copiedDateTime value.
For my specific use-case, the function I want to run strips the timezone from originalDateTime, then overwrites it with a new one, equivalent to the Java ZonedDateTime function withZoneSameLocal. Aka 9pm UTC becomes 9pm Brussels (therefore effectively 7pm UTC). The technical justification and methodology were answered in another Stack Overflow question here.
The part of the query I'm struggling with, is the part that updates/selects data from an element inside an array. In my simplistic example, for example I have crafted this query, but unfortunately it doesn't work:
This function puts copiedDateTime in the correct place... but doesn't evaluate the commands to manipulate the date:
db.innerArrayExample.updateMany({ "innerArray.0.originalDateTime" : { $exists : true }}, { $set: { "innerArray.0.copiedDateTime" : { $dateFromString: { dateString: { $dateToString: { "date" : "$innerArray.0.originalDateTime", format: "%Y-%m-%dT%H:%M:%S.%L" }}, format: "%Y-%m-%dT%H:%M:%S.%L", timezone: "Europe/Paris" }}});
// output
{
_id: 1,
innerArray: [
{
originalDateTime: ISODate("2022-01-01T01:01:01.000Z"),
copiedDateTime: {
'$dateFromString': {
dateString: { '$dateToString': [Object] },
format: '%Y-%m-%dT%H:%M:%S.%L',
timezone: 'Europe/Paris'
}
}
}
]
}
This simplified query, also has the same issue:
b.innerArrayExample.updateMany({ "innerArray.0.originalDateTime" : { $exists : true }}, { $set: { "innerArray.0.copiedDateTime" : "$innerArray.0.originalDateTime" }});
//output
{
_id: 1,
innerArray: [
{
originalDateTime: ISODate("2022-01-01T01:01:01.000Z"),
copiedDateTime: '$innerArray.0.originalDateTime'
}
]
}
As you can see this issue looks to be separate from the other stack overflow question. Instead of being able changing timezones, it's about getting things inside arrays to update.
I plan to take this query, create 70,000 variations of it with different location/timezone combinations and run it against a database with millions of records, so I would prefer something that uses updateMany instead of using Javascript to iterate over each row in the database... unless that's the only viable solution.
I have tried putting $set in square brackets. This changes the way it interprets everything, evaluating the right side, but causing other problems:
test> db.innerArrayExample.updateMany({ "_id" : 1 }, [{ $set: { "innerArray.0.copiedDateTime" : "$innerArray.0.originalDateTime" }}]);
//output
{
_id: 1,
innerArray: [
{
'0': { copiedDateTime: [] },
originalDateTime: ISODate("2022-01-01T01:01:01.000Z")
}
]
}
Above it seems to interpret .0. as a literal rather than an array element. (For my needs I know the array only has 1 item at all times). I'm at a loss finding an example that meets my needs.
I have also tried experimenting with the arrayFilters, documented on my mongo updateMany documentation but I cannot fathom how it works with objects:
test> db.innerArrayExample.updateMany(
... { },
... { $set: { "innerArray.$[element].copiedDateTime" : "$innerArray.$[element].originalDateTime" } },
... { arrayFilters: [ { "originalDateTime": { $exists: true } } ] }
... );
MongoServerError: No array filter found for identifier 'element' in path 'innerArray.$[element].copiedDateTime'
test> db.innerArrayExample.updateMany(
... { },
... { $set: { "innerArray.$[0].copiedDateTime" : "$innerArray.$[element].originalDateTime" } },
... { arrayFilters: [ { "0.originalDateTime": { $exists: true } } ] }
... );
MongoServerError: Error parsing array filter :: caused by :: The top-level field name must be an alphanumeric string beginning with a lowercase letter, found '0'
If someone can help me understand the subtleties of the Mongo syntax and help me back on to the right path I'd be very grateful.
You want to be using pipelined updates, the issue you're having with the syntax you're using is that it does not allow the usage of aggregation operators and document field values.
Here is a quick example on how to do it:
db.collection.updateMany({},
[
{
"$set": {
"innerArray": {
$map: {
input: "$innerArray",
in: {
$mergeObjects: [
"$$this",
{
copiedDateTime: "$$this.originalDateTime"
}
]
}
}
}
}
}
])
Mongo Playground
I Have Document Like,
{
"_id" : ObjectId("5ab4cc12c773133bae3d8dc9"),
"__v" : 19.0,
"geoGraphicalFilter" : {
"aCountries" : [
{
"country" : "Spain",
"_id" : ObjectId("5ad49ab21210c42aa6ccba23"),
"cities" : [ ]
},
{
"country" : "India",
"_id" : ObjectId("5ad49ab21210c42aa6ccba24"),
"cities" : [ ]
}
]
}
}
Model
const HuntingWindow = new Schema({
geoGraphicalFilter: {
aCountries: [
{
country: String,
cities: { type: Array }
}
]
},
});
Snippet
const addCityFilter = (req, res) => {
if (req.body.aCities === "") {
res.status(409).jsonp({ message: adminMessages.err_fill_val_properly });
return false;
} else {
var Cities = req.body.aCities.split(",");
huntingModel.update({ _id: req.body.id,"data.geoGraphicalFilter.aCountries":req.body.sName },
{$push:{"geoGraphicalFilter.0.aCountries.$.cities":Cities}},
(error, data) => {
// Cities.forEach(element => {
// data.geoGraphicalFilter.aCountries.find(req.body.sName)
// });
data.save((error, success) => {
res.status(200).jsonp({
message: adminMessages.succ_countryFilter_added
});
});
});
}
};
Now I want to first Find Document by root id and then I want to match the country name and insert the cities in the Array. I am new to MongoDB and nodejs how can i do this ? While i am trying to with update query but i thing i am doing it on wrong way plese help me with it.
Try following code:
huntingModel.update(
{ _id: ObjectId("5ab4cc12c773133bae3d8dc9"), "geoGraphicalFilter.aCountries.country": "Spain" },
{ $addToSet: { "geoGraphicalFilter.aCountries.$.cities": { $each: [ "city1", "city2" ] } } }
)
You should specify two filtering conditions: one for entire document and one to match array element. Then you can use $ operator to update first matching document in that array. To push multiple values you can use $each operator. To ensure that there will be no duplicates you should use $addToSet
I am working on an app that contain details of an academic year in Multilevel structure of mongoDB database.
For this I've created structure like this:
[
{
"_id" : ObjectId("5a1519a71fe8cc4df5888ff5"),
"academicyear" : "2016-2017",
"departments" : [
{
"deptname" : "computer",
"semesters" : [
{
"sem" : "2",
"classes" : [
{
"class" : "1",
"theory" : [
{
"subname" : "DS",
"faculty" : [
{
"facultyname" : "KSS",
"facultydept" : "COMP"
}
]
}
],
"practical" : [
{
"subname" : "DS",
"batch" : [
{
"bname" : "C",
"facultyname" : "KSS",
"facultydept" : "COMP"
}
]
}
]
}
]
}
]
}
]
}
]
Now for the same academic year and for the same department I would like to create a new semester node inside the array of semesters. For that I've tried syntax of $ and use of . operator but it did not worked well. How can I add a new semester node inside that semesters array? Also after creating a new semester node, how can I add a new class in an classes array?
it could be done by means of the positional operator $[],
db.collection.update(
{ "academicyear": "2016-2017", "departments.deptname": "computer"},
{ "$push":
{"departments.$[].semesters":
{
"sem" : "2",
"classes" : [ ...
}
}
}
)
It could also be done by indicating the position in the array if known. "departments.1.semesters", where 1 is the position of 'departments' in the array.
db.collection.update(
{ "academicyear": "2016-2017"},
{ "$push":
{"departments.1.semesters":
{
"sem" : "2",
"classes" : [ ...
}
}
}
)
One way of doing it would be:
var toInsert = {"sem" : "2", "classes" : [ ... }
db.collection.update(
{ "academicyear": "2016-2017", departments: { $elemMatch: { deptname: "computer" } } },
{ $addToSet: { 'departments.$.semesters': toInsert } }
)
The query part will find the document from the list array with id = 2. Then we use $ positional element to add a new element at that array index.
and the same would apply for classes.
db.collection.update(
{ "academicyear": "2016-2017"},
{ "$push":
{"departments.1.semesters.0.classes":
{
"class" : "1",
"theory" : [{ ...
}
}
}
)
Another alternative would be to extract the document, update it on the returned object and save it again.
db.collection.findOne({"academicyear": "2016-2017", "departments.deptname": "computer"}, {'departments.semesters': 1}, (err, doc) => {
if(err) return handleError(res)(err);
if(!doc) return res.send(404).end();
//I guess semester is 0, but you can find the one that you are going to update by some filter
doc.departments[0].semesters[0].classes.push({...});
doc.save( (err) => {
if(err) return handleError(res)(err)
return res.status(200).end();
});
});
I am working on an express js application where I need to update a nested array.
1) Schema :
//Creating a mongoose schema
var userSchema = mongoose.Schema({
_id: {type: String, required:true},
name: String,
sensors: [{
sensor_name: {type: String, required:true},
measurements: [{time: String}]
}] });
2)
Here is the code snippet and explanation is below:
router.route('/sensors_update/:_id/:sensor_name/')
.post(function (req, res) {
User.findOneAndUpdate({_id:req.body._id}, {$push: {"sensors" :
{"sensor_name" : req.body.sensor_name , "measurements.0.time": req.body.time } } },
{new:true},function(err, newSensor) {
if (err)
res.send(err);
res.send(newSensor)
}); });
I am able to successfully update a value to the measurements array using the findOneAndUpdate with push technique but I'm failing when I try to add multiple measurements to the sensors array.
Here is current json I get if I get when I post a second measurement to the sensors array :
{
"_id": "Manasa",
"name": "Manasa Sub",
"__v": 0,
"sensors": [
{
"sensor_name": "ras",
"_id": "57da0a4bf3884d1fb2234c74",
"measurements": [
{
"time": "8:00"
}
]
},
{
"sensor_name": "ras",
"_id": "57da0a68f3884d1fb2234c75",
"measurements": [
{
"time": "9:00"
}
]
}]}
But the right format I want is posting multiple measurements with the sensors array like this :
Right JSON format would be :
{
"_id" : "Manasa",
"name" : "Manasa Sub",
"sensors" : [
{
"sensor_name" : "ras",
"_id" : ObjectId("57da0a4bf3884d1fb2234c74"),
"measurements" : [
{
"time" : "8:00"
}
],
"measurements" : [
{
"time" : "9:00"
}
]
}],
"__v" : 0 }
Please suggest some ideas regarding this. Thanks in advance.
You might want to rethink your data model. As it is currently, you cannot accomplish what you want. The sensors field refers to an array. In the ideal document format that you have provided, you have a single object inside that array. Then inside that object, you have two fields with the exact same key. In a JSON object, or mongo document in this context, you can't have duplicate keys within the same object.
It's not clear exactly what you're looking for here, but perhaps it would be best to go for something like this:
{
"_id" : "Manasa",
"name" : "Manasa Sub",
"sensors" : [
{
"sensor_name" : "ras",
"_id" : ObjectId("57da0a4bf3884d1fb2234c74"),
"measurements" : [
{
"time" : "8:00"
},
{
"time" : "9:00"
}
]
},
{
// next sensor in the sensors array with similar format
"_id": "",
"name": "",
"measurements": []
}],
}
If this is what you want, then you can try this:
User.findOneAndUpdate(
{ _id:req.body._id "sensors.sensor_name": req.body.sensor_name },
{ $push: { "sensors.0.measurements": { "time": req.body.time } } }
);
And as a side note, if you're only ever going to store a single string in each object in the measurements array, you might want to just store the actual values instead of the whole object { time: "value" }. You might find the data easier to handle this way.
Instead of hardcoding the index of the array it is possible to use identifier and positional operator $.
Example:
User.findOneAndUpdate(
{ _id: "Manasa" },
{ $push: { "sensors.$[outer].measurements": { "time": req.body.time } } }
{ "arrayFilters:" [{"outer._id": ObjectId("57da0a4bf3884d1fb2234c74")}]
);
You may notice than instead of getting a first element of the array I specified which element of the sensors array I would like to update by providing its ObjectId.
Note that arrayFilters are passed as the third argument to the update query as an option.
You could now make "outer._id" dynamic by passing the ObjectId of the sensor like so: {"outer._id": req.body.sensorId}
In general, with the use of identifier, you can get to even deeper nested array elements by following the same procedure and adding more filters.
If there was a third level nesting you could then do something like:
User.findOneAndUpdate(
{ _id: "Manasa" },
{ $push: { "sensors.$[outer].measurements.$[inner].example": { "time": req.body.time } } }
{ "arrayFilters:" [{"outer._id": ObjectId("57da0a4bf3884d1fb2234c74"), {"inner._id": ObjectId("57da0a4bf3884d1fb2234c74"}}]
);
You can find more details here in the answer written by Neil Lunn.
refer ::: positional-all
--- conditions :: { other_conditions, 'array1.array2.field_to_be_checked': 'value' }
--- updateData ::: { $push : { 'array1.$[].array2.$[].array3' : 'value_to_be_pushed' } }
I have the following schema into my DB:
{ name: 'AFG',
documents: [
{ name: 'doc1',
templates: [
{ name: 'templ1',
lastModified: <Date>},
...]
},
...]
}
I am trying to make a query to look for the template with name 'templ1'. If I find it, I have to compare the last modification date with another and update the value. If I don't find it, I have to push it into the array.
I have tried to do it with the following query:
Country.findOne({name : countrySelected, documents : {$elemMatch: {name: documentSelected}}, 'documents.templates' : {$elemMatch: {name: templateSelected}}}, function(err, templateFound){...}
But I get the following error:
MongoError: cannot use the part (documents of documents.templates) to traverse the element ({documents: [ { name: "Passport", templates: [] }, { name: "Visa", templates: [] }, { name: "Driver's Licence", templates: [] }, { name: "Id Card", templates: [] }, { name: "Other", templates: [] } ]})
Someone knows how can I do it? Thanks!!
You have to use the $in operator to search in arrays.
Country.findOne({
name : countrySelected,
documents: {
$elemMatch: {
$and: [{
name: {
$in: documentSelected //<-- must be array like ['doc1']
}
}, {
'templates.name': {
$in: documentSelected //<-- must be array like ['tmpl1']
}
}]
}
}
}, function(err, templateFound){
//do something with err and templateFound
});
To update the LastModified date you need to update it in the callback function and save the document.
See this answer for nested array updates