The following code works great in that it updates the object in the nested array.
However, I'm struggling to find a way to push a new object (Ex. {"locale" : "ar" , value:"مرحبا"}) if locale does not exist or update value if locale already exists (Ex. {"locale" : "en" , value:"hello"})
Update code:
Project.findOneAndUpdate(
{_id:projectId, 'sections._id': sectionId},
{ "$set": { "sections.$.subheader": {"locale":args.lang,"value":args.title} }},
{ upsert : true, new: true, useFindAndModify: false },
(err, section) => {
}
)
Object structure:
"project": {
"name": "project name",
"sections": [
{
"subheader": [{
'locale' : "en",
'value' : "Helle"
},
{
'locale' : "fr",
'value' : "salut"
}]
}
]
}
Unfortunately, this is not possible to do in one go. The upsert option only works on objects in the collection, not on nested objects.
You could solve this by first trying to update the element in the array, then check if the object in the nested array was matched. If there was no match, you can insert it into the nested array using $addToSet.
Additionally, you need to use positional operators to match the nested arrays:
Project.findOneAndUpdate(
// match item in subheader array
{ _id: projectId, 'sections._id': sectionId, 'sections.subheader.locale': args.lang },
// update existing item in subheader array
{ "$set": { "sections.$[section].subheader.$[subheader].value": args.title } },
// we use arrayFilters here, don't use upsert now
{ arrayFilters: [{ 'section._id': sectionId }, { 'subheader.locale': args.lang }], useFindAndModify: false },
(err, section) => {
// check if section was found
if (!section) {
// add new object to array if it wasn't found yet
Project.findOneAndUpdate(
// match section
{ _id: projectId, 'sections._id': sectionId},
// add new object to array
{ "$addToSet": { "sections.$.subheader": {"locale": args.lang,"value": args.title } }},
(err, section) => {
console.log('created new locale')
}
)
} else {
console.log('updated existing locale')
}
}
)
Related
I have a Schema that holds an array of objects for comments and I would like to update the boolean value of the flagged comments accordingly, I have tried updateOne and aggregate but it isn't working out at this point, I have also tried to use $elemMatch but it isn't working.
The comment _id is being pulled from the front end element that has an ID that is the same as the id that needs to be pulled from MongoDB.
Comments Array within the question Schema:
comments: [
{
user: {
type: Object,
},
commentDate: {
type: Date,
default: Date.now()
},
flagged: {
type: Boolean,
default: false
},
flaggedDate:{type: Date},
comment: String,
}
],
function I tried to run last.
const id = req.params.id
const updateFlag = Question.updateOne(
{
comments: [
{
_id: id
}
]
},
{
$set: {
comments: [
{
flagged: req.body.flagged
}
]
}
}
)
Any help would be appreciated!
You can do it with positional operator - $:
db.collection.update({
"comments._id": "3"
},
{
"$set": {
"comments.$.flagged": true
}
})
Working example
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.
So I tried several ways, but I can't, I can modify several objects with the same key but I can't modify any with different keys, if anyone can help me is quite a complex problem
{
id: 123,
"infos": [
{ name: 'Joe', value: 'Disabled', id: 0 },
{ name: 'Adam', value: 'Enabled', id: 0 }
]
};
In my database I have a collection with an array and several objects inside which gives this.
I want to modify these objects, filter by their name and modify the value.
To give you a better example, my site returns me an object with the new data, and I want to modify the database object with the new object, without clearing the array, the name key never changes.
const object = [
{ name: 'Joe', value: 'Hey', id: 1 },
{ name: 'Adam', value: 'None', id: 1 }
];
for(const obj in object) {
Schema.findOneAndUpdate({ id: 123 }, {
$set: {
[`infos.${obj}.value`]: "Test"
}
})
}
This code works but it is not optimized, it makes several requests, I would like to do everything in one request, and also it doesn't update the id, only the value.
If anyone can help me that would be great, I've looked everywhere and can't find anything
My schema structure
new Schema({
id: { "type": String, "required": true, "unique": true },
infos: []
})
I use the $addToSet method to insert objects into the infos array
Try This :
db.collection.update({
id: 123,
},
{
$set: {
"infos.$[x].value": "Value",
"infos.$[x].name": "User"
}
},
{
arrayFilters: [
{
"x.id": {
$in: [
1
]
}
},
],
multi: true
})
The all positional $[] operator acts as a placeholder for all elements in the array field.
In $in you can use dynamic array of id.
Ex :
const ids = [1,2,..n]
db.collection.update(
//Same code as it is...
{
arrayFilters: [
{
"x.id": {
$in: ids
}
},
],
multi: true
})
MongoPlayGround Link : https://mongoplayground.net/p/Tuz831lkPqk
Maybe you look for something like this:
db.collection.update({},
{
$set: {
"infos.$[x].value": "test1",
"infos.$[x].id": 10,
"infos.$[y].value": "test2",
"infos.$[y].id": 20
}
},
{
arrayFilters: [
{
"x.name": "Adam"
},
{
"y.name": "Joe"
}
],
multi: true
})
Explained:
You define arrayFilters for all names in objects you have and update the values & id in all documents ...
playground
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