update object in an array if exists Push if not exists - mongodb

Here my example document
{
"_id" : "1",
"principal" : "Joe",
"classroom" : [
{
"grade" : "1",
"class" : "A",
"totalStudent" : 10.0
},
{
"grade" : "1",
"class" : "B",
"totalStudent" : 20.0
}
]
}
my goal is
increment TotalStudent when filter match ex.
principal : "Joe", classroom.grade: "1" classroom.class: "A"
push new object into classroom if object isn't exists ex. classroom.grade: "1" classroom.class: "C"
my current query (this work fine with increment into exists object)
db.getCollection("classsroom").updateOne(
{
pricipal: "Joe",
"classroom": {$elemMatch: {"grade":"1","class": "A"}}
},
{ $inc: { "classroom.$.totalStudent": 5 } },
{upsert: true}
)
expect result
{
"_id" : "1",
"principal" : "Joe",
"classroom" : [
{
"grade" : "1",
"class" : "A",
"totalStudent" : 10.0
},
{
"grade" : "1",
"class" : "B",
"totalStudent" : 20.0
},
{
"grade" : "1",
"class" : "c",
"totalStudent" : 5.0
}
]
}

Maybe something like this using update/aggregation ( 4.2+ ):
db.collection.update({ "principal":"Joe" },
[
{
$addFields: {
x: {
$size: {
$filter: {
input: "$classroom",
as: "c",
cond: {
$and: [
{
$eq: [
"$$c.grade",
"1"
]
},
{
$eq: [
"$$c.class",
"A"
]
}
]
}
}
}
}
}
},
{
$addFields: {
classroom: {
$cond: [
{
"$ne": [
"$x",
0
]
},
{
$map: {
input: "$classroom",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{
$and: [
{
$eq: [
"$$this.grade",
"1"
]
},
{
$eq: [
"$$this.class",
"A"
]
}
]
},
{
totalStudent: {
$add: [
"$$this.totalStudent",
5
]
}
},
{}
]
}
]
}
}
},
{
$concatArrays: [
"$classroom",
[
{
"class": "C",
"grade": "1",
"totalStudent": 5
}
]
]
}
]
}
}
},
{
$unset: "x"
}
],
{
multi: true
})
Explained:
Add temporary variable x that contain count of elements matching the criteria (grade="1",class="A")
Use the temporary variable x to check if array contain the matching element , if x=0 ( no elements matching ) perform concatArrays[] to add the new element. If x>0 (there is at least 1x element matching ) update the totalStudent value adding the needed increment amount.
Remove with $unset the temporary variable.
Add multi:true to perform the operation in all matching documents in collection
Playground
Improved version ( using $let for the temporary x variable ):
Playground2

Related

Dynamic fields array in to single object mongo db

