Update multiple elements in an array in mongodb [duplicate] - arrays

This question already has answers here:
How to Update Multiple Array Elements in mongodb
(16 answers)
Closed 4 years ago.
I have a document like following in my stocks collection in mongodb.
{ _id: 'xRMuRBhqRLgASyQyW',
History:
[ { orderId: '12032017001877',
status: 'COMPLETED',
},
{ orderId: '22122017002450',
status: 'PROCESSED',
},
{ orderId: '22122018002450',
status: 'DEPOSIT',
}
]
}
I want to iterate through all document in stocks collection and add a field flag: true if status is not 'PROCESSED'.

You need to use the all $[] positional operator to update each element in the array
db.collection.update(
{ "History": { "$elemMatch": { "status": { "$ne": "PROCESSED" } } } },
{ "$set": { "History.$[].flag": false } },
{ "multi": true }
)

You can achieve it using script.
db.stocks.find().forEach(function (e) {
var modified = false;
e.History.forEach(function (o) {
if (o.status != "PROCESSED") {
o.flag = true;
modified = true;
}
});
if (modified) {
db.stocks.save(e);
}
});

You can simply do it by using '$' operator. Something like:
db.stocks.update( {"history.status" : { $not: "PROCESSED" } ,
{ $set: {"history.$.flag": true }} ,
false ,
true);

My answer is very similar #Anthony but
a two extra parameter( for upsert and multi) are added. For reference You can check official document.
db.stocks.update(
{ "History": { "$elemMatch": { "status": { "$ne": "PROCESSED" } } } },
{ "$set": { "History.$[].flag": false }},
false, // show that if there is not any document with specified criteria , it will not create a new document.
ture // according to document of mongodb if multi is true then more than one document will be updated, other wise only one document will be updated.
)

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

adding data to nested document : mongodb

I want to add new record within 'music'. My document looks similar to
{
"username": "erkin",
"email": "erkin-07#hotmail.com",
"password": "b",
"playlists": [
{
"_id": 58,
"name": "asdsa",
"date": "09-01-15",
"musics": {
"song-one":{
"song-one": "INNA - Cola Song (feat. J Balvin)",
"duration": "3.00"
},
"song-two":{
"song-two": "blabla",
"duration": "3.00"
}
}
}
]
}
After navigating to "music" and then using $set to add/update multiple records at once. But new records is getting added in lexicographical manner(behaviour of $set).
I'm using query similar to (so in my case song-four is coming before song-three) :
db.getCollection('myCollection').update({username:"erkin"},{$set:{"playlists.musics.song-three":{...},"playlists.musics.song-four":{...}}})
Is there any way I can add new records to that location in a way my $set query is arranged ?
As playlists is an array:
Option 1: Update the first document in playlists.
Specify the index: 0 for the first document in playlists.
playlists.0.musics.song-three
db.collection.update({
username: "erkin"
},
{
$set: {
"playlists.0.musics.song-three": {
"song-three": "song-three"
},
"playlists.0.musics.song-four": {
"song-four": "song-four"
}
}
})
Sample Demo on Mongo Playground (Option 1)
Option 2: Update all documents in playlists.
With $[] all positional operator.
playlists.$[].musics.song-three
db.collection.update({
username: "erkin"
},
{
$set: {
"playlists.$[].musics.song-three": {
"song-three": "song-three"
},
"playlists.$[].musics.song-four": {
"song-four": "song-four"
}
}
})
Sample Demo on Mongo Playground (Option 2)
Option 3: Update specified document in playlists.
With $[<identifier>] filtered positional operator.
playlists.$[playlist].musics.song-three
db.collection.update({
username: "erkin"
},
{
$set: {
"playlists.$[playlist].musics.song-three": {
"song-three": "song-three"
},
"playlists.$[playlist].musics.song-four": {
"song-four": "song-four"
}
}
},
{
arrayFilters: [
{
"playlist._id": 58
}
]
})
Sample Demo on Mongo Playground (Option 3)

$unset on multiple fields in mongodb

Suppose I have a collection in mongoDB like given below -
{
name : "Abhishek",
Roll_no : null,
hobby : stackoverflow
},
{
name : null,
Roll_no : 1,
hobby : null
}
Now I want to delete the fields in my Documents where the field values are null. I know that I can do it using the $unset in following way -
db.collection.updateMany({name: null}, { $unset : { name : 1 }});
And we could do it in the same way for hobby and name field.
But I was wondering if I can do the same deletion operation using just one query? I was wondering if maybe I could use $or or something else to achieve the same effect but in a single command.
Any ideas?
On MongoDB version >= 3.2 :
You can take advantage of .bulkWrite() :
let bulkArr = [
{
updateMany: {
filter: { name: null },
update: { $unset: { name: 1 } }
}
},
{
updateMany: {
filter: { Roll_no: null },
update: { $unset: { Roll_no: 1 } }
}
},
{
updateMany: {
filter: { hobby: null },
update: { $unset: { hobby: 1 } }
}
},
];
/** All filter conditions will be executed on all docs
* but respective update operation will only be executed if respective filter matches (kind of individual ops) */
db.collection.bulkWrite(bulkArr);
Ref : bulkwrite
On MongoDB version >= 4.2 :
Since you wanted to delete multiple fields(where field names can't be listed down or unknown) having null value, try below query :
db.collection.update(
{}, // Try to use a filter if possible
[
/**
* using project as first stage in aggregation-pipeline
* Iterate on keys/fields of document & remove fields where their value is 'null'
*/
{
$project: {
doc: {
$arrayToObject: { $filter: { input: { $objectToArray: "$$ROOT" }, cond: { $ne: ["$$this.v", null] } } }
}
}
},
/** Replace 'doc' object as root of document */
{
$replaceRoot: { newRoot: "$doc" }
}
],
{ multi: true }
);
Test : mongoplayground
Ref : update-with-an-aggregation-pipeline , aggregation-pipeline
Note :
I believe this would be one time operation & in future you can use Joi npm package or mongoose schema validators to restrict writing null's as field values. If you can list down your field names as if not too many plus dataset size is way too high then try to use aggregation with $$REMOVE as suggested by '#thammada'.
As of now, aggregation-pipeline in .updateMany() is not supported by many clients even few mongo shell versions - back then my ticket to them got resolved by using .update(), if it doesn't work then try to use update + { multi : true }.
With MongoDB v4.2, you can do Updates with Aggregation Pipeline, along with the $$REMOVE system variable
db.collection.updateMany({
$or: [{
name: null
}, {
Roll_no: null
}, {
hobby: null
}]
}, [{
$set: {
name: { $ifNull: ["$name", "$$REMOVE"] }
Roll_no: { $ifNull: ["$Roll_no", "$$REMOVE"] },
hobby: { $ifNull: ["$hobby", "$$REMOVE"] }
}
}]

Push object into array if exists otherwise set object in MongoDB

This is the document I currently have:
{
"_id": "",
"title": "My Watchlist",
"series": [{
"seriesId": 1,
"following": true,
"seasons": []
}, {
"seriesId": 1,
"following": false,
"seasons": []
}]
}
As you can see there are currently 2 objects with the seriesId 1, but with a different following boolean.
If the query matches with _id it should push the new object into series, if within the "series"-array an object with the same "seriesId" already exists it should change the fields within that object, instead of adding a new object.
I currently have the following query:
users.update(
{"_id": req.body.userId},
{
"$push": {
"series": {"seriesId": req.body.seriesId, "following": req.body.following}
}
}, (err, data) => {
if (err)
next(err);
});
If I use $set it does not add the object if it didn't originaly exist yet, and as far as I know you cannot both use $push and $set?
Can this be fixed in any way or do I have to rethink my Schema?
You can use two update query :
if _id is found and seriesId is not in the array, add the new item to the array :
db.series.update({
"_id": req.body.userId,
"series": {
"$not": {
"$elemMatch": {
"seriesId": req.body.seriesId
}
}
}
}, {
$addToSet: {
series: {
"seriesId": req.body.seriesId,
"following": req.body.following,
"seasons": []
}
}
}, { multi: true });
if _id is found and seriesId is found in the array, update the array item :
db.series.update({
"_id": req.body.userId,
"series.seriesId": req.body.seriesId
}, {
$set: {
"series.$.following": req.body.following
}
}, { multi: true });

Insert object into MongoDB array element

I have to insert an object in every array in the MongoDB document. The items array displayed below has itemList; in every itemList I have to insert a itemSpec. The desired document shape before and after the process is shown below:
Before process
{
"items": [
{
"itemList":{
"rejected": true
},
"fProcBy": "automatic"
},
{
"itemList":{
"rejected": true
},
"fProcBy": "automatic"
}
]
}
After process:
{
"items": [
{
"itemList":{
"rejected": true
},
"itemSpec":{
"approved": true
},
"fProcBy": "automatic"
},
{
"itemList":{
"rejected": true
},
"itemSpec":{
"approved": true
},
"fProcBy": "automatic"
}
]
}
So in each element of the items array there has to be inserted a new object property itemSpec.
I am not aware of a solution which does it in a single run, but with the following simple script the maximal number of iterations is equal to the maximal number of array elements you have in a single documents, which is probably not so high (but it's just a guess as I have no further information about your data):
var q = { "items.itemSpec": null };
var u = {
$set: {
"items.$.itemSpec" : {
"approved": true
}
}
};
var yourColl = db.getCollection('fooBar');
while (yourColl.find(q).count() > 0) {
yourColl.update(q, u, { multi: true });
}
Use $push operator to update array in a document.
Try This:
db.test.update({"_id":"yourUniqueId"},
{$push: { "items":{ "itemSpec":{"approved": true}}}});

Resources