Let's assume I have a structure as;
{
name: "",
surname: "",
id: 1,
children: [
{id: 2, name: "", age: ""},
{id: 3, name: "", age: ""}
]
}
I want to add new children or update the existing children fields according to situation.
To add new children into the list, I wrote query which is;
db.collection.update({id: 1}, {
$push: {
children: {
name: "alex",
age: 12,
id : shortid.generate()
}
}
}, {
upsert:true
},function(err, result) {
}
Whether it is a insert or update, I should update the document with id 1. So I think about combining these 2 situations into 1 query.
If I want to add children to the children array I should use $push, otherwise if it is an update I should update the fields of children object. So by using $cond I wrote a query like this;
db.collection.update({id: 1},
{
$cond : {
if: {id: {$exists: 21}} ,
then: {
$set: {
children: { name: "new name", age: "new age"}
}
},
else: {
$push: {
children: {
name: "alex",
age: 12,
id : 21
}
}
}
}, {
upsert:true
},function(err, result){}
And I see it is not possible to have such query, I got an error. Was it too unrealistic? Should I take this two situation separately? I think it was a good idea because I update the same document with id:1 whether it's a update or insert. The only change is deciding whether I will set the fields or create and push them into the children array. What do you suggest for this situation? And yes, I admit, I'm trying to get some experience with mongodb and $cond.
Your first solution is correct and does what you want it to do. The upsert option tells mongo to check if the document exists or not. Mongo will then either insert the doc if it's not there or update an existing doc with the new values it a doc with the given id exists. You need not to do anything else.
Related
Suppose a I have a collection of document structure.
{
uuid: "157071f4-2624-4c49-8735-bd345425a16b",
price: {
item1: 20,
item2: 30
}
}
Now I want to upsert data based on uuid, using something like
await Coll.findOneAndUpdate({uuid}, {$set: {[`price.${itemNo}`]: 40}, {upsert: true}}
My issue is that $set here update values even in cases where itemNo already exists. And if I put a check on query, something like,
{uuid, [`price.${itemNo}`]: {$exists: 0}}
then it creates a new document due to upsert even if uuid exists.
My end goal is to achieve upsert if uuid doesn't exist and and if uuid exists then don't update the value for "price.item" if it also exists else set the value.
Some thing like $setOnInsert also doesn't meet the requirement I only need to update the value.
Thanks in advance.
Got the solution.
We can achieve this by first defining our update field something like:
let fieldName = `price.${itemNo}`
Then doing something like:
await Coll.findOneAndUpdate({
uuid
},
[{
$set: {
[fieldName]: {
$cond: [{
$or: [
{$ne: ["$price", null]},
{$ne: [`$${fieldName}`, null]}
]
}, valueToBeSet, `$${fieldName}`]
}
}
}], {upsert: true}
I have 3 unique documents in mydb, I want to update each document using there unique keys that are generated randomly.
I tried updateMany() and update({multi:true}). Using upadteMany() we can update the document that matches the query. Is there any way that I can solve this problem?
Thanks in advance.
If I have the following documents:
doc1 : {
_id: 1,
name: 'John Smith',
age: 20
}
doc2 : {
_id: 2,
name: 'Jane Smith',
age: 22
}
I want the client to be able to pass me both docs in the same request for update. In this case maybe add an address to both docs.
Is there a way the I can send one update statement to mongo such that it updates both of the documents with the name values?
the answer you provided is good. but i modified that code and it looks like
_id: { $in: [req.params.noteId,req.params.noteId,req.params.noteId]}
}
Note.updateMany(query,
{$set:{
lname: req.body.lname || "Untitled Note",
age: req.body.age
}}
, { new: true }
)
this code is not working
From the below code you can solve your problem .
//consider the following object to update
doc1 : {
_id: 1,
name: 'John Smith',
age: 20
}
doc2 : {
_id: 2,
name: 'Jane Smith',
age: 22
}
The below code will change the name of both the docs to manjesha
var criteria = {
_id : { $in : [ "1" , "2"] }
}
userModel.update(criteria , { name: "manjesha" }, { multi: true },function(err , found){
if(err){
res.send(err);
}else{
res.send(found);
}
});
// $in -The $in operator selects the documents where the value of a field equals any value in the specified array.
This will work perfect on your problem .
I have documents that looks something like that, with a unique index on bars.name:
{ name: 'foo', bars: [ { name: 'qux', somefield: 1 } ] }
. I want to either update the sub-document where { name: 'foo', 'bars.name': 'qux' } and $set: { 'bars.$.somefield': 2 }, or create a new sub-document with { name: 'qux', somefield: 2 } under { name: 'foo' }.
Is it possible to do this using a single query with upsert, or will I have to issue two separate ones?
Related: 'upsert' in an embedded document (suggests to change the schema to have the sub-document identifier as the key, but this is from two years ago and I'm wondering if there are better solutions now.)
No there isn't really a better solution to this, so perhaps with an explanation.
Suppose you have a document in place that has the structure as you show:
{
"name": "foo",
"bars": [{
"name": "qux",
"somefield": 1
}]
}
If you do an update like this
db.foo.update(
{ "name": "foo", "bars.name": "qux" },
{ "$set": { "bars.$.somefield": 2 } },
{ "upsert": true }
)
Then all is fine because matching document was found. But if you change the value of "bars.name":
db.foo.update(
{ "name": "foo", "bars.name": "xyz" },
{ "$set": { "bars.$.somefield": 2 } },
{ "upsert": true }
)
Then you will get a failure. The only thing that has really changed here is that in MongoDB 2.6 and above the error is a little more succinct:
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 16836,
"errmsg" : "The positional operator did not find the match needed from the query. Unexpanded update: bars.$.somefield"
}
})
That is better in some ways, but you really do not want to "upsert" anyway. What you want to do is add the element to the array where the "name" does not currently exist.
So what you really want is the "result" from the update attempt without the "upsert" flag to see if any documents were affected:
db.foo.update(
{ "name": "foo", "bars.name": "xyz" },
{ "$set": { "bars.$.somefield": 2 } }
)
Yielding in response:
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
So when the modified documents are 0 then you know you want to issue the following update:
db.foo.update(
{ "name": "foo" },
{ "$push": { "bars": {
"name": "xyz",
"somefield": 2
}}
)
There really is no other way to do exactly what you want. As the additions to the array are not strictly a "set" type of operation, you cannot use $addToSet combined with the "bulk update" functionality there, so that you can "cascade" your update requests.
In this case it seems like you need to check the result, or otherwise accept reading the whole document and checking whether to update or insert a new array element in code.
if you dont mind changing the schema a bit and having a structure like so:
{ "name": "foo", "bars": { "qux": { "somefield": 1 },
"xyz": { "somefield": 2 },
}
}
You can perform your operations in one go.
Reiterating 'upsert' in an embedded document for completeness
I was digging for the same feature, and found that in version 4.2 or above, MongoDB provides a new feature called Update with aggregation pipeline.
This feature, if used with some other techniques, makes possible to achieve an upsert subdocument operation with a single query.
It's a very verbose query, but I believe if you know that you won't have too many records on the subCollection, it's viable. Here's an example on how to achieve this:
const documentQuery = { _id: '123' }
const subDocumentToUpsert = { name: 'xyz', id: '1' }
collection.update(documentQuery, [
{
$set: {
sub_documents: {
$cond: {
if: { $not: ['$sub_documents'] },
then: [subDocumentToUpsert],
else: {
$cond: {
if: { $in: [subDocumentToUpsert.id, '$sub_documents.id'] },
then: {
$map: {
input: '$sub_documents',
as: 'sub_document',
in: {
$cond: {
if: { $eq: ['$$sub_document.id', subDocumentToUpsert.id] },
then: subDocumentToUpsert,
else: '$$sub_document',
},
},
},
},
else: { $concatArrays: ['$sub_documents', [subDocumentToUpsert]] },
},
},
},
},
},
},
])
There's a way to do it in two queries - but it will still work in a bulkWrite.
This is relevant because in my case not being able to batch it is the biggest hangup. With this solution, you don't need to collect the result of the first query, which allows you to do bulk operations if you need to.
Here are the two successive queries to run for your example:
// Update subdocument if existing
collection.updateMany({
name: 'foo', 'bars.name': 'qux'
}, {
$set: {
'bars.$.somefield': 2
}
})
// Insert subdocument otherwise
collection.updateMany({
name: 'foo', $not: {'bars.name': 'qux' }
}, {
$push: {
bars: {
somefield: 2, name: 'qux'
}
}
})
This also has the added benefit of not having corrupted data / race conditions if multiple applications are writing to the database concurrently. You won't risk ending up with two bars: {somefield: 2, name: 'qux'} subdocuments in your document if two applications run the same queries at the same time.
Is there anyway within mongo, via MapReduce or Aggregation to apply a second query based on the result set of the first?, such as an Aggregate within an aggregate, or new emit/query within MapReduce.
For example, I have a materialized path pattern of items (which also includes parentId), I can get all of the roots simply by:
db.collection.find({parentId: null}
.toArray(function(err, docs) {
});
What I want to do is determine if these docs have children, just a flag true/false. I can iterate through these docs using async each and check, but on large docs, this is not very performant at all and causes event loop delays, I can use eachSeries, but this is just slow.
Ideally, I'd like to be able to handle this all within Mongo. Any suggestions if that's possible?
Edit, Example collection:
{
_id: 1,
parentId: null,
name: 'A Root Node',
path: ''
}
{
_id: 2,
parentId: 1,
name: 'Child Node A',
path: ',1'
}
{
_id: 3,
parentId: 2,
name: 'Child Node B',
path: ',1,2'
}
{
_id: 4,
parentId: null,
name: 'Another Root Node',
path: ''
}
This basically represents two root nodes, where one root node ({_id: 1}) has two children (one being direct), example:
1
2
3
4
What I would like to do is do a query based on parentId so I can get the root nodes by using null or by passing a parentId I can get the children of that and determine if the result set from this, any of the items contain children, example response for where {parentId: null}:
[{
_id: 1,
parentId: null,
name: 'A Root Node',
path '',
hasChildren: true
},
{
_id: 4,
parentId: null,
name: 'Another Root Node',
path '',
hasChildren: false
}]
You could try creating an array of the parentIds from the materialized paths that you can then use in the aggregation pipeline to project the extra field/flag hasChildren.
This can be done by using the map() method on the cursor returned from the find() method. The following illustrates this:
var arr = db.collection.find({ "parentId": { "$ne": null } })
.map(function (e){ return e.path; })
.join('')
.split(',')
.filter(function (e){ return e; })
.map(function (e){ return parseInt(e); }),
parentsIds = _.uniq(arr); /* using lodash uniq method to return a unique array */
Armed with this array of parentIds, you can then use the aggregation framework in particular the $project pipeline which makes use of the the set operator $setIsSubset which takes two arrays and returns true when the first array is a subset of the second, including when the first array equals the second array, and false otherwise:
db.collection.aggregate([
{
"$match": {
"parentId": null
}
},
{
"$project": {
"parentId": 1,
"name": 1,
"path": 1,
"hasChildren": { "$setIsSubset": [ [ "$_id" ], parentIds ] }
}
}
], function (err, res) { console.log(res); });
Is there a way to update values in an object?
{
_id: 1,
name: 'John Smith',
items: [{
id: 1,
name: 'item 1',
value: 'one'
},{
id: 2,
name: 'item 2',
value: 'two'
}]
}
Lets say I want to update the name and value items for item where id = 2;
I have tried the following w/ mongoose:
var update = {name: 'updated item2', value: 'two updated'};
Person.update({'items.id': 2}, {'$set': {'items.$': update}}, function(err) { ...
Problem with this approach is that it updates/sets the entire object, therefore in this case I lose the id field.
Is there a better way in mongoose to set certain values in an array but leave other values alone?
I have also queried for just the Person:
Person.find({...}, function(err, person) {
person.items ..... // I might be able to search through all the items here and find item with id 2 then update the values I want and call person.save().
});
You're close; you should use dot notation in your use of the $ update operator to do that:
Person.update({'items.id': 2}, {'$set': {
'items.$.name': 'updated item2',
'items.$.value': 'two updated'
}}, function(err) { ...
model.update(
{ _id: 1, "items.id": "2" },
{
$set: {
"items.$.name": "yourValue",
"items.$.value": "yourvalue",
}
}
)
MongoDB Document
There is a mongoose way for doing it.
const itemId = 2;
const query = {
item._id: itemId
};
Person.findOne(query).then(doc => {
item = doc.items.id(itemId );
item["name"] = "new name";
item["value"] = "new value";
doc.save();
//sent respnse to client
}).catch(err => {
console.log('Oh! Dark')
});
There is one thing to remember, when you are searching the object in array on the basis of more than one condition then use $elemMatch
Person.update(
{
_id: 5,
grades: { $elemMatch: { grade: { $lte: 90 }, mean: { $gt: 80 } } }
},
{ $set: { "grades.$.std" : 6 } }
)
here is the docs
For each document, the update operator $set can set multiple values, so rather than replacing the entire object in the items array, you can set the name and value fields of the object individually.
{'$set': {'items.$.name': update.name , 'items.$.value': update.value}}
Below is an example of how to update the value in the array of objects more dynamically.
Person.findOneAndUpdate({_id: id},
{
"$set": {[`items.$[outer].${propertyName}`]: value}
},
{
"arrayFilters": [{ "outer.id": itemId }]
},
function(err, response) {
...
})
Note that by doing it that way, you would be able to update even deeper levels of the nested array by adding additional arrayFilters and positional operator like so:
"$set": {[`items.$[outer].innerItems.$[inner].${propertyName}`]: value}
"arrayFilters":[{ "outer.id": itemId },{ "inner.id": innerItemId }]
More usage can be found in the official docs.
cleaner solution using findOneAndUpdate
await Person.findOneAndUpdate(
{ _id: id, 'items.id': 2 },
{
$set: {
'items.$.name': 'updated item2',
'items.$.value': 'two updated',
}
},
);
In Mongoose, we can update array value using $set inside dot(.) notation to specific value in following way
db.collection.update({"_id": args._id, "viewData._id": widgetId}, {$set: {"viewData.$.widgetData": widgetDoc.widgetData}})
Having tried other solutions which worked fine, but the pitfall of their answers is that only fields already existing would update adding upsert to it would do nothing, so I came up with this.
Person.update({'items.id': 2}, {$set: {
'items': { "item1", "item2", "item3", "item4" } }, {upsert:
true })
I had similar issues. Here is the cleanest way to do it.
const personQuery = {
_id: 1
}
const itemID = 2;
Person.findOne(personQuery).then(item => {
const audioIndex = item.items.map(item => item.id).indexOf(itemID);
item.items[audioIndex].name = 'Name value';
item.save();
});
Found this solution using dot-object and it helped me.
import dot from "dot-object";
const user = await User.findByIdAndUpdate(id, { ...dot.dot(req.body) });
I needed to update an array element with dynamic key-value pairs.
By mapping the update object to new keys containing the $ update operator, I am no longer bound to know the updated keys of the array element and instead assemble a new update object on the fly.
update = {
name: "Andy",
newKey: "new value"
}
new_update = Object.fromEntries(
Object.entries(update).map(
([k, v], i) => ["my_array.$." + k, v]
)
)
console.log({
"$set": new_update
})
In mongoose we can update, like simple array
user.updateInfoByIndex(0,"test")
User.methods.updateInfoByIndex = function(index, info) ={
this.arrayField[index]=info
this.save()
}
update(
{_id: 1, 'items.id': 2},
{'$set': {'items.$[]': update}},
{new: true})
Here is the doc about $[]: https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up.S[]