How to access value in object in array Mongodb findAndModify - arrays

How do I update a value inside a object inside an array. I will provide the exact array number in a variable... Heres my code:
var num = 0;
var ObjectID=require('mongodb').ObjectID;
db.collection('polls').findAndModify({
query: {_id: ObjectID(_id param)},
update: { $inc: { total: 1, "Data.chart." + num.toString + ".value": 1} }
});
This is what I want to update
{total: 0, "Data": [
{ value: 0, label: 'Beatles', color: '#4169E1' },
{ value: 0, label: 'Sting', color: '#C0C0C0' },
{ value: 0, label: 'Police', color: '#FFA500' },
{ value: 0, label: 'Journey', color: '#FF4500' },
{ value: 0, label: 'Genesis', color: '#EE82EE' }
]
}

You seem to have a subsection, chart in the query that does not exist in the actual data to update. Just eliminate that;
db.collection('polls').findAndModify({
query: {_id: ObjectID(_id param)},
update: { $inc: { total: 1, "Data." + num.toString + ".value": 1} }
});
turns the query into
db.polls.update({}, { $inc: { total: 1, "Data.2.value": 1} })
resulting in
db.polls.find().pretty()
{
"_id" : ObjectId("56cbebb6ef788d178e2dfdc0"),
"total" : 1,
"Data" : [
...
{
"value" : 1,
"label" : "Police",
"color" : "#FFA500"
},
...

JavaScript "stringifies" presented keys, so this is why you are not getting what you expect.
To use a variable as part of a "calculated key name" you need to use the bracket [] notation to define the object:
var num = 0;
var update = { "$inc": { "total": 1 } };
update["$inc"]["Data." + num + ".value"] = 1;
db.collection('polls').findAndModify({
query: { _id: ObjectID(_id param) },
update: update
});
Better yet, you should not presume the "index" that you think you want to modify is the actual location of the data. Instead match the array element in the query and use the positional $ operator instead of a hard index:
db.collection('polls').findAndModify({
query: { "_id": ObjectID(_idParam), "Data.label": 'Beatles' },
update: { "$inc": { "total": 1, "Data.$.value": 1 } }
});
So the element that matches Data.label as "Beatles" will be the one updated, and regardless of the current index position in the array.
That means "scalable", where other writes could alter the array content but you are still updating the one you want, even if it changed position due to another update.
N.B The document in your question has the path to array elements as Data and not Data.chart. But apply however your real data is contructed.

Related

Mongo updateMany statement with an inner array of objects to manipulate

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

MongoDB - AND request on array with GeoJSON element

I've looked at other issues without finding a suitable answer for my problem, so I hope you may help me.
SITUATION
I am currently working on a localisation app with MongoDB. For the moment I test my queries in the MongoDB shell.
I have a collection of Point documents with locations. I use GeoJSON objects for the coordinates of my point.
Here is what a Point document looks like :
{
_id: "Roma_DellArte",
localisations: [
{ location_type_id: 1, location: { type: "Point", coordinates: [ 41.9097306,12.2558141 ] } }
]
}
My location_type_id refers to another collection, juste for you to know.
I already made a query which gets me all the points near a precise location :
db.point.createIndex({ 'localisations.location': "2dsphere" })
db.point.find({
'localisations.location': {
$near: {
$geometry: { type: "Point", coordinates: [ 48.8588377, 2.2770207 ] },
$minDistance: 0,
$maxDistance: 100000
}
}
})
Now I would like to query all the points which are near a precise location AND those with a specific location_type_id.
TRIES AND FAILS
I tried many queries in the MongoDB shell but none of them produced a satisfying result.
Query 1
I think it doesn't return anything because location: isn't an exact field.
db.point.createIndex({ location: "2dsphere" })
db.point.find({
'localisations': {
location_type_id: 1,
location: {
$near: {
$geometry: { type: "Point", coordinates: [ 48.8588377, 2.2770207 ] },
$minDistance: 0,
$maxDistance: 100000
}
}
}
})
Query 2
The main problem here is it gets me a point document if in the localisations field there is an object with a correct location_type_id and an object with the correct location. It is not necessarily the same object, which I want it to be.
see: https://docs.mongodb.com/manual/tutorial/query-array-of-documents/#combination-of-elements-satisfies-the-criteria - second paragraph at the end.
db.point.createIndex({ 'localisations.location': "2dsphere" })
db.point.find({
'localisations.location_type_id': 1,
'localisations.location': {
$near: {
$geometry: { type: "Point", coordinates: [ 48.8588377, 2.2770207 ] },
$minDistance: 0,
$maxDistance: 100000
}
}
})
Query 3
I wanted to try this method : https://docs.mongodb.com/manual/tutorial/query-arrays/#query-for-an-array-element-that-meets-multiple-criteria
db.point.createIndex({ location: "2dsphere" })
db.point.find({
localisations: {
$elemMatch: {
location_type_id: 1,
location: {
$near: {
$geometry: { type: "Point", coordinates: [ 48.8588377, 2.2770207 ] },
$minDistance: 0,
$maxDistance: 100000
}
}
}
}
})
Unfortunately, I get this error:
Error: error: {
"ok" : 0,
"errmsg" : "geoNear must be top-level expr",
"code" : 2,
"codeName" : "BadValue"
}
Now you know everything, I hope you can help me.

Move an element from one array to another within same document MongoDB

I have data that looks like this:
{
"_id": ObjectId("4d525ab2924f0000000022ad"),
"array": [
{ id: 1, other: 23 },
{ id: 2, other: 21 },
{ id: 0, other: 235 },
{ id: 3, other: 765 }
],
"zeroes": []
}
I'm would like to to $pull an element from one array and $push it to a second array within the same document to result in something that looks like this:
{
"_id": ObjectId("id"),
"array": [
{ id: 1, other: 23 },
{ id: 2, other: 21 },
{ id: 3, other: 765 }
],
"zeroes": [
{ id: 0, other: 235 }
]
}
I realize that I can do this by doing a find and then an update, i.e.
db.foo.findOne({"_id": param._id})
.then((doc)=>{
db.foo.update(
{
"_id": param._id
},
{
"$pull": {"array": {id: 0}},
"$push": {"zeroes": {doc.array[2]} }
}
)
})
I was wondering if there's an atomic function that I can do this with.
Something like,
db.foo.update({"_id": param._id}, {"$move": [{"array": {id: 0}}, {"zeroes": 1}]}
Found this post that generously provided the data I used, but the question remains unsolved after 4 years. Has a solution to this been crafted in the past 4 years?
Move elements from $pull to another array
There is no $move in MongoDB. That being said, the easiest solution is a 2 phase approach:
Query the document
Craft the update with a $pull and $push/$addToSet
The important part here, to make sure everything is idempotent, is to include the original array document in the query for the update.
Given a document of the following form:
{
_id: "foo",
arrayField: [
{
a: 1,
b: 1
},
{
a: 2,
b: 1
}
]
}
Lets say you want to move { a: 1, b: 1 } to a different field, maybe called someOtherArrayField, you would want to do something like.
var doc = db.col.findOne({_id: "foo"});
var arrayDocToMove = doc.arrayField[0];
db.col.update({_id: "foo", arrayField: { $elemMatch: arrayDocToMove} }, { $pull: { arrayField: arrayDocToMove }, $addToSet: { someOtherArrayField: arrayDocToMove } })
The reason we use the $elemMatch is to be sure the field we are about to remove from the array hasn't changed since we first queried the document. When coupled with a $pull it also isn't strictly necessary, but I am typically overly cautious in these situations. If there is no parallelism in your application, and you only have one application instance, it isn't strictly necessary.
Now when we check the resulting document, we get:
db.col.findOne()
{
"_id" : "foo",
"arrayField" : [
{
"a" : 2,
"b" : 1
}
],
"someOtherArrayField" : [
{
"a" : 1,
"b" : 1
}
]
}

Count an array inside an array and then sum them MongoDB NodeJS

I have an object that has an array of page objects and each page object has an array of questions.
Ex object:
{
Id: 1,
UserId: 14,
Deleted: false,
Collaborators: [],
Title: "Awesome",
Pages: [{
Id: 1,
Title: 'Jank',
Questions: [
{ Id: 1, Content: 'Ask me about it' },
{ Id: 2, Content: 'Ask me about it again' }
]
}, {
Id: 2,
Title: 'Janker',
Questions: [
{ Id: 1, Content: 'Tell me about it' },
{ Id: 2, Content: 'Tell me about it again' }
]
}]
}
What I am trying to do is to get a count of all the questions for the entire bas object. I am not sure how to do that. I have tried to use aggregate and $sum the total questions and then do another function to $sum those all together to get a total for the entire object. Unfortunately my $sum is not working like I thought it would.
Ex code (nodejs):
var getQuestionCount = function(id) {
var cursor = mongo.collection('surveys').aggregate([{
$match: {
$or: [{
"UserId": id
}, {
"Collaborators": {
$in: [id]
}
}]
}
}, {
$match: {
"Deleted": false
}
}, {
$unwind: "$Pages"
},
{ $group: { _id: null, number: { $sum: "$Pages.Questions" } } }
], function(err, result) {
//This log just gives me [object Object], [object Object]
console.log('q count ' + result);
});
}
Any idea how to do this? My end result from the example object above would ideally return 4 as the question count for the whole object.
I'd try following shell query.
db.collection.aggregate([
// filter out unwanted documents.
{$match:{Id: 1}},
// Unwind Pages collection to access Questions array
{$unwind:"$Pages"},
// Count items in Questions array
{$project:{count: {$size:"$Pages.Questions"}}},
// Finally sum items previously counted.
{$group:{_id:"$_id", total: {$sum: "$count"}}}
])
Based on your sample document, it should return correct count of Questions.
{
"_id" : ObjectId("57723bb8c10c41c41ff4897c"),
"total" : NumberInt(4)
}

Update embedded mongoose document in array

Lets say that I have the following document in the books collection:
{
_id:0 ,
item: "TBD",
stock: 0,
info: { publisher: "1111", pages: 430 },
tags: [ "technology", "computer" ],
ratings: [ { _id:id1, by: "ijk", rating: 4 }, {_id:id2 by: "lmn", rating: 5 } ],
reorder: false
}
I would like to update the value of ratings[k].rating and all I know is the id of the collection and the _id of the objects existing in the array ratings.
The tutorial of mongoDB has the following example that uses the position of the object inside the array but I suppose that if the update can only be done by knowing the position, this means that I firstly have to find the position and then proceed with the update? Can I do the update with only one call and if so how I can do that?
db.books.update(
{ _id: 1 },
{
$inc: { stock: 5 },
$set: {
item: "ABC123",
"info.publisher": "2222",
tags: [ "software" ],
"ratings.1": { by: "xyz", rating: 3 }
}
}
)
Sorry for late answer; I think this is what you want to do with mongoose.
Books.findOneAndUpdate({
_id: 1,
'ratings._id': id1
},
{
$set: {
'ratings.$.rating' : 3
}
}, function(err, book){
// Response
});
Positional operator may help you:
db.books.update(
// find book by `book_id` with `rating_id` specified
{ "_id": book_id, "ratings._id": rating_id },
// set new `value` for that rating
{ $set: { 'ratings.$.rating': value }}
);
$ will save position of matched document.

Resources