I am having mongo collection like below,
{
"_id" : ObjectId("62aeb8301ed12a14a8873df1"),
"Fields" : [
{
"FieldId" : "e8efd0b0-9d10-4584-bb11-5b24f189c03b",
"Value" : [
"test_123"
]
},
{
"FieldId" : "fa6745c2-b259-4a3b-8c6f-19eb78fbbbf5",
"Value" : [
"123"
]
},
{
"FieldId" : "2a1be5d0-8fb6-4b06-a253-55337bfe4bcd",
"Value" : []
},
{
"FieldId" : "eed12747-0923-4290-b09c-5a05107f5609",
"Value" : [
"234234234"
]
},
{
"FieldId" : "fe41d8fb-fa18-4fe5-b047-854403aa4d84",
"Value" : [
"Irrelevan"
]
},
{
"FieldId" : "93e46476-bf2e-44eb-ac73-134403220e9e",
"Value" : [
"test"
]
},
{
"FieldId" : "db434aca-8df3-4caf-bdd7-3ec23252c2c8",
"Value" : [
"2019-06-16T18:30:00.000Z"
]
},
{
"FieldId" : "00df903f-5d59-41c1-a3df-60eeafb77d10",
"Value" : [
"tewt"
]
},
{
"FieldId" : "e97d0386-cd42-6277-1207-e674c3268cec",
"Value" : [
"1"
]
},
{
"FieldId" : "35e55d27-7d2c-467d-8a88-09ad6c9f5631",
"Value" : [
"10"
]
}
]
}
This is all dynamic form fields.
So I want to query and get result like to below object,
{
"_id" : ObjectId("62aeb8301ed12a14a8873df1"),
"e8efd0b0-9d10-4584-bb11-5b24f189c03b": ["test_123"],
"fa6745c2-b259-4a3b-8c6f-19eb78fbbbf5": ["123"],
"2a1be5d0-8fb6-4b06-a253-55337bfe4bcd": [],
"eed12747-0923-4290-b09c-5a05107f5609": ["234234234"],
"fe41d8fb-fa18-4fe5-b047-854403aa4d84": ["Irrelevan"],
"93e46476-bf2e-44eb-ac73-134403220e9e":["test"],
"db434aca-8df3-4caf-bdd7-3ec23252c2c8":["2019-06-16T18:30:00.000Z"],
"00df903f-5d59-41c1-a3df-60eeafb77d10":["1"]
}
I want final output like this combination of fields Fields.FieldID should be key and Fields.Value should be value here.
Please try to help to me to form the object like above.
Thanks in advance!
You can restructure your objects using $arrayToObject, then using that value to as a new root $replaceRoot like so:
db.collection.aggregate([
{
$match: {
// your query here
}
},
{
$project: {
newRoot: {
"$arrayToObject": {
$map: {
input: "$Fields",
in: {
k: "$$this.FieldId",
v: "$$this.Value"
}
}
}
}
}
},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": [
"$newRoot",
{
_id: "$_id"
}
]
}
}
}
])
Mongo Playground
I try this and get result like you want
db.collection.aggregate([{
$replaceWith: {
$mergeObjects: [
{
_id: "$_id"
},
{
$arrayToObject: { $zip: {inputs: ["$Fields.FieldId","$Fields.Value"]}}
}
]
}
}])
playground

MongoDB: add element to an inner array of array with an object that contains field calculated on another field

I have this document:
{
"_id" : ObjectId("626c0440e1b4f9bb5568f542"),
"ap" : [
{
"ap_id" : ObjectId("000000000000000000000001"),
"shop_prices" : [
{
"shop_id" : ObjectId("000000000000000000000097"),
"price" : 102
}
]
}
],
"bc" : [
{
"bc_id" : ObjectId("000000000000000000000003"),
"price" : 102
},
{
"bc_id" : ObjectId("000000000000000000000004"),
"price" : 104
}
],
"stock_price" : 70
}
My need is to eventually add to ap.shop_prices an element if not exists with this structure:
{
"shop_id" : ObjectId("000000000000000000000096"),
"price" : 104
}
where the price is bc.price where bc.bc_id = ObjectId("000000000000000000000004")
This is my first (unsuccesfull) try:
updateMany(
{
"_id": {"$eq": ObjectId("626c0421e1b4f9bb5568f531")},
"ap":{
$elemMatch:{
"ap_id":{$in:[ObjectId("000000000000000000000001")]},
"shop_prices.shop_id":{$ne:ObjectId("000000000000000000000096")}
}
},
"bc.bc_id": ObjectId("000000000000000000000003")
},
[
{"$set":
{"ap.$.shop_prices":
{"$cond":
[{"$in": [ObjectId("000000000000000000000096"), "$ap.$.shop_prices.shop_id"]}, "$ap.$.shop_prices",
{"$concatArrays":
["$ap.$.shop_prices",
[{"shop_id": ObjectId("000000000000000000000096"), "price": ???}]
]
}
]
}
}
}
]
)
thanks in advance
You can do that:
finding the bc related to your request using the $project
using $map in the $set operator
This should be the solution:
db.getCollection('test').update({
"ap": {
$elemMatch: {
"ap_id":{$in:[ObjectId("000000000000000000000001")]},
"shop_prices.shop_id":{$ne:ObjectId("000000000000000000000096")}
}
},
"bc.bc_id": ObjectId("000000000000000000000004")
},
[
{
$project: {
ap: 1,
bc: 1,
stock_price: 1,
current_bc: {
$arrayElemAt: [ {
$filter: {
input: "$bc",
as: "curr_bc",
cond: {$eq: ["$$curr_bc.bc_id", ObjectId("000000000000000000000004")]}
}
}, 0 ]
}
}
},
{
$set: {
"ap": {
"$map": {
input: "$ap",
as: "current_ap",
in: {
$cond: [
{$eq: [ObjectId("000000000000000000000001"), "$$current_ap.ap_id"]},
{
"$mergeObjects": [
"$$current_ap",
{"shop_prices": {$concatArrays: ["$$current_ap.shop_prices", [{"shop_id": ObjectId("000000000000000000000096"), "price": "$current_bc.price"}]]}}
]
},
"$$current_ap"
]
}
}
}
}
}
])

