How can I update a record in a document with multiple levels of array nesting?
My document structure is the following:
{
"_id": "5bfa09f0a0441f38d45dcc9c",
"nombre": "PROYECTO MAIN",
"area": "Sistemas",
"fecha": "27/01/2018",
"reuniones": [
{
"_id": "5bfa09f0a0441f38d45dcc99",
"objetivo": "Objetivo MODIFICADO",
"fecha": "25/10/2018",
"participantes": [
{
"nomina": 1,
"nombre": "MODIFICADO",
"rol": "rol",
"area": "area",
"firma": "url/imagen.jpg"
},
{
"nomina": 2,
"nombre": "nombre 2",
"rol": "rol",
"area": "area",
"firma": "url/imagen.jpg"
}
]
}
],
"_class": "proyecto"
}
Using the following query, returns me the document mentioned above.
db.proyectos.find({
_id:ObjectId("5bfa09f0a0441f38d45dcc9c"),
"reuniones._id":ObjectId("5bfa09f0a0441f38d45dcc99"),
"reuniones.participantes.nomina":2
})
I want to update the firma field of participant with nomina 2.
Since Mongo 3.6, you can update multi-nested arrays by combining the following operators:
$set (to update a specific field)
$[] (to match any item in an array)
$[<identifier>] (to match specific items in an array)
Example
Here's how you can update a specific proyectos document that has a reuniones array that has a participantes array that has an object with the field nomina equal to 2:
// update a specific proyectos document
// that has a field "reuniones" which is an array
// in which each item is an object with a field "participantes" that is an array
// in which each item is an object that has a field "nomina" equal to 2
db.proyectos.update({
_id: ObjectId("5bfa09f0a0441f38d45dcc9c"),
}, {
$set: {
"reuniones.$[].participantes.$[j].firma": <your update>
},
}, {
arrayFilters: [
{
"j.nomina": 2
}
]
})
If you wanted to limit your query to a specific reunion, you can do:
db.proyectos.update({
_id: ObjectId("5bfa09f0a0441f38d45dcc9c"),
}, {
$set: {
"reuniones.$[i].participantes.$[j].firma": <your update>
},
}, {
arrayFilters: [
{
"i._id": ObjectId("5bfa09f0a0441f38d45dcc99")
},
{
"j.nomina": 2
}
]
})
To update all proyectos satisfying the above condition, just omit the _id query:
// update all proyectos
// that have a field "reuniones" which is an array
// in which each item is an object with a field "participantes" that is an array
// in which each item is an object that has a field "nomina" equal to 2
db.proyectos.update({}, {
$set: {
"reuniones.$[].participantes.$[j].firma": <your update>
},
}, {
arrayFilters: [
{
"j.nomina": 2
}
]
})
Related
"ParentType": {
"Food": [
{
"Name": "Burger",
"FoodId": "5e3abe145c1bfb31b4e335de",
"Price": 0,
"Quantity": 1,
"SubCategory": 0
}
],
"Inventory": [
{
"Name": "Small Popcorn",
"InventoryId": "5e3a64245c1bfb31b4e335b7",
"Price": 0,
"Quantity": 1,
"SubCategory": 0
}
]
}
I need to add UOM as new column only for Inventory array.I have used aggregate as below but collection is not getting updated.Pls help me with adding this new Column in mongodb
db.Concession.aggregate([
{
$addFields: { ParentType.Inventory.UOM: "null" }
}
])
add UOM to all eliment in Inventory
db.collection.update(
{},
{ $set: { 'ParentType.Inventory.$[].UOM':''} }
)
Option 1: ( Update/$set 3.6+ )
db.collection.update({},
{
$set: {
"ParentType.Inventory.$[].UOM": null
}
},
{
multi: true
})
Explained:
Use update() operation with positional operator $[] ( mongoDB 3.6+ )
Use option multi to update all documents in collection matching the filter criteria
Playground
Option 2: ( Update/aggregation 4.2+)
db.collection.update({},
[
{
$addFields: {
"ParentType.Inventory": {
"$map": {
input: "$ParentType.Inventory",
as: "i",
in: {
"$mergeObjects": [
"$$i",
{
UOM: null
}
]
}
}
}
}
}
],
{
multi: true
})
Explained:
Use update with aggregation pipeline ( mongoDB 4.2+ )
In aggregation use the addFileds with mergeObjects so you add the new fields to the array element.
Use multi:true to affect all documents in collection matching the filter criteria
Playground 2
Suppose I have a document that looks like this:
{
"id": 1,
"entries": [
{
"id": 100,
"urls": {
"a": "url-a",
"b": "url-b",
"c": "url-c"
},
"revisions": []
}
]
}
I am trying to add a new object to the revisions array that contains its own urls field. Two of the fields should be copied from the entry's urls, while the last one will be new. The result should look like this:
{
"id": 1,
"entries": [
{
"id": 100,
"urls": {
"a": "url-a",
"b": "url-b",
"c": "url-c"
},
"revisions": [
{
"id": 1000,
"urls": {
"a": "url-a", <-- copied
"b": "url-b", <-- copied
"c": "some-new-url" <-- new
}
}
]
}
]
}
I am on MongoDB 4.2+, so I know I can use $property on the update query to reference values. However, this does not seem to be working as I expect:
collection.updateOne(
{
id: 1,
"enntries.id": 100
},
{
$push: {
"entries.$.revisions": {
id: 1000,
urls: {
"a": "$entries.$.urls.a",
"b": "$entries.$.urls.b",
"c": "some-new-url"
}
}
}
}
);
The element gets added to the array, but all I see for the url values is the literal $entries.$.urls.a. value I suspect the issue is with combining the reference with selecting a specific positional array element. I have also tried using $($entries.$.urls.a), with the same result.
How can I make this work?
Starting from MongoDB version >= 4.2 you can use aggregation pipeline in updates which means your update part of query will be wrapped in [] where you can take advantage of executing aggregation in query & also use existing field values in updates.
Issue :
Since you've not wrapped update part in [] to say it's an aggregation pipeline, .updateOne() is considering "$entries.$.urls.a" as a string. I believe you'll not be able to use $ positional operator in updates which use aggregation pipeline.
Try below query which uses aggregation pipeline :
collection.updateOne(
{
id: 1,
"entries.id": 100 /** "entries.id" is optional but much needed to avoid execution of below aggregation for doc where `id :1` but no `"entries.id": 100` */,
}
[
{
$set: {
entries: {
$map: { // aggregation operator `map` iterate over array & creates new array with values.
input: "$entries",
in: {
$cond: [
{ $eq: ["$$this.id", 100] }, // `$$this` is current object in array iteration, if condition is true do below functionality for that object else return same object as is to array being created.
{
$mergeObjects: [
"$$this",
{
revisions: { $concatArrays: [ "$$this.revisions", [{ id: 1000, urls: { a: "$$this.urls.a", b: "$$this.urls.b", c: "some-new-url" } } ]] }
}
]
},
"$$this" // Returning same object as condition is not met.
]
}
}
}
}
}
]
);
$mergeObjects will replace existing revisions field in $$this (current) object with value of { $concatArrays: [ "$$this.revisions", { id: 1000, urls: { a: "$$this.urls.a", b: "$$this.urls.b", c: "some-new-url" } } ] }.
From the above field name revisions and as it being an array I've assumed there will multiple objects in that field & So we're using $concatArrays operator to push new objects into revisions array of particular entires object.
In any case, if your revisions array field does only contain one object make it as an object instead of array Or you can keep it as an array & use below query - We've removed $concatArrays cause we don't need to merge new object to existing revisions array as we'll only have one object every-time.
collection.update(
{
id: 1,
"entries.id": 100
}
[
{
$set: {
entries: {
$map: {
input: "$entries",
in: {
$cond: [
{ $eq: ["$$this.id", 100] },
{
$mergeObjects: [
"$$this",
{
revisions: [ { id: 1000, urls: { a: "$$this.urls.a", b: "$$this.urls.b", c: "some-new-url" } } ]
}
]
},
"$$this"
]
}
}
}
}
}
]
);
Test : Test your aggregation pipeline here : mongoplayground
Ref : .updateOne()
Note : If in any case .updateOne() throws in an error due to in-compatible client or shell, try this query with .update(). This execution of aggregation pipeline in updates helps to save multiple DB calls & can be much useful on arrays with less no.of elements.
My documents have the following structure:
{
_id: ObjectId("59303aa1bad1081d4b98d636"),
clear_number: "83490",
items: [
{
name: "83490_1",
file_id: "e7209bbb",
hash: "2f568bb196f74263c64b7cf273f8ceaa",
},
{
name: "83490_2",
file_id: "9a56a935",
hash: "9c6230f7bf19d3f3186c6c3231ac2055",
},
{
name: "83490_2",
file_id: "ce5f6773",
hash: "9c6230f7bf19d3f3186c6c3231ac2055",
}
],
group_id: null
}
How to remove one of two subdocuments with the same items hash?
The following should do the trick if I understand you question correctly:
collection.aggregate({
$unwind: "$items" // flatten the items array
}, {
$group: {
"_id": { "_id": "$_id", "clear_number": "$clear_number", "group_id": "$group_id", "hash": "$items.hash" }, // per each document group by hash value
"items": { $first: "$items" } // keep only the first of all matching ones per group
}
}, {
$group: {
"_id": { "_id": "$_id._id", "clear_number": "$_id.clear_number", "group_id": "$_id.group_id" }, // now let's group everything again without the hashes
"items": { $push: "$items" } // push all single items into the "items" array
}
}, {
$project: { // this is just to restore the original document layout
"_id": "$_id._id",
"clear_number": "$_id.clear_number",
"group_id": "$_id.group_id",
"items": "$items"
}
})
In response to your comment I would suggest the following query to get the list of all document ids that contain duplicate hashes:
collection.aggregate({
$addFields: {
"hashes": {
$setUnion: [
[ { $size: "$items.hash" } ], // total number of hashes
[ { $size: { $setUnion: "$items.hash" } } ] // number of distinct hashes
]
}
}
}, {
$match:
{
"hashes.1": { $exists: true } // find all documents with a different value for distinct vs total number of hashes
}
}, {
$project: { _id: 1 } // only return _id field
})
There might be different approaches but this one seems pretty straight forward:
Basically, in the $addFields part, for each document, we first create an array consisting of two numbers:
the total number of hashes
the number of distinct hashes
Then we drive this array of two numbers through a $setUnion. After this step there can
either be two different numbers left in the array in which case the hash field does contain duplicates
or there is only one element left, in which case the number of distinct hashes equals the total number of hashes (so there are no duplicates).
We can check if there are two items in the array by testing if the element at position 1 (arrays are zero-based!) exists. That's what the $match stage does.
And the final $project stage is just to limit the output to the _id field only.
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 });
i am pushing sub documents into an array.
Basically the subdocs are something like this:
{
id:"someId",
date:Date
}
i want to add new subdoc only if the theres no other subdoc with matching id.I found $addToSet modifier on mongodb documentation but i am not sure if it can just check a key inside a sub document rather than comparin entire array item to another one.
Is there any way to do this in single query or anyway should i go with 2 queries?
You can do this in a single atomic update operation. Suppose your collection has the following documents:
Mongo shell
db.test.insert([
{
"_id": 1,
"items": [
{ "id": 1, "date": new Date("2016-05-20") },
{ "id": 2, "date": new Date("2016-05-21") }
]
},
{
"_id": 2,
"items": [
{ "date": new Date("2016-05-22") }
]
}
])
Then the following update statement in mongo shell will update the document that has the items subdoc which does not have the id key by adding the new sub document { "id": 3, "date": new Date() } to the items array:
db.test.update(
{ "items.id": { "$exists": false } },
{
"$push": {
"items": { "id": 3, "date": new Date() }
}
}
)
Querying the test collection
> db.test.find().pretty()
{
"_id" : 1,
"items" : [
{
"id" : 1,
"date" : ISODate("2016-05-20T00:00:00Z")
},
{
"id" : 2,
"date" : ISODate("2016-05-21T00:00:00Z")
}
]
}
{
"_id" : 2,
"items" : [
{
"date" : ISODate("2016-05-22T00:00:00Z")
},
{
"id" : 3,
"date" : ISODate("2016-05-23T09:33:15.913Z")
}
]
}
To add a new subdoc only if the there's no other subdoc with a matching id, you only need one update operation where you can specify the matching condition
in the update query document.
The following update operation will add a subdocument with id = 4 to the array since there is no matching array element in the existing document:
db.test.update(
{ "items.id": { "$ne": 4 } },
{
"$push": {
"items": { "id": 4, "date": new Date() }
}
}
)
For an exclusive update where you just want to add the subdocument if there is an id field AND the given id does not match, then include the condition together with the equality check:
db.test.update(
{
"items.id": {
"$exists": true,
"$ne": 4
}
},
{
"$push": {
"items": { "id": 4, "date": new Date() }
}
}
)