Dynamically Querying From an Input Object - mongodb

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"
}
}
}
]

Related

update object in an array if exists Push if not exists

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

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

Query nested array from document

Given the following document data in collection called 'blah'...
[
{
"_id" : ObjectId("60913f55987438922d5f0db6"),
"procedureCode" : "code1",
"description" : "Description 1",
"coding" : [
{
"system" : "ABC",
"code" : "L111"
},
{
"system" : "DEFG",
"code" : "S222"
}
]
},
{
"_id" : ObjectId("60913f55987438922d5f0dbc"),
"procedureCode" : "code2",
"description" : "Description 2",
"coding" : [
{
"system" : "ABC",
"code" : "L999"
},
{
"system" : "DEFG",
"code" : "X3333"
}
]
}
]
What I want to get is all of the coding elements where system is ABC for all parents, and an array of codes like so.
[
{ "code": "L111" },
{ "code": "L999" },
]
If I use db.getCollection('blah').find({"coding.system": "ABC"}) I get the parent document with any child in the coding array of ICD.
If I use...
db.getCollection("blah")
.find({ "coding.system": "ABC" })
.projection({ "coding.code": 1 })
I do get the parent documents which have a child with a system of "ABC", but the coding for "DEFG" seems to come along for the ride too.
{
"_id" : ObjectId("60913f55987438922d5f0db6"),
"coding" : [
{
"code" : "L989"
},
{
"code" : "S102"
}
]
},
{
"_id" : ObjectId("60913f55987438922d5f0dbc"),
"coding" : [
{
"code" : "L989"
},
{
"code" : "X382"
}
]
}
I have also tried experimenting with:
db.getCollection("blah").aggregate(
{ $unwind: "$coding" },
{ $match: { "system": "ICD" } }
);
.. as per this page: mongoDB query to find the document in nested array
... but go no where fast with that approach. i.e. no records at all.
What query do I need, please, to achieve something like this..?
[
{ "code": "L111" },
{ "code": "L999" },
...
]
or even better, this..?
[
"L111",
"L999",
...
]
db.collection.aggregate([
{
$match: { "coding.system": "ABC" }
},
{
$unwind: "$coding"
},
{
$match: { "coding.system": "ABC" }
},
{
$project: { code: "$coding.code" }
}
])
mongoplayground
db.collection.aggregate([
{
$match: { "coding.system": "ABC" }
},
{
$unwind: "$coding"
},
{
$match: { "coding.system": "ABC" }
},
{
$group: {
_id: null,
coding: { $push: "$coding.code" }
}
}
])
mongoplayground
Instead of $unwind, $match you can also use $filter:
db.collection.aggregate([
{ $match: { "coding.system": "ABC" } },
{
$project: {
coding: {
$filter: {
input: "$coding",
cond: { $eq: [ "$$this.system", "ABC" ] }
}
}
}
}
])

How to update array of objects to LowerCase in mongodb?

I need to update the role in team array to lowercase.
db.users.find().pretty().limit(1)
{
"_id" : ObjectId("5d9fd81d3d598088d2ea5dc9"),
"employed" : "USA-Atlanta",
"firstName" : "Rory",
"siteRole" : "super admin",
"status" : "active",
"team" : [
{
"name" : "SALES AND MARKETING",
"displayName" : "S&M",
"role" : "Manager"
}
]
}
Tried this code.I m getting it with normal fields.
db.users.find( {}, { 'role': 1 } ).forEach(function(doc) {
db.users.update(
{ _id: doc._id},
{ $set : { 'role' : doc.role.toLowerCase() } },
{ multi: true }
)
});
sample output
"team" : [
{
"name" : "SALES AND MARKETING",
"displayName" : "S&M",
"role" : "manager"
}
]
I think the below Aggregation query is what you are looking for
var count = 0;
db.users.aggregate([
{
"$match": {
"team.role": {$exists: true}
}
},
{
"$project": {
"_id": 1,
// "team": 1,
"teamModified": {
"$map": {
"input": "$team",
"as": "arrayElems",
"in": {
"$mergeObjects": [
"$$arrayElems",
{"role": {"$toLower": "$$arrayElems.role"}}
]
}
}
}
}
},
]).forEach(function(it) {
db.users.updateOne({
"_id": it["_id"]
}, {
"$set": {
"team": it["teamModified"]
}
})
printjson(++count);
})
printjson("DONE!!!")
Note: I haven't tested the script properly in my local, so do let me know if it didn't help you out

MongoDB $cond with embedded document array

I am trying to generate a new collection with a field 'desc' having into account a condition in field in a documment array. To do so, I am using $cond statement
The origin collection example is the next one:
{
"_id" : ObjectId("5e8ef9a23e4f255bb41b9b40"),
"Brand" : {
"models" : [
{
"name" : "AA"
},
{
"name" : "BB"
}
]
}
}
{
"_id" : ObjectId("5e8ef9a83e4f255bb41b9b41"),
"Brand" : {
"models" : [
{
"name" : "AG"
},
{
"name" : "AA"
}
]
}
}
The query is the next:
db.runCommand({
aggregate: 'cars',
'pipeline': [
{
'$project': {
'desc': {
'$cond': {
if: {
$in: ['$Brand.models.name',['BB','TC','TS']]
},
then: 'Good',
else: 'Bad'
}
}
}
},
{
'$project': {
'desc': 1
}
},
{
$out: 'cars_stg'
}
],
'allowDiskUse': true,
})
The problem is that the $cond statement is always returning the "else" value. I also have tried $or statement with $eq or the $and with $ne, but is always returning "else".
What am I doing wrong, or how should I fix this?
Thanks
Since $Brand.models.name returns an array, we cannot use $in operator.
Instead, we can use $setIntersection which returns an array that contains the elements that appear in every input array
db.cars.aggregate([
{
"$project": {
"desc": {
"$cond": [
{
$gt: [
{
$size: {
$setIntersection: [
"$Brand.models.name",
[
"BB",
"TC",
"TS"
]
]
}
},
0
]
},
"Good",
"Bad"
]
}
}
},
{
"$project": {
"desc": 1
}
},
{
$out: 'cars_stg'
}
])
MongoPlayground | Alternative $reduce