Mongoose get only specific object from array or empty array if there is none - mongodb

So I have a questionnaire model:
const schema = new mongoose.Schema({
title: String,
category: String,
description: String,
requirementOption: String,
creationDate: String,
questions: [],
answers: []
})
As you can see the answers is an array. This array contains object that have this structure
{
"participantEmail": "someEmail#email.email"
"currentIndex": 14,
...
}
Now I want to get a specific questionnaire by id, but in answers array I only want specific participant email. So the answers array should have either one element or no element. But I don't want to get null result if there is no such email in the answers array.
I figure it out how to get that specific element from array with this query:
dbModel.findOne({_id: id, 'answers': {$elemMatch: {participantEmail: "someEmail#email.com"}}}, {'answers.$': 1}).exec();
And if that email exists in the answer array I will get this:
"data": {
"questionnaireForParticipant": {
"id": "5d9ca298cba039001b916c55",
"title": null,
"category": null,
"creationDate": null,
"description": null,
"questions": null,
"answers": [
{
"participantEmail": "someEmail#email.com",
....
}
}
}
But if that email is not in the answers array I will get only null. Also I would like to get the title and category and all of the other fields. But I can't seem to find a way to do this.

Since you've this condition 'answers': {$elemMatch: {participantEmail: "someEmail#email.com"}} in filter part of .findOne() - If for given _id document there are no elements in answers. participantEmail array match with input value "someEmail#email.com" then .findOne() will return null as output. So if you wanted to return document irrespective of a matching element exists in answers array or not then try below query :
db.collection.aggregate([
{
$match: { "_id": ObjectId("5a934e000102030405000000") }
},
/** addFields will re-create an existing field or will create new field if there is no field with same name */
{
$addFields: {
answers: {
$filter: { // filter will result in either [] or array with matching elements
input: "$answers",
cond: { $eq: [ "$$this.participantEmail", "someEmail#email.com" ] }
}
}
}
}
])
Test : mongoplayground
Ref : aggregation-pipeline
Note : We've used aggregation as you wanted to return either answers array with matched element or an empty array. Also you can use $project instead of $addFields to transform the output as you wanted to.

The accepted answer is correct, but if you are using mongoose like I do this is how you have to write the accepted answer query:
dbModel.aggregate([
{
$match: { "_id": mongoose.Types.ObjectId("5a934e000102030405000000") }
}]).addFields({
answers: {
$filter: {
input: "$answers",
cond: { $eq: [ "$$this.participantEmail", "someEmail#email.com" ] }
}
}
}).exec();

With this sample input document:
{
_id: 1,
title: "t-1",
category: "cat-abc",
creationDate: ISODate("2020-05-05T07:01:09.853Z"),
questions: [ ],
answers: [
{ participantEmail: "someEmail#email.email", currentIndex: 14 }
]
}
And, with this query:
EMAIL_TO_MATCH = "someEmail#email.email"
db.questionnaire.findOne(
{ _id: 1 },
{ title: 1, category: 1, answers: { $elemMatch: { participantEmail: EMAIL_TO_MATCH } } }
)
The query returns (when the answers.participantEmail matches):
{
"_id" : 1,
"title" : "t-1",
"category" : "cat-abc",
"answers" : [
{
"participantEmail" : "someEmail#email.email",
"currentIndex" : 12
}
]
}
And, when the answers.participantEmail doesn't match or if the amswers array is empty, the result is:
{ "_id" : 1, "title" : "t-1", "category" : "cat-abc" }
NOTE: The $elemMatch used in the above query is a projection operator.

Related

MongoDB Comparison query Object Array Operators

Suppose that there is the applicants collection in a MongoDB database with these documents:
[
{
name: "Tom Hanks",
age: 42,
email: "tom.hanks#example.com"
job:{
"data engineer": 7,
"professor": 3
}
},
{
name: "Ken Smith",
age: 36,
email: "ken.smith#example.com"
job:{
"electronics engineer" : 10,
"database administrator" : 5
}
}
]
I want to write a query that retrieves the applicants who have some experience in the database field.
Tried: db.applications.find({ 'job': { $all: [ '.data.'] } })
However it's not working.
Can someone help, please.
I'm not sure how efficient this is or if its what you're looking for, but you can have a go at expanding the object to an array and then regex matching on each field using the $objectToArray operator and then regex matching on the keys
{
$project: {
jobMatch: {
$objectToArray: "$job"
}
}
},
{
$match: {
"jobMatch.k": {
$regex: "data"
}
}
}
])
You can go deeper and add a value match by doing values greater than a certain number with $and, let me know if you found it useful

