How I Can get the result set with IDs which are not there in MongoDB Collection.
Consider the below scenario :
I am passing 5 IDs Eg : [1,2,3,4,5] to check whether its exist or not in the collection.
out of 5, only 3 IDs are present [2,3,5]
then as a result, I should get [1,4]
Is there any way to achieve this with the help of aggregation or any other method in MongoDb.
I would personally just do it with some code, using distinct like so:
const ids = [1,2,3,4,5];
const existing = await db.collection.distinct('id', { id: { $in: ids } })
const output = ids.filter(i => !existing.includes(i))
You can also achieve this in a single call, but it has some overhead compared to this approach:
const ids = [1,2,3,4,5];
const output = db.collection.aggregate([
{
$match: {
id: { $in: ids },
},
},
{
$group: {
_id: null,
ids: { $addToSet: '$id' },
},
},
{
$project: {
ids: { $setDifference: [ids, '$ids'] },
},
},
]);
Related
I have two collections: Sharing and Material.
Sharing:
{
from_id: 2
to_id: 1
material_id: material1
}
Material:
{
_id: material1
organization_id: 2
},
{
_id: material2
organization_id: 1
},
{
_id: material3
organization_id: 1
},
--Edit:
There are three materials, 2 belong to organization_id(1) and 1 belongs to organization_id(2). The organization_id does not match 1 in material1 (and instead belongs to material2), but in the Sharing collection, the to_id does match 1. If the match exists, I'd like to find the Material document _id which is equal to the material_id of Sharing AND find the Material documents where the organization_id is equal to 1.
I'd like to check if a field in Sharing (to_id) has a value that is equal to a field in Material (organization_id) AND check if organization_id is equal to 1. If there is a document that exists from this, do another check to find whether the _id of Material is equal to the material_id of Sharing and return all documents & the total count.
If there is no equal value, I'd like to omit that result and send the object with only organization_id equal to 1 and get the total count of this result.
Right now, I do it in a very inefficient way using .map() to find this. Below is my code:
export const getMaterials = async (req, res) => {
const sharing = await Sharing.find({to_id: 1});
let doneLoad;
try {
if (sharing && sharing.length>0) {
const sharingTotal = await Material.find( {$or: [ {organization_id: 1}, {_id: sharing.map((item) => item.material_id)} ] } ).countDocuments();
const sharingMats = await Material.find( {$or: [ {organization_id: 1}, {_id: sharing.map((item) => item.material_id)} ] } );
res.status(200).json({data: sharingMats});
doneLoad= true;
}
else if (!doneLoad) {
const materialTotal = await Material.find({organization_id: 1}).countDocuments();
const materials = await Material.find({organization_id: 1});
res.status(200).json({data: materials});
}
} catch (error) {
res.status(404).json({ message: error.message });
}
}
I have tried using aggregation to get my desired result but I cannot find any solution that fits my requirements. Any help would be great as I am quite new to using mongodb. Thanks.
Edit (desired result):
Materials: [
{
_id: material1,
organization_id: 1
},
{
_id: material2,
organization_id: 1
},
{
_id: material3,
organization_id: 1
}
]
You can use sub-pipeline in a $lookup to perform the filtering. $addFields the count using $size later.
db.Sharing.aggregate([
{
"$match": {
to_id: 1
}
},
{
"$lookup": {
"from": "Material",
"let": {
to_id: "$to_id",
material_id: "$material_id"
},
"pipeline": [
{
"$match": {
$expr: {
$or: [
{
$eq: [
"$$to_id",
"$organization_id"
]
},
{
$eq: [
"$$material_id",
"$_id"
]
}
]
}
}
},
{
"$addFields": {
"organization_id": 1
}
}
],
"as": "materialLookup"
}
},
{
"$addFields": {
"materialCount": {
$size: "$materialLookup"
}
}
}
])
Here is the Mongo playground for your reference.
I want to query using _id in array of object.
const members = [
{ _id: 60fb6f2859fd441f38e21172 },
{ _id: 60fb70a059fd441f38e21175 },
{ _id: 60fb6d9459fd441f38e2116c }
]
I know if it's an simple array, we can use $in operator. Please help how to query using above array.
const files = await File.find({ _id: { $in: members._id } });
I have a collection containing a subcollection. In one request, I would like to update a record in the subcollection or append to it if a match doesn't exist. For a bonus point I would also like this update to be a merge rather than an overwrite.
A crude example:
// Schema
{
subColl: [
{
name: String,
value: Number,
other: Number,
},
];
}
// Existing record
{
_id : 123,
subColl: [
{name: 'John',
value: 10,
other: 20}
]
}
// example
const update = { _id: 123, name: 'John', other: 1000 };
const { _id, name, other } = update;
const doc = await Schema.findById(_id);
const idx = doc.subColl.findIndex(({ name: nameInDoc }) => nameInDoc === name);
if (idx >= 0) {
doc.subColl[idx] = { ...doc.subColl[idx], other };
} else {
doc.subColl.push({ name, other });
}
doc.save();
Currently I can achieve this result by pulling the record, and doing the update/append manually but I am assuming that achieving it with a pure mongo query would be much faster.
I have tried:
Schema.findOneAndUpdate(
{ _id: 123, 'subColl.name': 'John' },
{ $set: { 'subColl.$': [{ name: 'John', other: 1000 }] } }
)
but this won't handle the append behaviour and also doesn't merge the object with the existing record, rather it overwrites it completely.
I am not sure is there any straight way to do this in single query,
Update with aggregation pipeline starting from MongoDB v4.2,
$cond to check name is in subColl array,
true condition, need to merge with existing object, $map to iterate loop, check condition if matches condition then merge new data object with current object using $mergeObjects
false condition, need to concat arrays, current subColl array and new object using $concatArrays
const _id = 123;
const update = { name: 'John', other: 1000 };
Schema.findOneAndUpdate(
{ _id: _id },
[{
$set: {
subColl: {
$cond: [
{ $in: [update.name, "$subColl.name"] },
{
$map: {
input: "$subColl",
in: {
$cond: [
{ $eq: ["$$this.name", update.name] },
{ $mergeObjects: ["$$this", update] },
"$$this"
]
}
}
},
{ $concatArrays: ["$subColl", [update]] }
]
}
}
}]
)
Playground
I have an Event document structured like so and I'm trying to query against the employeeResponses array to gather all responses (which may or may not exist) for a single employee:
[
{
...
eventDate: 2019-10-08T03:30:15.000+00:00,
employeeResponses: [
{
_id:"5d978d372f263f41cc624727",
response: "Available to work.",
notes: ""
},
...etc
];
}
];
My current mongoose aggregation is:
const eventResponses = await Event.aggregate([
{
// find all events for a selected month
$match: {
eventDate: {
$gte: startOfMonth,
$lte: endOfMonth,
},
},
},
{
// unwind the employeeResponses array
$unwind: {
path: "$employeeResponses",
preserveNullAndEmptyArrays: true,
},
},
{
$group: {
_id: null,
responses: {
$push: {
// if a response id matches the employee's id, then
// include their response; otherwise, it's a "No response."
$cond: [
{ $eq: ["$employeeResponses._id", existingMember._id] },
"$employeeResponses.response",
"No response.",
],
},
},
},
},
{ $project: { _id: 0, responses: 1 } },
]);
As you'll no doubt notice, the query above won't work after more than 1 employee records a response because it treats each individual response as a T/F condition, instead of all of the responses within the employeeResponses array as a single T/F condition.
As a result, I had remove all subsequent queries after the initial $match and do a manual reduce:
const responses = eventResponses.reduce((acc, { employeeResponses }) => {
const foundResponse = employeeResponses.find(response => response._id.equals(existingMember._id));
return [...acc, foundResponse ? foundResponse.response : "No response."];
}, []);
I was wondering if it's possible to achieve the same reduce result above, but perhaps using mongo's $reduce function? Or refactor the aggregation query above to treat all responses within the employeeResponses as a single T/F condition?
The ultimate goal of this aggregation is extract any previously recorded employee's responses and/or lack of a response from each found Event within a current month and place their responses into a single array:
["I want to work.", "Available to work.", "Not available to work.", "No response.", "No response." ...etc]
You can use $filter with $map to reshape your data and filter by _id. Then you can keep using $push with $ifNull to provide default value if an array is empty:
db.collection.aggregate([
{
$addFields: {
employeeResponses: {
$map: {
input: {
$filter: {
input: "$employeeResponses",
cond: {
$eq: [ "$$this._id", "5d978d372f263f41cc624727"]
}
}
},
in: "$$this.response"
}
}
}
},
{
$group: {
_id: null,
responses: { $push: { $ifNull: [ { $arrayElemAt: [ "$employeeResponses", 0 ] }, "No response" ] } }
}
}
])
Mongo Playground
So I have 2 models: Question and Answer.
Answer has: questionId, userId, answer (String).
I need an aggregation pipline that will:
match all answers by questionId
see if the current user already voted (is his id in matched documents)
group answers and count them
I implemented 1 and 3 like this:
const q = ObjectId('5d6e52a68558b63fb9302efd');
const user = ObjectId('5d0b3f7daceeb50c477b49e0');
Answer.aggregate([
{ $match: { questionId: q } },
{ $group: { _id: '$answer', count: { $sum: 1 } } },
])
I am missing a step between those 2 aggregation pipelines, where I would iterate thru matched documents, and check if userId matches user.
I would like to get some object like this:
{
didIVote: true,
result: [ { _id: 'YES', count: 5 }, { _id: 'NO', count: 2 } ]
}
Or maybe even like this:
[
{ _id: 'YES', count: 5, didIVote: true },
{ _id: 'NO', count: 2, didIVote: false },
]
In the $group stage, create an array with the users that voted
for each answer.
Add an aditional $project stage to check if the user is in the array.
const q = ObjectId('5d6e52a68558b63fb9302efd');
const user = ObjectId('5d0b3f7daceeb50c477b49e0');
Answer.aggregate([
{ $match: { questionId: q } },
{
$group: {
_id: '$answer',
count: { $sum: 1 },
voted: { $addToSet: "$userId" }
}
},
{
$project: {
count: 1,
didIVote: { $in: [ user, "$voted" ] },
}
}
]);