When using $mergeObjects in update pipeline, the original object is being
overwritten. Are there any alternatives to avoid this from happening or can $mergeObjects be used in any way to avoid this.
I have a document as follows:
{
"employment": {
"selfEmployed": {
"netProfit": 1000,
"addbacks": [
{
"addbackId": "5a934e000102030405000001",
"value": 1000,
}
]
}
}
}
I have update data as follows:
const update = {
"selfEmployed": {
"netProfit": 22,
}
};
The update query is:
db.collection.update({},
[
{
$set: {
"employment": {
$mergeObjects: [
"$employment",
update
]
}
}
}
])
However $mergeObjects overwrites the object to give following result:
{
"employment": {
"selfEmployed": {
"netProfit": 22
}
}
}
Expected Result:
{
"employment": {
"selfEmployed": {
"netProfit": 22,
"addbacks": [
{
"addbackId": "5a934e000102030405000001",
"value": 1000,
}
]
}
}
}
Playground
Edit 1:
I can only rely on top level field to perform update.
Some answers below do work for selfEmployed.
But I have other fields on the collection as well.
e.g.
{
"employment": {
"selfEmployed": {
"netProfit": 1000,
"addbacks": [
{
"addbackId": "5a934e000102030405000001",
"value": 1000,
}
]
},
"unemployed": {
"reason": "some reason",
//... other data
}
// .. other data
}
}
And i don't want to repeat or write multiple queries for updating nested fields and using direct set is out of the question as well.
I'm looking for a way using the update pipeline as I have given above.
According to $mergeObjects Behaviour,
$mergeObjects overwrites the field values as it merges the documents. If documents to merge include the same field name, the field, in the resulting document, has the value from the last document merged for the field.
Hence, the original employment.selfEmployed will be replaced with the new value.
Solution 1: Direct $set value for employment.selfEmployed.netProfit.
db.collection.update({},
[
{
$set: {
"employment.selfEmployed.netProfit": 22
}
}
])
Sample Mongo Playground for Solution 1
Solution 2: $mergeObjects for employment.selfEmployed only.
db.collection.update({},
[
{
$set: {
"employment.selfEmployed": {
$mergeObjects: [
"$employment.selfEmployed",
{
"netProfit": 22
}
]
}
}
}
])
Sample Mongo Playground for Solution 2
Related
I have array of objects with following structure:
const myArr = [{
'name':'question1',
'grade':6
},
{
'name':'question2',
'grade':7
}]
Question collection:
{
_id:623749f845844e7d273d801c,
questions:[
{
'name':'question1',
'grade':10,
'someInfo':'blabla',
_id:623749f845844e7d273d801m
},
{
'name':'question2',
'grade':10,
'someInfo':'blabla',
_id:623749f845844e7d273d801a
},
{
'name':'question3',
'grade':10,
'someInfo':'blabla',
_id:623749f845844e7d273d801f
}
]
}
I just want to update all objects in array questions in collection which i have provided in myArr array. So the desired result should be:
{
_id:623749f845844e7d273d801c,
questions:[
{
'name':'question1',
'grade':6,
'someInfo':'blabla',
_id:623749f845844e7d273d801m
},
{
'name':'question2',
'grade':7,
'someInfo':'blabla',
_id:623749f845844e7d273d801a
},
{
'name':'question3',
'grade':10,
'someInfo':'blabla',
_id:623749f845844e7d273d801f
}
]
}
What i've found so far:
I managed to update grade when i specify the question name with positional operator:
await Prijave.updateOne(
{
_id: mongoose.Types.ObjectId(q_id),
'questions.name': 'question1',
},
{
$set: {
'questions.$.grade': '10',
},
}
)
But how to do this for every element in myArr ? I could make forEach loop which iterates through myArrand call updateOne for every element but i dont think it should be done that way because it will make multiple connection to database instead of one.
I have the following documents:
[{
"_id":1,
"name":"john",
"position":1
},
{"_id":2,
"name":"bob",
"position":2
},
{"_id":3,
"name":"tom",
"position":3
}]
In the UI a user can change position of items(eg moving Bob to first position, john gets position 2, tom - position 3).
Is there any way to update all positions in all documents at once?
You can not update two documents at once with a MongoDB query. You will always have to do that in two queries. You can of course set a value of a field to the same value, or increment with the same number, but you can not do two distinct updates in MongoDB with the same query.
You can use db.collection.bulkWrite() to perform multiple operations in bulk. It has been available since 3.2.
It is possible to perform operations out of order to increase performance.
From mongodb 4.2 you can do using pipeline in update using $set operator
there are many ways possible now due to many operators in aggregation pipeline though I am providing one of them
exports.updateDisplayOrder = async keyValPairArr => {
try {
let data = await ContestModel.collection.update(
{ _id: { $in: keyValPairArr.map(o => o.id) } },
[{
$set: {
displayOrder: {
$let: {
vars: { obj: { $arrayElemAt: [{ $filter: { input: keyValPairArr, as: "kvpa", cond: { $eq: ["$$kvpa.id", "$_id"] } } }, 0] } },
in:"$$obj.displayOrder"
}
}
}
}],
{ runValidators: true, multi: true }
)
return data;
} catch (error) {
throw error;
}
}
example key val pair is: [{"id":"5e7643d436963c21f14582ee","displayOrder":9}, {"id":"5e7643e736963c21f14582ef","displayOrder":4}]
Since MongoDB 4.2 update can accept aggregation pipeline as second argument, allowing modification of multiple documents based on their data.
See https://docs.mongodb.com/manual/reference/method/db.collection.update/#modify-a-field-using-the-values-of-the-other-fields-in-the-document
Excerpt from documentation:
Modify a Field Using the Values of the Other Fields in the Document
Create a members collection with the following documents:
db.members.insertMany([
{ "_id" : 1, "member" : "abc123", "status" : "A", "points" : 2, "misc1" : "note to self: confirm status", "misc2" : "Need to activate", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") },
{ "_id" : 2, "member" : "xyz123", "status" : "A", "points" : 60, "misc1" : "reminder: ping me at 100pts", "misc2" : "Some random comment", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") }
])
Assume that instead of separate misc1 and misc2 fields, you want to gather these into a new comments field. The following update operation uses an aggregation pipeline to:
add the new comments field and set the lastUpdate field.
remove the misc1 and misc2 fields for all documents in the collection.
db.members.update(
{ },
[
{ $set: { status: "Modified", comments: [ "$misc1", "$misc2" ], lastUpdate: "$$NOW" } },
{ $unset: [ "misc1", "misc2" ] }
],
{ multi: true }
)
Suppose after updating your position your array will looks like
const objectToUpdate = [{
"_id":1,
"name":"john",
"position":2
},
{
"_id":2,
"name":"bob",
"position":1
},
{
"_id":3,
"name":"tom",
"position":3
}].map( eachObj => {
return {
updateOne: {
filter: { _id: eachObj._id },
update: { name: eachObj.name, position: eachObj.position }
}
}
})
YourModelName.bulkWrite(objectToUpdate,
{ ordered: false }
).then((result) => {
console.log(result);
}).catch(err=>{
console.log(err.result.result.writeErrors[0].err.op.q);
})
It will update all position with different value.
Note : I have used here ordered : false for better performance.
I have this structure in my Mother Model (this is a fixed structure and I just push cards or update them on these 3 array levels):
{
cards: {
starter: [],
intermediate: [],
advanced: [ {Object}, {Object}, {Object} ]
},
}
The Objects inside cards.advanced array above are like:
{
cards: [
{ // this is a single card object
title: 'this is a card',
id: 'main-2-1' // this is unique id only in advanced array, we may have exact id for a card in starter or in intermediate array
}
],
unit: 2 // this is the unit
}
Assuming I have access to Mother model like this:
const motherModel = await db.Mother.findOne({}); // this retrieves all data in the Model
How can we update a card object based on its id and the level it belongs to and replace the whole card object with newCard ?
const level = 'advanced'; // the level of the card we want to search for
const cardID = 'main-2-1'; // the exact id of the card we want to be replaced
const cardUnit = cardID.split('-')[1]; // I can calculate this as the unit in which the card exist inside
const newCard = { // new card to be replaced
title: 'this is our new updated card',
id: 'main-2-1'
}
I have tried this with no luck:
const updated = await db.Mother.update(
{ ["cards." + level + ".unit"]: cardUnit },
{ ["cards." + level + ".$.cards"]: newCard }
)
I have tried this one too but it doesn't change anything in the Model:
async function updateMotherCard(card, level) {
const cardID = card.id;
const cardUnit = cardID.split('-')[1];
const motherModel = await db.Mother.findOne({});
const motherLevel = motherModel.cards[level];
const selectedUnit = motherLevel.find(e => e.unit == cardUnit);
let selectedCard = selectedUnit.cards.find(e => e.id == cardID);
selectedCard = card;
const updated = await motherModel.save();
console.log(updated);
}
You can actually sort your problem out with the update method, but you have to do it in a different way if you are using MongoDB 4.2 or later. The second parameter can be the $set operation you want to perform or an aggregation pipeline. Using the later you have more liberty shaping the data. This is the way you can solve your problem, I will breakdown after:
db.collection.update({
"cards.advanced.unit": 2
},
[
{
$set: {
"cards.advanced": {
$map: {
input: "$cards.advanced",
as: "adv",
in: {
cards: {
$map: {
input: "$$adv.cards",
as: "advcard",
in: {
$cond: [
{
$eq: [
"$$advcard.id",
"main-2-1"
]
},
{
title: "this is a NEW updated card",
id: "$$advcard.id"
},
"$$advcard"
]
}
}
},
unit: "$$adv.unit"
}
}
}
}
}
],
{
new: true,
});
First with use the update method passing three parameters:
Filter query
Aggregation pipeline
Options. Here I just used new: true to return the updated document and make it easier to test.
This is the structure:
db.collection.update({
"cards.advanced.unit": 2
},
[
// Pipeline
],
{
new: true,
});
Inside the pipeline we only need one stage, the $set to replace the property advanced with an array we will create.
...
[
{
$set: {
"cards.advanced": {
// Our first map
}
}
}
]
...
We first map the advanced array to be able to map the nested cards array after:
...
[
{
$set: {
"cards.advanced": {
$map: {
input: "$cards.advanced",
as: "adv",
in: {
// Here we will map the nested array
}
}
}
}
}
]
...
We use the variable we declared on the first map and which contains the advanced array current item being mapped ( adv ) to access and map the nested "cards" array ( $$adv.cards ):
...
[
{
$set: {
"cards.advanced": {
$map: {
input: "$cards.advanced",
as: "adv",
in: {
cards: {
$map: {
input: "$$adv.cards",
as: "advcard",
in: {
// We place our condition to check for the chosen card here
}
}
},
unit: "$$adv.unit",
}
}
}
}
}
]
...
Lastly we check if the current card id is equal to the id being searched $eq: [ "$$advcard.id", "main-2-1" ] and return the new card if it matches or the current card:
...
{
$cond: [
{
$eq: [
"$$advcard.id",
"main-2-1"
]
},
{
title: "this is a NEW updated card",
id: "$$advcard"
},
"$$advcard"
]
}
...
Here is a working example of what is described:
https://mongoplayground.net/p/xivZGNeD8ng
I am using the following query to update the documents in mongodb but it throws me the error The dollar ($) prefixed field '$subtract' in 'abc.$subtract' is not valid for storage.
const bulkUpdate = arrs.map(arr => {
const { val = 0 } = arr
return {
updateMany: {
filter: {
date: 20201010
},
update: {
$set: {
abc: {
$subtract: [val, { $add: [{ $ifNull: ['$a1', 0] }, { $ifNull: ['$b1', 0] } ] }]
}
},
},
},
}
})
if (bulkUpdate.length > 0) {
return mongoConnection.pool
.db('test')
.collection('testTable')
.bulkWrite(bulkUpdate)
}
Thanks is advance
$subtract and $ifNull are not update operators, hence you can't use them within an update (except a within a pipelined update).
If you're using Mongo version 4.2+ you can use use a pipeline update instead of a "document update" like so:
{
updateMany: {
filter: {
date: 20201010
},
update: [
{
$set: {
abc: {
$subtract: [val, {$add: [{$ifNull: ['$a1', 0]}, {$ifNull: ['$b1', 0]}]}]
}
}
}
]
}
}
If you aren't then you'll have to read each document and update it sepratley as prior to version 4.2 you could not access document fields while updating as you are trying to do.
I want to calculate the average in .js file and insert the result of all documents inside a collection in Mongo DB, but code below doesn't work.
db.createCollection("myCol");
db.myCol.insert( { item: "card", qty: 15 } );
db.myCol.insert( { item: "card", qty: 92 } );
var mean = db.myCol.aggregate(
[
{
$group:
{
_id: "$card",
average: { $avg: "$qty" }
}
}
]
);
db.myCol.updateMany(
{ "item": { $eq: "card" } },
{
$set: { "mean" : mean },
}
)
I run .js files in Mongo Shell with command
load (jsfile.js)
db.createCollection("myCol");
db.myCol.insert( { item: "card", qty: 15 } );
db.myCol.insert( { item: "card", qty: 92 } );
var mean = db.myCol.aggregate(
[
{
$group:
{
_id: "$item",
average: { $avg: "$qty" }
}
}
]
).toArray();
db.myCol.updateMany(
{ "item": { $eq: "card" } },
{
$set: { "mean" : mean },
}
)
you should try this one. It will work.
Problem
There are few operations which return cursor objects. In your case, the cursor object for aggregation query was storing at mean variable. That's the reason you were not getting your desired value instead a cursor object was getting updated in myCol collection.
Solution
I have updated the code with toArray method, which converts cursor to an array of objects.So this will work for sure.
let me know if its help you.