MongoDb 4.x Query with projection without empty entries using find

We are now using mongo-db to store data from tests. I am using Mongo-Shell
The document is structured like this:
{
static1:"abc",
static2:"xyz",
static3:"asd", [...],
nested:[
{
data1: "d1",
data2: "d2",
},
{
data1: "dx",
data4: "d4",
data5: "d5",
data6: "d6",
},
{
data1: "ds",
data8:"data8"
}, [...]
]
}
So the static-data is always in the same structure, but for every measurement the object can look different. It could be voltage with upper, lower and actual value. Or just comparison of a target value and an actual value. Highly dynamic.
Same data1-name represent same attributes.
Now e.g. I want to display only some static data and one (or several) attributes of a nested document.
I am using this query:
find({}, {_id:0, data1:1, "nested.data8":1}).pretty()
As expected, only the static-data1 is displayed, but the dynamic measurements has lots of empty objects in the shell-output
Example-Output:
{
"static1" : "123",
"nested" : [
{ },
{ },
{ }, [...] ,
{ "data8" : "OK" }
] }
The desired output would be:
{
"static1" : "123",
"nested" : [
{ "data8" : "d8" }
]
}
I also tried this query on the mongo shell:
aggregate( { $addFields: {"static":"$static1", "data8":"$nested.data8"} }, { $project:{"static1":1, "nested.data8":1} } ).pretty()
But the result is the same.
I hope there is a ways to get rid of the empty documents in the output.
Thanks
You can try $filter operator to filter result of nested array by checking condition not equal to empty object {}
db.collection.aggregate([
{
$project: {
_id: 0,
data1: 1,
"nested.data8": 1
}
},
{
$set: {
nested: {
$filter: {
input: "$nested",
cond: { $ne: ["$$this", {}] }
}
}
}
}
])
Playground

Filter only documents that have ALL FIELDS non null (with aggregation framework)

I have many documents, but I want to figure out how to get only documents that have ALL FIELDS non null.
Suppose I have these documents:
[
{
'a': 1,
'b': 2,
'c': 3
},
{
'a': 9,
'b': 12
},
{
'a': 5
}
]
So filtering the documents, only the first have ALL FIELDS not null. So filtering out these documents, I would get only the first. How can I do this?
So when you wanted to get only the documents which have ALL FIELDS, without specifying all of them in filter query like this : { a: {$exists : true}, b : {$exists : true}, c : {$exists : true}} then it might not be a good idea, in other way technically if you've 10s of fields in the document then it wouldn't either be a good idea to mention all of them in the query. Anyhow as you don't want to list them all - We can try this hack if it performs well, Let's say if you've a fixed schema & say that all of your documents may contain only fields a, b & c (_id is default & exceptional) but nothing apart from those try this :
If you can get count of total fields, We can check for field count which says all fields do exists, Something like below :
db.collection.aggregate([
/** add a new field which counts no.of fields in the document */
{
$addFields: { count: { $size: { $objectToArray: "$$ROOT" } } }
},
{
$match: { count: { $eq: 4 } } // we've 4 as 3 fields + _id
},
{
$project: { count: 0 }
}
])
Test : mongoplayground
Note : We're only checking for field existence but not checking for false values like null or [] or '' on fields. Also this might not work for nested fields.
Just in case if you wanted to check all fields exist in the document with their names, So if you can pass all fields names as input, then try below query :
db.collection.aggregate([
/** create a field with all keys/field names in the document */
{
$addFields: {
data: {
$let: {
vars: { data: { $objectToArray: "$$ROOT" } },
in: "$$data.k"
}
}
}
},
{
$match: { data: { $all: [ "b", "c", "a" ] } } /** List down all the field names from schema */
},
{
$project: { data: 0 }
}
])
Test : mongoplayground
Ref : aggregation-pipeline
You can try to use explain to check your queries performance.

MongoDB : Retrieve Associated Value from Object in an Array of Arrays

