I have a MongoDB Model which consist of array of members as obejcts.
const guestSchema = new mongoose.Schema({
salutation: {
type: String,
},
members: [membersSchema],
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
},
});
Members Schema:
const membersSchema = new mongoose.Schema({
name: String,
status: {
type: String,
enum: ['regular', 'helper'],
default: 'regular',
},
});
I want to achieve of doing an update in case documet with given ID exist or push to an array in case ID with document in array does not exist. I use aggregation pipeline, however I am not able to achieve pushing new document to array. Why can't I use push after else statement like this.
const subDocumentToUpsert = { 'name': mem.name, 'status': mem.status, '_id': ObjectId(mem.id)}
const subDocumentNoID = { 'name': mem.name, 'status': mem.status}
await Guest.findOneAndUpdate(
{ "_id": req.params.id },
[
{
$set: {
members: {
$cond: {
if: { $in: [subDocumentToUpsert._id, '$members._id'] },
then: {
$map: {
input: '$members',
as: 'sub_document',
in: {
$cond: {
if: { $eq: ['$$sub_document._id', subDocumentToUpsert._id] },
then: subDocumentToUpsert,
else: '$$sub_document',
},
},
},
},
else: {
$push: {
subDocumentNoID
},
},
},
},
},
},
},
]);
What is the best way of doing so? Thank you
You can do as follow:
db.collection.update({
_id: {
$in: [
1,
2
]
}
},
[
{
$set: {
members: {
$cond: {
if: {
$in: [
5,
"$members._id"
]
},
then: {
$map: {
input: "$members",
as: "sub",
in: {
$cond: {
if: {
$eq: [
"$$sub._id",
5
]
},
then: {
_id: 5,
status: "regular_updated",
name: "Negan_updated"
},
else: "$$sub"
},
},
},
},
else: {
$concatArrays: [
"$members",
[
{
_id: 5,
status: "regular_upserted",
name: "Negan_upserted"
}
]
]
}
}
}
}
}
}
],
{
multi: true
})
Explained:
Check if _id:5 exist in the subobject and update via $map/$cond only the object that has the _id:5.
In case there is no _id:5 add the new object to the array with $concatArrays.
Playground
Related
With MongoDB aggregate, how do you lookup child documents from an array on the parent document and match a field on the child to a value?
I want to lookup the ShopItems for a Shop. And I only want the ShopItems that are not kidFriendly.
Details:
Let's say I have Shops collection
Shop Schema
{
shopItems: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'ShopItem',
},
],
}
And a shop can have multiple Shop Items:
ShopItem Schema
[
itemName: { type: String },
kidFriendly: { type: Boolean, default: false },
]
I want to lookup the ShopItems for a Shop. And I only want the ShopItems that are not kidFriendly.
I tried
const result3 = await Shop.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId('623ae52ba5b1af0004e1c4ec'),
},
},
{
$lookup: {
from: ShopItem.collection.collectionName,
pipeline: [
{
$match: {
kidFriendly: { $ne: true },
_id: { $in: '$shopItems' },
},
},
],
as: 'adultOnlyItems',
},
},
]);
but I get an error:
$in needs an array.
Example data
// shops
[
{
_id: ObjectId('623ae52ba5b1af0004e1c4ec'),
shopItems: [ObjectId('631e6b133b688a0004a17265'), ObjectId('62f4cc974a255f00044c01b5'), ObjectId('625ffc48eec7b20004c9294c')]
},
{
_id: ObjectId('623ae52ba5b1af0004e1c4eb'),
shopItems: [ObjectId('631e6b133b688a0004a17263')]
}
]
//shop items
[
{
_id: ObjectId('631e6b133b688a0004a17265'),
itemName: "Barbie",
kidFriendly: true
},
{
_id: ObjectId('62f4cc974a255f00044c01b5'),
itemName: "Alcohol",
kidFriendly: false
},
{
_id: ObjectId('625ffc48eec7b20004c9294c'),
itemName: "Glass Vase",
kidFriendly: false
},
{
_id: ObjectId('631e6b133b688a0004a17263'),
itemName: "Beach Ball",
kidFriendly: true
}
]
When you use $lookup with pipeline, you need to have the let to store the field value from shops into variable. And use the $expr operator as well.
db.shops.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId('623ae52ba5b1af0004e1c4ec')
}
},
{
$lookup: {
from: ShopItem.collection.collectionName,
let: {
shopItems: "$shopItems"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$ne: [
"$kidFriendly",
true
]
},
{
$in: [
"$_id",
"$$shopItems"
]
}
]
}
}
}
],
as: "adultOnlyItems"
}
}
])
Demo # Mongo Playground
Reference
Join Conditions and Subqueries on a Joined Collection
I have this Event array but can't figure out how to query into the 'guests' nested array and do two things.
count the number of guests
count the number of attended guests (marked 'Y')
{ _id: new ObjectId('1'),
name: event1,
guests: [
{
phone: +12222222222,
_id: new ObjectId,
attended: 'Y'
},
{
phone: +12344466666,
_id: new ObjectId,
attended: 'Y'
},
{ phone: +11234567890,
_id: new ObjectId,
attended:
},
{
phone: +14443332222,
_id: new ObjectId,
attended: 'Y'
},
{ phone: +19090909090,
_id: new ObjectId
attended:
}
],
},
{ _id: new ObjectId('2'),
name: event2,
guests: [
{
phone: +11111111111,
_id: new ObjectId,
attended:
},
{
phone: +12222222222,
_id: new ObjectId,
attended: 'Y'
},
{
phone: +133333333333,
_id: new ObjectId,
attended: 'Y'
}
],
},
My code below is on its 20th iteration without getting any closer.
const event = await Event.findById(req.params.id);
For MongoDB query, you can use $size for calculating the size of the array field and $filter to filter specific document(s) that matched the criteria in the projection.
db.collection.find({
_id: "1"
},
{
_id: 1,
name: 1,
guests: 1,
totalGuests: {
$size: "$guests"
},
attendedGuests: {
$size: {
"$filter": {
input: "$guests",
cond: {
$eq: [
"$$this.attended",
"Y"
]
}
}
}
}
})
Sample Mongo Playground
Yong Shun helped above but didn't get 100% of the way -- here is the full answer in case anyone encounters something like this in the future:
module.exports.showEvent = async (req, res) => {
const event = await Event.findById(req.params.id).populate('artist');
const { guest_id } = req.cookies;
let totalGuests = 0;
let attendedGuests = 0;
const eventData = await Event.aggregate([
{
"$match": {
"_id": objectId(req.params.id)
}
},
{
$project: {
_id: 1,
name: 1,
guests: 1,
totalGuests: { $cond: { if: { $isArray: "$guests" }, then: { $size: "$guests" }, else: "NA" } },
attendedGuests: {
$size: {
$filter: {
input: "$guests",
as: "guest",
cond: {
$and: [{
$eq: ["$$guest.attended", "Y"]
}]
}
}
}
}
}
}
])
if (eventData && Array.isArray(eventData) && eventData.length > 0) {
totalGuests = eventData[0].totalGuests;
attendedGuests = eventData[0].attendedGuests;
}
if (!event) {
req.flash('error', 'Cannot find that Event');
return res.redirect('/events');
}
res.render('events/show', { event, eventData });
console.log(totalGuests, attendedGuests);
};
I’ve defined the following query which fetches me all items with an id which is in a given list of ids, and a status of either active or retracted.
const query = {
$and : [
{
$or: [
{
status: ‘active’,
},
{
status: ‘retracted’,
},
],
},
{
id: { $in: ids },
},
],
};
Each of these items has a parent_id field, which can either be null if the item does not have a parent, or can be the id of the parent.
I want my query to fetch all items with the ids I supply, as well as their parent items, if such a parent exists.
For example, if I supply the following IDs
[1,2,3]
and item 2 has a parent with id 5, while item 1 and 3 have parent_id set to null, I want my query to return the following items:
[1,2,3,5].
To achieve this I wrote the following query:
const collection = db.collection(‘myCollection’);
const data = await collection.aggregate([
{$match : query},
{
$lookup: {
from: ‘myCollection’,
let: { parentID: ‘$parent_id’},
pipeline: [
{
$match: {
$expr: {
$eq: [‘$id’, ‘$$parentID’],
},
},
},
as: ‘parent’,
},
},
]).sort(‘created_date’, ‘desc’).toArray();
return data;
However, this always returns null.
Sample Data:
[
{
id: 1,
parent_id: 3,
data: ‘bla bla’
},
{
id: 2,
parent_id: null,
data: ‘bla bla bla’
},
{
id: 3,
parent_id: null,
data: ‘bla’
}
]
Input: [1]
Output:
[
{
id: 1,
parent_id: 3,
data: ‘bla bla’
},
{
id: 3,
parent_id: null,
data: ‘bla’
}
]
The approach with $lookup being run upon same collection should work however it gives you a nested array so you need few additional stages to flatten such array and get all elements as on result set:
db.collection.aggregate([
{
$match: { id: { $in: [1] } }
},
{
$lookup: {
from: "collection",
localField: "parent_id",
foreignField: "id",
as: "parent"
}
},
{
$project: {
all: {
$concatArrays: [
"$parent",
[ "$$ROOT" ]
]
}
}
},
{
$project: {
"all.parent": 0
}
},
{
$unwind: "$all"
},
{
$replaceRoot: {
newRoot: "$all"
}
}
])
Mongo Playground
Your aggregation was malformed and lack some "]" for example closing the pipeline fied.
If you fix that the query works fine for me. Example
You can try this. The input array is [2,3] where 2 has parent id=1 and that is not in the input array. But the output array has the entry.
Working Playground
db.collection.aggregate([
{
$match: {
_id: {
$in: [
2,
3
]
}
}
},
{
$lookup: {
from: "collection",
localField: "p",
foreignField: "_id",
as: "parent"
}
},
{
$project: {
_id: 0,
id: {
$concatArrays: [
[
"$_id"
],
"$parent._id"
]
}
}
},
{
$unwind: "$id"
},
{
$sort: {
id: 1
}
}
])
For my DB, I wrote the following pipeline:
let orders = await Order.aggregate(
{
$unwind: "$candidate",
},
{
$lookup: {
from: "groups",
localField: "candidate.groupId",
foreignField: "_id",
as: "groupData",
},
},
{
$lookup: {
from: "users",
let: {
id: "$candidate.groupId",
},
pipeline: [
{ $match: { groupId: { $ne: null } } },
{
$match: {
$expr: { $in: ["$$id", "$groupId"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "members",
},
},
{ $match: { "members._id": new ObjectId(req.userId) } },
{
$lookup: {
from: "users",
let: { ids: "$candidate.autonomousId" },
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$ids"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "candidate",
},
},
{
$project: {
groupData: 1,
members: 1,
candidate: 1,
stillAvailable: 1,
_id: 0,
},
}
).toArray();
The output was the expected...
{ candidate:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' } ],
groupData:
[ { _id: 606632403fffb851b8c41d12,
name: 'Giraia' } ],
members:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' },
{ _id: 60182cbb2b654330d2458f89,
name: 'Jonas'} ] }
The last step in the pipeline would be to compare the arrays, filter which members were not candidates and add them to the array stillAvailable. I tried in many ways but I couldn't achieve my goal with aggregation. The only solution I could find was to process the result of the incomplete pipeline on my backend. The code is:
orders.forEach(
(order) =>
(order.stillAvailable = order.members.filter(
(autonomous) =>
!order.candidate.some((el) => {
return el._id.toString() === autonomous._id.toString();
})
))
);
With that, I reach the expected output...
{ candidate:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' } ],
groupData:
[ { _id: 606632403fffb851b8c41d12,
name: 'Giraia' ],
members:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' },
{ _id: 60182cbb2b654330d2458f89,
name: 'Jonas' ],
stillAvailable:
[ { _id: 60182cbb2b654330d2458f89,
name: 'Jonas' ] }
The problem is to better compartmentalize my code, it would be necessary to realize the last step (done with javascript on my backend) as one more step on the pipeline. Does anyone have an idea how to reach that?
After I wrote the question here, somehow the idea was better structured and I achieved the result, using $map and one more level of $lookup! I left the answer documented here in case someone falls into the same issue.
let orders = await Order.aggregate(
{
$unwind: "$candidate",
},
{
$lookup: {
from: "groups",
localField: "candidate.groupId",
foreignField: "_id",
as: "groupData",
},
},
{
$lookup: {
from: "users",
let: {
id: "$candidate.groupId",
},
pipeline: [
{ $match: { groupId: { $ne: null } } },
{
$match: {
$expr: { $in: ["$$id", "$groupId"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "members",
},
},
{ $match: { "members._id": new ObjectId(req.userId) } },
{
$lookup: {
from: "users",
let: { ids: "$candidate.autonomousId" },
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$ids"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "candidate",
},
},
{
$project: {
groupData: 1,
members: 1,
candidate: 1,
_id: 0,
stillAvailable: {
$setDifference: [
{
$map: {
input: "$members",
as: "member",
in: "$$member._id",
},
},
{
$map: {
input: "$candidate",
as: "el",
in: "$$el._id",
},
},
],
},
},
},
{
$lookup: {
from: "users",
let: {
ids: "$stillAvailable",
},
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$ids"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "stillAvailable",
},
}
).toArray();
I have database model
Schema({
members: [
{
type: String,
required: true,
ref: "User"
}
],
createdAt: {
type: Date,
default: Date.now(),
required: true
},
lastMessage: {
message: {
type: String,
required: true
},
from: {
type: String,
required: true
},
createdAt: {
type: Date,
required: true
}
},
messages: [
{
createdAt: {
type: Date,
required: true
},
message: {
type: String,
required: true
},
from: {
type: String,
ref: "User",
required: true
}
}
]
});
That code I use to find some documents
Chats.countDocuments(
{
members: {
$in: ["userIdOne", "userIdTwo"]
}
},
cb
)
I use $in to find data what I need, but $in find all documents which contains one of this userId.... Me need to find only one document which contains that user's ids.
How can i do this?
Use $all instead
Chats.countDocuments(
{
members: {
$all: ["userIdOne", "userIdTwo"]
}
},
cb
)
If you need exact match, you can use $setEquals aggregation operator.
db.collection.aggregate([
{
$match: {
$expr: {
$eq: [
{
$setEquals: [
"$members",
[
"userIdOne",
"userIdTwo"
]
]
},
true
]
}
}
},
{
$count: "count"
}
])
Input:
[
{
"members": [
"userIdOne"
]
},
{
"members": [
"userIdTwo"
]
},
{
"members": [
"userIdOne",
"userIdTwo"
]
},
{
"members": [
"userIdOne",
"userIdTwo",
"userIdThere"
]
}
]
Output:
[
{
"count": 1
}
]
Playground
You can integrate this to mongoose like this:
const result = await Chats.aggregate([
{
$match: {
$expr: {
$eq: [
{
$setEquals: ["$members", ["userIdOne", "userIdTwo"]]
},
true
]
}
}
},
{
$count: "count"
}
]);
const count = result[0].count;