sum array of elements without unwind in group by?

Below are the projected Result and I want to get the sum of Expenses Amount where ExpenseType equal to "1" and the result should group by Type and Quarter. How to achieve this functionality without unwinding the Expenses Array.?
{
"Type" : "CreditCard",
"Quarter": "20201",
"Expenses" : [
{
"ExpenseType" : "1",
"Amount" : 123
},
{
"ExpenseType" : "2",
"Amount" : 183
}
]
}
{
"Type" : "Cash",
"Quarter": "20202",
"Expenses" : [
{
"ExpenseType" : "1",
"Amount" : 345
},
{
"ExpenseType" : "2",
"Amount" : 200
}
]
}
Expected Output:
{
"Type" : "CreditCard",
"Quarter": "20201",
"Total":"123"
}
{
"Type" : "Cash",
"Quarter": "20202",
"Total":"345"
}****
Mechanism
Group by Quarter and Tpy
Sum values
Pipeline
db.collection.aggregate({
$group: {
"_id": {
"Quarter": "$Quarter",
"Type": "$Type"
},
"Total": {
$push: {
$reduce: {
input: "$Expenses",
initialValue: 0,
in: {
$cond: [
{
$eq: [
"$$this.ExpenseType",
"1"
]
},
{
$add: [
"$$value",
"$$this.Amount"
]
},
{
$add: [
"$$value",
0
]
}
]
}
}
}
}
}
})
Playground

How to divide the elements inside array using mongodb aggregation

I have used aggregation to find below output.
Next step is to divide the values of elements present in each array.
{
"_id" : {
“type”:”xxx”,
"year" : 2019
},
"allvalues" : [
{
"label" : “used”,
"value" : 50
},
{
"label" : “total”,
"value" : 58
}
]
}
{
"_id" : {
“type”:”yyy”,
"year" : 2019
},
"allvalues" : [
{
"label" : “used”,
"value" : 63.214285714285715
},
{
"label" : “total”,
"value" : 59.535714285714285
}
]
}
How to write the query to divide used/total in each doc.
For first doc it is 50/58 and second doc it is 63.21/59.53.
The structure of json remains constant.
Output should look like below:
{
“type”:”xxx”,
"year" : 2019,
“result” : 0.8
}
{
“type”:”yyy”,
"year" : 2019,
“result” : 1.06
}
Add this in the aggregate pipeline after you get above input first you need to use $filter and $arrayElemAt to get used and total's value after that as divide doesn't give fixed decimal place, I've used $trunc to make value to 2 decimal fixed place
{
$project: {
_id: 1,
used: {
$arrayElemAt: [
{
$filter: {
input: "$allvalues",
as: "item",
cond: {
$eq: [
"$$item.label",
"used"
]
}
}
},
0
]
},
total: {
$arrayElemAt: [
{
$filter: {
input: "$allvalues",
as: "item",
cond: {
$eq: [
"$$item.label",
"total"
]
}
}
},
0
]
},
}
},
{
$project: {
_id: 1,
result: {
$divide: [
{
$trunc: {
$multiply: [
{
$divide: [
"$used.value",
"$total.value"
]
},
100
]
}
},
100
]
}
}
}