In mongo I have a documents that follow the below pattern :
{
name: "test",
codes: [
[
{
code: "abc",
value: 123
},
{
code: "def",
value: 456
},
],
[
{
code: "ghi",
value: 789
},
{
code: "jkl",
value: 012
},
]
]
}
I'm using an aggregate query (because of joins) and in a $project block I need to return the "name" and the value of the object that has a code of "def" if it exists and an empty string if it doesn't.
I can't simply $unwind codes and $match because the "def" code is not guaranteed to be there.
$filter seems like the right approach as $elemMatch doesn't work, but its not obvious to me how to do this on nested array of arrays.
You can try below query, instead of unwinds & filter this can give you required result with less docs to operate on :
db.collection.aggregate([
/** merge all arrays inside codes array into code array */
{
$addFields: {
codes: {
$reduce: {
input: '$codes',
initialValue: [],
in: { $concatArrays: ["$$value", "$$this"] }
}
}
}
},
/** project only needed fields & value will be either def value or '',
* if 'def' exists in any doc then we're check index of it to get value of that particular object using arrayElemAt */
{
$project: {
_id:0, name: 1, value:
{
$cond: [{ $in: ["def", '$codes.code'] }, { $arrayElemAt: ['$codes.value', { $indexOfArray: ["$codes.code", 'def'] }] }, '']
}
}
}])
Test : MongoDB-Playground

way to update multiple documents with different values

I have the following documents:
[{
"_id":1,
"name":"john",
"position":1
},
{"_id":2,
"name":"bob",
"position":2
},
{"_id":3,
"name":"tom",
"position":3
}]
In the UI a user can change position of items(eg moving Bob to first position, john gets position 2, tom - position 3).
Is there any way to update all positions in all documents at once?
You can not update two documents at once with a MongoDB query. You will always have to do that in two queries. You can of course set a value of a field to the same value, or increment with the same number, but you can not do two distinct updates in MongoDB with the same query.
You can use db.collection.bulkWrite() to perform multiple operations in bulk. It has been available since 3.2.
It is possible to perform operations out of order to increase performance.
From mongodb 4.2 you can do using pipeline in update using $set operator
there are many ways possible now due to many operators in aggregation pipeline though I am providing one of them
exports.updateDisplayOrder = async keyValPairArr => {
try {
let data = await ContestModel.collection.update(
{ _id: { $in: keyValPairArr.map(o => o.id) } },
[{
$set: {
displayOrder: {
$let: {
vars: { obj: { $arrayElemAt: [{ $filter: { input: keyValPairArr, as: "kvpa", cond: { $eq: ["$$kvpa.id", "$_id"] } } }, 0] } },
in:"$$obj.displayOrder"
}
}
}
}],
{ runValidators: true, multi: true }
)
return data;
} catch (error) {
throw error;
}
}
example key val pair is: [{"id":"5e7643d436963c21f14582ee","displayOrder":9}, {"id":"5e7643e736963c21f14582ef","displayOrder":4}]
Since MongoDB 4.2 update can accept aggregation pipeline as second argument, allowing modification of multiple documents based on their data.
See https://docs.mongodb.com/manual/reference/method/db.collection.update/#modify-a-field-using-the-values-of-the-other-fields-in-the-document
Excerpt from documentation:
Modify a Field Using the Values of the Other Fields in the Document
Create a members collection with the following documents:
db.members.insertMany([
{ "_id" : 1, "member" : "abc123", "status" : "A", "points" : 2, "misc1" : "note to self: confirm status", "misc2" : "Need to activate", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") },
{ "_id" : 2, "member" : "xyz123", "status" : "A", "points" : 60, "misc1" : "reminder: ping me at 100pts", "misc2" : "Some random comment", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") }
])
Assume that instead of separate misc1 and misc2 fields, you want to gather these into a new comments field. The following update operation uses an aggregation pipeline to:
add the new comments field and set the lastUpdate field.
remove the misc1 and misc2 fields for all documents in the collection.
db.members.update(
{ },
[
{ $set: { status: "Modified", comments: [ "$misc1", "$misc2" ], lastUpdate: "$$NOW" } },
{ $unset: [ "misc1", "misc2" ] }
],
{ multi: true }
)
Suppose after updating your position your array will looks like
const objectToUpdate = [{
"_id":1,
"name":"john",
"position":2
},
{
"_id":2,
"name":"bob",
"position":1
},
{
"_id":3,
"name":"tom",
"position":3
}].map( eachObj => {
return {
updateOne: {
filter: { _id: eachObj._id },
update: { name: eachObj.name, position: eachObj.position }
}
}
})
YourModelName.bulkWrite(objectToUpdate,
{ ordered: false }
).then((result) => {
console.log(result);
}).catch(err=>{
console.log(err.result.result.writeErrors[0].err.op.q);
})
It will update all position with different value.
Note : I have used here ordered : false for better performance.