I have a schema like this
{
"_id": "5f86da4b5bb9a62742371409",
"status" : true,
"doc": [
{
"_id": "5f86cacbe4d7c423f21faf50",
"a": 4,
"b": null
},
{
"_id": "5f86cb01a1ca5124063299c1",
"a": 6,
"b": null
}
]
}
And I have an update in the form of
[
{
"_id": "5f86cacbe4d7c423f21faf50",
"b": 90
},
{
"_id": "5f86cb01a1ca5124063299c1",
"b": 45
}
]
How can I update the collection to end up like this?
{
"_id": "5f86da4b5bb9a62742371409",
"status" : true,
"doc": [
{
"_id": "5f86cacbe4d7c423f21faf50",
"a": 4,
"b": 90
},
{
"_id": "5f86cb01a1ca5124063299c1",
"a": 6,
"b": 45
}
]
}
Basically I want to update the subdocument with specific keys only (leaving other keys intact)
You can use update with aggregation pipeline from MongoDB 4.2,
$map to iterate loop of array doc and inside it iterate through updateDocs array, it will return matching b, and then $mergeObjects will return updated document,
let updateDocs = [
{ "_id": mongoose.Types.ObjectId("5f86cacbe4d7c423f21faf50"), "b": 90 },
{ "_id": mongoose.Types.ObjectId("5f86cb01a1ca5124063299c1"), "b": 45 }
];
db.collection.updateMany({},
[
{
$set: {
doc: {
$map: {
input: "$doc",
as: "d",
in: {
$mergeObjects: [
"$$d",
{
$reduce: {
input: updateDocs,
initialValue: {},
in: {
$cond: [
{ $eq: ["$$this._id", "$$d._id"] },
{ b: "$$this.b" },
"$$value"
]
}
}
}
]
}
}
}
}
}
]
)
Playground
Related
I have a collection with the document of this structure:
{
"_id": {
"$oid": "63e8af476a3674484ea14888"
},
"my_id": 321123,
"version": 0,
"parameters": [
{"a": 1},
{"a": 1, "b": 2}
]
}
Using a RESTful API the user can change the version field to any number between 0 and len(parameters). Another endpoint let the user push an object to parameters and set version to the new len(parameters) - 1. So using the previous example, after using this endpoint we will get:
{
"_id": {
"$oid": "63e8af476a3674484ea14888"
},
"my_id": 321123,
"version": 2,
"parameters": [
{"a": 1},
{"a": 1, "b": 2},
{"a": 1, "b": 2, "c": 3}
]
}
I tried many things, but nothing seems to work. Here are examples of what I tried:
db.parameters.findOneAndUpdate({"my_id": 321123}, {"$inc":{version: parameters}, "$push":{"a": 1, "b": 2, "c": 3}}, upsert=true)
db.parameters.findOneAndUpdate({"my_id": 321123}, {"$inc":{version: {"$size": parameters}}, "$push":{"a": 1, "b": 2, "c": 3}}, upsert=true)
As your update requires referring to another field, you have to use the update query with the aggregation pipeline.
For the first scenario,
Add a new parameter object by combining it with the current parameters array via $concatArrays.
I don't think that increment is what you need, but rather update the version value via $set stage. An example as below is to compare the version param between the range of 0 and len(parameters), if true, then update with the version param, else update with the size of the parameters array.
db.parameters.update({
"my_id": 321123
},
[
{
"$set": {
parameters: {
"$concatArrays": [
"$parameters",
[
{
"a": 1,
"b": 2,
"c": 3
}
]
]
}
}
},
{
"$set": {
version: {
$cond: {
if: {
$and: [
{
$gt: [
2,
// version
0
]
},
{
$lt: [
2,
// version
{
$size: "$parameters"
}
]
}
]
},
then: 2,
else: {
$size: "$parameters"
}
}
}
},
}
],
{
upsert: true
})
Demo (1st scenario) # Mongo Playground
For the second scenario,
Add a new parameter object by combining it with the current parameters array via $concatArrays.
Set the version with the size of parameters array minus 1.
db.parameters.update({
"my_id": 321123
},
[
{
"$set": {
parameters: {
"$concatArrays": [
"$parameters",
[
{
"a": 1,
"b": 2,
"c": 3
}
]
]
}
}
},
{
"$set": {
version: {
"$subtract": [
{
$size: "$parameters"
},
1
]
}
},
}
],
{
upsert: true
})
Demo (2nd scenario) # Mongo Playground
My collection
[
{
"_id": "value",
"data": [
{
"d": "2022-05-16",
"v": 10.06
},
{
"d": "2022-05-18",
"v": 9.11
},
{
"d": "2022-05-20",
"v": 7.06
}
],
"name": "Vegeta"
}
]
Now I want to make a query say
{name : "something" , "data.d" : "2022-05-18"} --> this filter returns v = 9.11
{name : "something" , "data.d" : "2022-05-17"} --> this filter also should return v = 10.06
{name : "something" , "data.d" : "2022-05-19"} --> this filter also should return v = 9.11
Basically I want to the return the result from data which matches the given date if not present then return the previous date data.
I have tried using elemmatch. Unwind works for me but need to increase the performance of the query.If possible can we do in the mongo shell itself.
db.collection.aggregate([
{
$match: {
"name": "Vegeta" //input
}
},
{
"$project": {
"_id": 1,
"name": 1,
"data": { //filter the array elements
"$filter": {
"input": "$data",
"as": "data",
"cond": {
"$lte": [
"$$data.d",
"2022-05-20" //input
]
}
}
}
}
},
{
$project: {
"_id": 1,
"name": 1,
"data": { //sort the remaining array elements
$sortArray: {
input: "$data",
sortBy: {
"d": -1
}
}
}
}
},
{
$project: {
"_id": 1,
"name": 1,
"data": { //pick one
"$arrayElemAt": [
"$data",
0
]
}
}
}
])
Mongo Playground
Is there an aggregation query available to convert the following:
after unwind I plan to change the key based on the value inside each object and append it too has---(type)
{
"rows": [
{
"type": "aaa",
"values": [
1,
2,
3
]
},
{
"type": "bbb",
"values": [
4,
5,
6
]
}
]
}
to
{
"hasaaa": {
"type": "aaa",
"values": [
1,
2,
3
]
},
"hasbbb": {
"type": "bbb",
"values": [
4,
5,
6
]
}
}
$map to iterate loop of rows array
$concat to prepare the custom key string
return key-value from map
$arrayToObject convert key-value array to object
db.collection.aggregate([
{
$project: {
rows: {
$arrayToObject: {
$map: {
input: "$rows",
in: {
k: { $concat: ["has", "$$this.type"] },
v: "$$this"
}
}
}
}
}
}
])
Playground
I'm looking for a simple solution to add a field to a subdocument without using $unwind and $group.
I need only to calculate the sum and the size of a nested subdocuments and show it in a new field.
This is my starting collection:
{
"_id": ObjectId("5a934e000102030405000000"),
"subDoc": [
{
"a": 1,
"subArray": [1,2,3]
},
{
"b": 2
}
]
},
{
"_id": ObjectId("5a934e000102030405000001"),
"subDoc": [
{
"a": 1,
"subArray": [4,5,6]
},
{
"b": 2
},
{
"c": 3,
"subArray": [8,8,8]
}
]
}
And this is my desired result, where I've added sum (sum of subArray) and size (number of elements in subArray):
{
"_id": ObjectId("5a934e000102030405000000"),
"subDoc": [
{
"a": 1,
"subArray": [1,2,3],
"sum": 6,
"size": 3
},
{
"b": 2
"sum": 0,
"size": 0
}
]
},
{
"_id": ObjectId("5a934e000102030405000001"),
"subDoc": [
{
"a": 1,
"subArray": [4,5,6],
"sum": 15,
"size": 3
},
{
"b": 2,
"sum": 0,
"size": 0
},
{
"c": 3,
"subArray": [8,8],
"sum": 16,
"size": 2
}
]
}
I know how to obtain this result using $unwind and then $group, but I'd like to know if there is any other way (or a better way!) to achieve the same result. I've tried using $addFields and $map without success.
Working playground example: https://mongoplayground.net/p/fK8t6SLlOHa
$map to iterate loop of subDoc array
$sum to get total of subArray array of numbers
$ifNull to check if field is not present or null then return empty array because $size operator only allows array input
$size to get total elements in subArray array
$mergeObjects to merge current object with new added fields
db.collection.aggregate([
{
$addFields: {
subDoc: {
$map: {
input: "$subDoc",
in: {
$mergeObjects: [
"$$this",
{
sum: { $sum: "$$this.subArray" },
size: {
$size: { $ifNull: ["$$this.subArray", []] }
}
}
]
}
}
}
}
}
])
Playground
I was looking to pull some value from array and simultaneously trying to update it.
userSchema.statics.experience = function (id,xper,delet,callback) {
var update = {
$pull:{
'profile.experience' : delet
},
$push: {
'profile.experience': xper
}
};
this.findByIdAndUpdate(id,update,{ 'new': true},function(err,doc) {
if (err) {
callback(err);
} else if(doc){
callback(null,doc);
}
});
};
i was getting error like:
MongoError: exception: Cannot update 'profile.experience' and 'profile.experience' at the same time
I found this explanation:
The issue is that MongoDB doesn’t allow multiple operations on the
same property in the same update call. This means that the two
operations must happen in two individually atomic operations.
And you can read that posts:
Pull and addtoset at the same time with mongo
multiple mongo update operator in a single statement?
In case you need replace one array value to another, you can use arrayFilters for update.
(at least, present in mongo 4.2.1).
db.your_collection.update(
{ "_id": ObjectId("your_24_byte_length_id") },
{ "$set": { "profile.experience.$[elem]": "new_value" } },
{ "arrayFilters": [ { "elem": { "$eq": "old_value" } } ], "multi": true }
)
This will replace all "old_value" array elements with "new_value".
Starting from MongoDB 4.2
You can try to update the array using an aggregation pipeline.
this.updateOne(
{ _id: id },
[
{
$set: {
"profile.experience": {
$concatArrays: [
{
$filter: {
input: "$profile.experience",
cond: { $ne: ["$$this", delet] },
},
},
[xper],
],
},
},
},
]
);
Following, a mongoplayground doing the work:
https://mongoplayground.net/p/m1C1LnHc0Ge
OBS: With mongo regular update query it is not possible.
Since Mongo 4.2 findAndModify supports aggregation pipeline which will allow atomically moving elements between arrays within the same document. findAndModify also allows you to return the modified document (necessary to see which array elements were actually moved around).
The following includes examples of:
moving all elements from one array onto the end of a different array
"pop" one element of an array and "push" it to another array
To run the examples, you will need the following data:
db.test.insertMany( [
{
"_id": ObjectId("6d792d6a756963792d696441"),
"A": [ "8", "9" ],
"B": [ "7" ]
},
{
"_id": ObjectId("6d792d6a756963792d696442"),
"A": [ "1", "2", "3", "4" ],
"B": [ ]
}
]);
Example 1 - Empty array A by moving it into array B:
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696441") },
update: [
{ $set: { "B": { $concatArrays: [ { $ifNull: [ "$B", [] ] }, "$A" ] } } },
{ $set: { "A": [] } }
],
new: true
});
Resulting in:
{
"_id": {
"$oid": "6d792d6a756963792d696441"
},
"A": [],
"B": [
"7",
"8",
"9"
]
}
Example 2.a - Pop element from array A and push it onto array B
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696442"),
"A": {$exists: true, $type: "array", $ne: [] }},
update: [
{ $set: { "B": { $concatArrays: [ { $ifNull: [ "$B", [] ] }, [ { $first: "$A" } ] ] } } },
{ $set: { "A": { $slice: ["$A", 1, {$max: [{$subtract: [{ $size: "$A"}, 1]}, 1]}] } }}
],
new: true
});
Resulting in:
{
"_id": {
"$oid": "6d792d6a756963792d696442"
},
"A": [
"2",
"3",
"4"
],
"B": [
"1"
]
}
Example 2.b - Pop element from array A and push it onto array B but in two steps with a temporary placeholder:
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696442"),
"temp": { $exists: false } },
update: [
{ $set: { "temp": { $first: "$A" } } },
{ $set: { "A": { $slice: ["$A", 1, {$max: [{$subtract: [{ $size: "$A"}, 1]}, 1]}] } }}
],
new: true
});
// do what you need to do with "temp"
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696442"),
"temp": { $exists: true } },
update: [
{ $set: { "B": { $concatArrays: [ { $ifNull: [ "$B", [] ] }, [ "$temp" ] ] } } },
{ $unset: "temp" }
],
new: true
});