Dynamically Querying From an Input Object

I'm trying to dynamically query a database that looks like this:
db.test.insert({
"_id" : ObjectId("58e574a768afb6085ec3a388"),
"place": "A",
"tests" : [
{
"name" : "1",
"thing" : "X",
"evaluation" : [
{
"_id": ObjectId("58f782fbbebac50d5b2ae558"),
"aHigh" : [1,2],
"aLow" : [ ],
"zHigh" : [ ],
"zLow" : [1,3]
},
{
"_id": ObjectId("58f78525bebac50d5b2ae5c9"),
"aHigh" : [1,4],
"aLow" : [2],
"zHigh" : [ 3],
"zLow" : [ ]
},
{
"_id": ObjectId("58f78695bebac50d5b2ae60e"),
"aHigh" : [ ],
"aLow" : [1,2,3],
"zHigh" : [1,2,3,4],
"zLow" : [ ]
},]
},
{
"name" : "1",
"thing" : "Y",
"evaluation" : [
{
"_id": ObjectId("58f78c37bebac50d5b2ae704"),
"aHigh" : [1,3],
"aLow" : [4],
"zHigh" : [ ],
"zLow" : [3]
},
{
"_id": ObjectId("58f79159bebac50d5b2ae75c"),
"aHigh" : [1,3,4],
"aLow" : [2],
"zHigh" : [2],
"zLow" : [ ]
},
{
"_id": ObjectId("58f79487bebac50d5b2ae7f1"),
"aHigh" : [1,2,3],
"aLow" : [ ],
"zHigh" : [ ],
"zLow" : [1,2,3,4]
},]
}
]
})
db.test.insert({
"_id" : ObjectId("58eba09e51f7f631dd24aa1c"),
"place": "B",
"tests" : [
{
"name" : "2",
"thing" : "Y",
"evaluation" : [
{
"_id": ObjectId("58f7879abebac50d5b2ae64f"),
"aHigh" : [2],
"aLow" : [3 ],
"zHigh" : [ ],
"zLow" : [1,2,3,4]
},
{
"_id": ObjectId("58f78ae1bebac50d5b2ae6db"),
"aHigh" : [ ],
"aLow" : [ ],
"zHigh" : [ ],
"zLow" : [3,4]
},
{
"_id": ObjectId("58f78ae1bebac50d5b2ae6dc"),
"aHigh" : [1,2],
"aLow" : [3,4],
"zHigh" : [ ],
"zLow" : [1,2,3,4]
},]
}
]
})
In order to query the database, I have an object that is created by another part of my program. It comes in the form of:
var outputObject = {
"top": {
"place": [
"A"
]
},
"testing": {
"tests": {
"name": [
"1",
],
"thing": [
"X",
"Y"
]
}
}
}
I then use that outputObject and $match statements within the aggregate framework to execute the query. I have included two queries which do not seem to work.
db.test.aggregate([
{$match: {outputObject.top}},
{$unwind: '$tests'},
{$match: {outputObject.testing}},
{$unwind: '$tests.evaluation'},
{$group: {_id: null, uniqueValues: {$addToSet: "$tests.evaluation._id"}}}
])
db.test.aggregate([
{$match: {$and: [outputObject.top]}},
{$unwind: '$tests'},
{$match: {$and: [outputObject.testing]}},
{$unwind: '$tests.evaluation'},
{$group: {_id: null, uniqueValues: {$addToSet: "$tests.evaluation._id"}}}
])
However, this approach does not seem to be functioning. I have a couple questions:
Do I need to modify the object outputObject before applying it to the $match statement?
Are my queries correct?
Should I be using $and or $in in combination with the $match statement?
What code will produce the desired result?
Currently using mongoDB 3.4.4
You have a couple of problems here. Firstly the array arguments in your input value should rather be compared with $in which many "any of these in the list" in order to match.
The second problem is that that since the paths are "nested" here you actually need to transform to "dot notation" otherwise you have another variant of the first problem where the conditions would be looking in the "test" array for elements that only have the supplied fields you specify in the input.
So unless you "dot notate" the path as well then since your array items also contain "evaluation" which is not supplied in the input, then it would not match as well.
The other issue here, but easily corrected is the "top" and "testing" separation here is not actually needed. Both conditions actually apply within "both" the $match stages in your pipeline. So you could in fact "flatten" that, as the example shows:
var outputObject = {
"top" : {
"place" : [
"A"
]
},
"testing" : {
"tests" : {
"name" : [
"1"
],
"thing" : [
"X",
"Y"
]
}
}
};
function dotNotate(obj,target,prefix) {
target = target || {},
prefix = prefix || "";
Object.keys(obj).forEach(function(key) {
if ( Array.isArray( obj[key] ) ) {
return target[prefix + key] = { "$in": obj[key] };
} else if ( typeof(obj[key]) === "object" ) {
dotNotate(obj[key],target,prefix + key + ".");
} else {
return target[prefix + key] = obj[key];
}
});
return target;
}
// Run the transformation
var queryObject = dotNotate(Object.assign(outputObject.top,outputObject.testing));
This produces queryObject which now looks like:
{
"place" : {
"$in" : [
"A"
]
},
"tests.name" : {
"$in" : [
"1"
]
},
"tests.thing" : {
"$in" : [
"X",
"Y"
]
}
}
And then you can run the aggregation:
db.test.aggregate([
{ '$match': queryObject },
{ '$unwind': "$tests" },
{ '$match': queryObject },
{ '$unwind': "$tests.evaluation" },
{ '$group': {
'_id': null,
'uniqueValues': {
'$addToSet': "$tests.evaluation._id"
}
}}
])
Which correctly filters the objects
{
"_id" : null,
"uniqueValues" : [
ObjectId("58f79487bebac50d5b2ae7f1"),
ObjectId("58f79159bebac50d5b2ae75c"),
ObjectId("58f782fbbebac50d5b2ae558"),
ObjectId("58f78c37bebac50d5b2ae704"),
ObjectId("58f78525bebac50d5b2ae5c9"),
ObjectId("58f78695bebac50d5b2ae60e")
]
}
Please note that the conditions you supply here actually matches all documents and array entries you supplied in your question anyway. But it will of course actually remove anything that does not match.
Also ideally the "initial" query would rather use $elemMatch
{
"place" : {
"$in" : [
"A"
]
},
"tests": {
"$elemMatch": {
"name" : { "$in" : [ "1" ] },
"thing" : { "$in" : [ "X", "Y" ] }
}
}
}
Which would actually filter all of the documents properly in the initial query stage, since it would only select documents that actually had array elements which did in fact match "only" those conditions as opposed to the dot notated form in the "initial" query which would also return documents where the notated conditions for the "test" array were met in "any element" instead of "both conditions" on the element. But that may be another exercise to consider as the restructured query can apply to both the initial and "inner" filters without the $elemMatch.
Actually with thanks to this nice solution to a "Deep Object Merge" without additional library dependencies, you can use the $elemMatch like this:
var outputObject = {
"top" : {
"place" : [
"A"
]
},
"testing" : {
"tests" : {
"name" : [
"1"
],
"thing" : [
"X",
"Y"
]
}
}
};
function dotNotate(obj,target,prefix) {
target = target || {},
prefix = prefix || "";
Object.keys(obj).forEach(function(key) {
if ( Array.isArray( obj[key] ) ) {
return target[prefix + key] = { "$in": obj[key] };
} else if ( typeof(obj[key]) === "object" ) {
dotNotate(obj[key],target,prefix + key + ".");
} else {
return target[prefix + key] = obj[key];
}
});
return target;
}
function isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
function mergeDeep(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (var key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
mergeDeep(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return mergeDeep(target, ...sources);
}
var queryObject = dotNotate(Object.assign(outputObject.top,outputObject.testing));
// Replace dot with $elemMatch
var initialQuery = Object.keys(queryObject).map( k => (
( k.split(/\./).length > 1 )
? { [k.split(/\./)[0]]: { "$elemMatch": { [k.split(/\./)[1]]: queryObject[k] } } }
: { [k]: queryObject[k] }
)).reduce((acc,curr) => mergeDeep(acc,curr),{})
db.test.aggregate([
{ '$match': initialQuery },
{ '$unwind': "$tests" },
{ '$match': queryObject },
{ '$unwind': "$tests.evaluation" },
{ '$group': {
'_id': null,
'uniqueValues': {
'$addToSet': "$tests.evaluation._id"
}
}}
])
With the pipeline being sent to the server as:
[
{
"$match" : {
"place" : {
"$in" : [
"A"
]
},
"tests" : {
"$elemMatch" : {
"name" : {
"$in" : [
"1"
]
},
"thing" : {
"$in" : [
"X",
"Y"
]
}
}
}
}
},
{
"$unwind" : "$tests"
},
{
"$match" : {
"place" : {
"$in" : [
"A"
]
},
"tests.name" : {
"$in" : [
"1"
]
},
"tests.thing" : {
"$in" : [
"X",
"Y"
]
}
}
},
{
"$unwind" : "$tests.evaluation"
},
{
"$group" : {
"_id" : null,
"uniqueValues" : {
"$addToSet" : "$tests.evaluation._id"
}
}
}
]
Also your $group is probably better written as:
{ "$group": { "_id": "$tests.evaluation._id" } }
Which returns "distinct" just like $addToSet does, but also puts the output into separate documents, instead of trying to combine into "one" which is probably not the best practice and could in extreme cases break the BSON limit of 16MB. So it is generally better to obtain "distinct" in that way instead.
It is better to agree on a fixed format for outputObject and write aggregation query accordingly.
You can now process the outputObject to inject the query operators and transform the keys to match the fields.
Something like below.
{
"top": {
"place": {
"$in": [
"A"
]
}
},
"testing": {
"tests.name": {
"$in": [
"1"
]
},
"tests.thing": {
"$in": [
"X",
"Y"
]
}
}
}
JS Code
var top = outputObject.top;
Object.keys(top).forEach(function(a) {
top[a] = {
"$in": top[a]
};
});
var testing = outputObject.testing;
Object.keys(testing).forEach(function(a) {
Object.keys(testing[a]).forEach(function(b) {
var c = [a + "." + b];
testing[c] = {
"$in": testing[a][b]
};
})
delete testing[a];
});
You can now use your aggregation query
db.test.aggregate([{
$match: top
},
{
$unwind: "$tests"
},
{
$match: testing
},
{
$unwind: "$tests.evaluation"
},
{
$group: {
_id: null,
uniqueValues: {
$addToSet: "$tests.evaluation._id"
}
}
}
])
You can refactor your code to use below aggregation pipeline in 3.4
Process your output object ( includes $in operator ) to
{
"top": {
"place": {
"$in": [
"A"
]
}
},
"testing": {
"tests": {
"name": [
"1"
],
"thing": [
"X",
"Y"
]
}
}
};
JS Code
var top = outputObject.top;
Object.keys(top).forEach(function(a) {top[a] = {"$in":top[a]};});
Aggregation:
[
{
"$match": top
},
{
"$addFields": {
"tests": {
"$filter": {
"input": "$$tests",
"as": "res",
"cond": {
"$and": [
{
"$in": [
"$$res.name",
outputObject.testing.tests.name
]
},
{
"$in": [
"$$res.thing",
outputObject.testing.tests.thing
]
}
]
}
}
}
}
},
{
"$unwind": "$tests.evaluation"
},
{
"$group": {
"_id": null,
"uniqueValues": {
"$addToSet": "$tests.evaluation._id"
}
}
}
]