$lookup for each element within a sub-array - node.js

So in my Database, I have the 3 collections, and they look like this:
Customers:
customers = [
{_id: 1, username: "jack", ... },
{_id: 2, username: "jane", ... }
...
]
Reviews:
reviews = [
{ _id: 1, customerID: 1, message: "my message", ...}
...
]
Comments:
comments = [
{ _id: 1, reviewID: 1, customerID: 2, message: "my response" ...}
...
]
Customers can post reviews, and can also comment on other reviews.
So, what I want is a mongodb aggregation query to:
Retrieve the reviews.
The data of the customer who made the review.
The comments on that review.
The data of the customers who made the comment on that review.
i.e
reviews = [
{
_id: 1,
username: "jack",
message: "my message"
comments: [
{ _id: 1, username: "jane", message: "my response", ...},
...
]
...
}
...
]

You can start from comments collection and $lookup with customers to get customer name, then you can $group all comments by review and $lookup twice (with reviews and customer). Every time you know that it's a one-to-one relationship you can use $unwind after $lookup. Try:
db.comments.aggregate([
{
$lookup: {
from: "customers",
localField: "customerID",
foreignField: "_id",
as: "customer"
}
},
{
$unwind: "$customer"
},
{
$project: {
_id: 1,
reviewID: 1,
username: "$customer.username",
message: 1
}
},
{
$group: {
_id: "$reviewID",
comments: { $push: { _id: "$_id", username: "$username", message: "$message" } }
}
},
{
$lookup: {
from: "reviews",
localField: "_id",
foreignField: "_id",
as: "review"
}
},
{
$unwind: "$review"
},
{
$lookup: {
from: "customers",
localField: "review.customerID",
foreignField: "_id",
as: "customer"
}
},
{
$unwind: "$customer"
},
{
$project: {
_id: 1,
message: "$review.message",
username: "$customer.username",
comments: 1
}
}
])
Outputs:
{ "_id" : 1, "comments" : [ { "_id" : 1, "username" : "jane", "message" : "my response" } ], "message" : "my message", "username" : "jack" }
EDIT:
If you want to start from reviews and filter it out to single movie you can then you can use $lookup with custom pipeline
db.reviews.aggregate([
{
$match: {
movieId: 1,
}
},
{
$lookup: {
from: "customers",
localField: "customerID",
foreignField: "_id",
as: "customer"
}
},
{
$unwind: "$customer"
},
{
$lookup: {
from: "comments",
let: { reviewId: "$_id" },
pipeline: [
{
$match: { $expr: { $eq: [ "$$reviewId", "$reviewID" ] } }
},
{
$lookup: {
from: "customers",
localField: "customerID",
foreignField: "_id",
as: "customer"
}
},
{
$unwind: "$customer"
},
{
$project: {
_id: 1,
message: 1,
username: "$customer.username"
}
}
],
as: "comments"
}
},
{
$project: {
_id: 1,
message: 1,
username: "$customer.username",
comments: 1
}
}
])
Brings the same output

You can use below aggregation with mongodb 3.6 and above
Reviews.aggregate([
{ "$lookup": {
"from": Customers.collection.name,
"let": { "customerID": "$customerID" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$customerID"] } } }
],
"as": "customer"
}},
{ "$unwind": "$customer" },
{ "$lookup": {
"from": Comments.collection.name,
"let": { "reviewID": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$reviewID", "$$reviewID"] } } },
{ "$lookup": {
"from": Customers.collection.name,
"let": { "customerID": "$customerID" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$customerID"] } } }
],
"as": "customer"
}},
{ "$unwind": "$customer" },
],
"as": "comments"
}}
])

Related

MongoDB nested $lookup for 3 levels

MongoDB: How to put all the subcategory in array like child_category? you can see the response, the way its coming. how to merge that clothes category of all sub_category in array
Below is the aggregate lookup:
$Lookup
{
$match: { _id: ObjectId(req.params.id) }
},
{
$lookup: {
from: 'sub_category',
localField: '_id',
foreignField: 'category',
as: 'sub_category',
},
},
{
$unwind: {
path: '$sub_category',
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: 'child_category',
localField: 'sub_category._id',
foreignField: 'sub_category',
as: 'sub_category.child_category',
},
}
Below is the response I get when run this lookup
response
[
{
"_id": "6219f96e2504161acb0f82a4",
"name": "clothes",
"__v": 0,
"sub_category": {
"_id": "6219fcccb2ba385797e02439",
"name": "Shirt",
"category": "6219f96e2504161acb0f82a4",
"__v": 0,
"child_category": [
{
"_id": "6219fdcd477bd10b4d4bbf12",
"name": "striped",
"sub_category": "6219fcccb2ba385797e02439",
"filters": [],
"__v": 0
},
{
"_id": "621a045be776b1775e9c9ec7",
"name": "checked",
"sub_category": "6219fcccb2ba385797e02439",
"filters": []
}
]
}
},
{
"_id": "6219f96e2504161acb0f82a4",
"name": "clothes",
"__v": 0,
"sub_category": {
"_id": "621a0431e776b1775e9c9ebe",
"name": "Pant",
"category": "6219f96e2504161acb0f82a4",
"child_category": []
}
}
]
I want both sub_category to come in array, so that name: clothes wont repeat in array
Add $group at the end of pipeline.
db.collection.aggregate([
{
$lookup: {
from: "sub_category",
localField: "_id",
foreignField: "category",
as: "sub_category"
}
},
{
$unwind: {
path: "$sub_category",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "child_category",
localField: "sub_category._id",
foreignField: "sub_category",
as: "sub_category.child_category"
}
},
{
$group: {
"_id": {
_id: "$_id",
name: "$name"
},
"sub_category": { "$push": "$sub_category" }
}
}
])
mongoplayground

Aggregate with multiple lookup and pipeline return only the last element

I try below code, it's working to lookup value from other collection. But why it only return the last element.
If I omitted the unwind function, It does return all result from the model, but the second lookup will not working as the first lookup return arrays.
My objective is to look up folder that include the model id which represented in templatefolders collection.
const result = await this.dashboardModel
.aggregate([{ $match: filter }])
.lookup({
from: 'templatefolders',
as: 'template',
let: { id: '$_id' },
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: ['$dashboardId', '$$id'],
},
{
$eq: ['$deletedAt', null],
},
],
},
},
},
{
$project: {
_id: 1,
folderId: 1,
},
},
],
})
.unwind('template')
.lookup({
from: 'folders',
as: 'folder',
let: { folderId: '$template.folderId' },
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: ['$_id', '$$folderId'],
},
{
$eq: ['$deletedAt', null],
},
],
},
},
},
{
$project: {
_id: 1,
name: 1,
},
},
],
})
.unwind('folder')
.exec();
return result;
Result
{
"data": [
{
...(parent field)
"template": {
"_id": "60ab22b03b39e40012b7cc4a",
"folderId": "60ab080b3b39e40012b7cc41"
},
"folder": {
"_id": "60ab080b3b39e40012b7cc41",
"name": "Folder 1"
}
}
],
"meta": {},
"success": true,
"message": "Succesfully get list"
}
I came from Front end background. I hope my question is not a silly one.
Thanks!
EDIT:
dashboard: [{
_id: dashboard1
}]
templatefolders: [{
dashboardId: dashboard1,
folderId: folder123
}]
folders: [{
_id: folder123
}]
You can use $lookup to join collections
$lookup to join two collections .Lookup doc
$unwind to deconstruct the array. Unwind doc
$group to reconstruct the array which we already deconstructed Group doc
Here is the code
db.dashboard.aggregate([
{
"$lookup": {
"from": "templatefolders",
"localField": "_id",
"foreignField": "dashboardId",
"as": "joinDashboard"
}
},
{
"$unwind": "$joinDashboard"
},
{
"$lookup": {
"from": "folders",
"localField": "joinDashboard.folderId",
"foreignField": "_id",
"as": "joinDashboard.joinFolder"
}
},
{
"$group": {
"_id": "$_id",
"joinDashboard": {
"$push": "$joinDashboard"
}
}
}
])
Working Mongo playground

Joining mongodb collection based on condition [duplicate]

This question already has an answer here:
Conditional $lookup in MongoDB?
(1 answer)
Closed 3 years ago.
I have two collections in MongoDB and want to join the two collections based on some condition.
I want to join 'order' and 'order-status' table to get all orders assigned to '123' with status 'ready'
orders
{
"_id":"1",
"name": "Fridge",
"assignee": "123"
},
{
"_id":"2",
"name": "TV",
"assignee": "567"
},
{
"_id":"3",
"name": "Music system",
"assignee": "123"
}
order-status
{
"_id":"1",
"status": "ready",
"orderId": "1"
},
{
"_id":"2",
"status": "cancelled",
"orderId": "2"
},
{
"_id":"3",
"status": "cancelled",
"orderId": "3"
}
assignee
{
"_id":"123",
"name": "Jak"
}
{
"_id":"567",
"name": "Mac"
}
I want to join 'order' and 'order-status' table to get all orders assigned to '123' with status 'ready'
Expecting a final result as
[
{
"_id":"1",
"name": "Fridge",
"assignee": "123",
"status": {
"_id":"1",
"status": "ready",
"orderId": "1"
}
}
]
Tried following but how to check order status in another table with lookup
const resultObject = orders.aggregate([
{ $match : {assignee: Objectid('123')} },
{
$lookup: {
from: 'user-status',
localField: 'assignee',
foreignField : '_id',
as : 'assignee'
}
},
{
$unwind: '$assignee'
}
]);
First you need to use match to filter by "assignee": "123", then you need to lookup order-status, match "orderStatus.status": "ready".
const resultObject = orders.aggregate([
{
$match: {
assignee: "123"
}
},
{
$lookup: {
from: "order-status",
localField: "_id",
foreignField: "orderId",
as: "statuses"
}
},
{
$match: {
"statuses.status": "ready"
}
},
{
$project: {
id: "_id",
name: "$name",
assignee: "$assignee",
status: {
$arrayElemAt: ["$statuses", 0]
}
}
}
]);
This will give result like this:
[
{
"_id": "1",
"assignee": "123",
"name": "Fridge",
"status": {
"_id": "1",
"orderId": "1",
"status": "ready"
}
}
]
Playground
I would use the following pipeline:
const resultObject = orders.aggregate([
{
$match: {
assignee: Objectid('123')
}
},
{
$lookup:
{
from: "order-status",
let: {order_id: "$_id"},
pipeline: [
{
$match:
{
$expr:
{
$and:
[
{$eq: ["$orderId", "$$order_id"]},
{$eq: ["$status", "ready"]}
]
}
}
}
],
as: "stock"
}
},
{
$unwind: "$stock"
},
// now we get the assignee info.
{
$lookup: {
from: 'user-status',
localField: 'assignee',
foreignField: '_id',
as: 'assignee'
}
},
{
$unwind: '$assignee'
},
//finaly create the required structure.
{
$project: {
name: "$assignee.name",
assignee: "$assignee._id",
status: "$stock.0"
}
}
]);

Joining two collections in mongodb

I have separate collections for 'comments','products' and 'users'. The 'comments' collection contains text, product_id and user_id. When a product is fetched I want the details of product along with details of the user in the result.
I have created schema using mongoose odm. I am using aggregate function to populate the product with comments using $lookup.
Product.aggregate([
{
$match:{
_id:mongoose.Types.ObjectId(id)
}
},
{
$lookup: {
from: "comments",
localField: "_id",
foreignField: "product",
as: "comments"
}
},
{
$match:{
"comments.product":mongoose.Types.ObjectId(id)
}
},
{
$lookup: {
from: "users",
localField: "comments.user._id",
foreignField: "user",
as: "comments.user"
}
}
])
expected result is
[
{
"_id": "5cc9441feed4c258881c99cd",
"title": "Batman",
"imageUrl": "images\\1556694047310_Batman.jpg",
"price": 555,
"description": "The dark knight",
"user": "5cbca36d4acc5d538c209014",
"__v": 2,
"comments": [
{
"_id": "5cc947125c69600d58c1be05",
"date": "2019-05-01T07:12:42.229Z",
"text": "This product is very nice",
"user":{
"_id": "5cbca36d4acc5d538c209014",
"name": "Clark Kent"
}
},
{
"_id": "5cc96eb4b2834d43f8a24470",
"date": "2019-05-01T09:46:34.774Z",
"text": "Anyone can be Batman",
"user":{
"_id": "5cbca5504acc5d538c209015",
"name": "Bruce Wayne"
},
}
}
]
actual result is
[
{
"_id": "5cc9441feed4c258881c99cd",
"title": "Batman",
"imageUrl": "images\\1556694047310_Batman.jpg",
"price": 555,
"description": "The dark knight",
"user": "5cbca36d4acc5d538c209014",
"__v": 2,
"comments": {
"user": [
{
"_id": "5cbca5504acc5d538c209015",
"name": "Bruce Wayne",
"email": "batman#gotham.com",
"password": "$2a$12$L.t/nBXq/xlic25Y0a884uGxjlimuNH/tcmWLg.sNkcjJ/C40Q14m",
"contactNumber": 9999999999,
"address": "Somewhere in Gotham",
"__v": 0
},
{
"_id": "5cbca7334acc5d538c209016",
"name": "Superman",
"email": "superman#metro.com",
"password": "$2a$12$mrogzC1Am86b0DnvTzosm.qfu38Ue7RqSNcnVSoCR55PtmLddeZv.",
"contactNumber": 9999999999,
"address": "Somewhere in metropolis",
"__v": 0
},
{
"_id": "5cbca7e54acc5d538c209017",
"name": "Wonder Woman",
"email": "ww#amazon.com",
"password": "$2a$12$Vt9XZUyOTULvel5zNAsMLeoMi3HlaGJJZN7OH2XkWuoAiZtDIGaMq",
"contactNumber": 9999999999,
"address": "Somewhere in Amazon",
"__v": 0
},
{
"_id": "5cbe192934ae2944c8704a5a",
"name": "Barry Allen",
"email": "barry#flash.com",
"password": "$2a$12$k73Wp1HTMv/MhUV3BOok3OSh.nnLq3vWG1Qz9ZTO7iB7saFlxhLjW",
"contactNumber": 9999999999,
"address": "Somewhere in Central City",
"__v": 0
}
]
}
}
]
Your $lookup query of users is overwriting the comments array. Its not working as you think it'll.
You need to unwind the comments array and then run that $lookup of users and then group by the products.
Edit: I have updated the query with $group by code too. Also you can playa around with the query here:
https://mongoplayground.net/p/2EA-Glz8Hrm
Product.aggregate([
{
$match: {
_id: "5cc9441feed4c258881c99cd"
}
},
{
$lookup: {
from: "comments",
localField: "_id",
foreignField: "product",
as: "comments"
}
},
{
$unwind: "$comments"
},
{
$lookup: {
from: "users",
localField: "comments.user",
foreignField: "_id",
as: "comments.user"
}
},
{
$unwind: "$comments.user"
},
{
$group: {
_id: "$_id",
// add other fields you want to include
comments: {
$addToSet: "$comments"
}
}
},
])
As suggested by Hamza, I made following changes to my query
Product.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId(id)
}
},
{
$lookup: {
from: "comments",
localField: "_id", //field from input document
foreignField: "product", // field from documents of the 'from' collection
as: "comments"
}
},
{
$unwind: "$comments"
},
{
$lookup: {
from: "users",
localField: "comments.user", //field from input document
foreignField: "_id", // field from documents of the 'from' collection
as: "comments.user"
}
},
{
$unwind: "$comments.user"
},
{
$group: {
_id: "$_id",
title: { $first: "$title" }, // $first returns the first expression of the document it encounters, ex. first title
price: { $first: "$price" },
imageUrl: { $first: "$imageUrl" },
description: { $first: "$description" },
rating: { $first: "$rating" },
comments: {
$addToSet: "$comments" // group comments and create an array
}
}
},
{
$project: {
_id: 1,
title: 1,
price: 1,
imageUrl: 1,
description: 1,
rating: 1,
comments: {
_id: 1,
text: 1,
date: 1,
user: {
_id: 1,
name: 1
}
}
}
}
])
With this I got the desired result.

Get only one document of each user - mongoDB

I m stuck with a mongo aggregate query. Right now I have a collection which contains posts of various users (whose details are present in users collection).
I need a query to fetch only one post of each user (like group by in SQL)
POSTS collection data
{
language:'english',
status:'A',
desc:'Hi there',
userId:'5b891370f43fe3302bbd8918'
},{
language:'english',
status:'A',
desc:'Hi there - 2'
userId:'5b891370f43fe3302bbd8918'
},{
language:'english',
status:'A',
desc:'Hi there - 3'
userId:'5b891370f43fe3302bbd8001'
}
Here is my query
db.col('posts').aggregate([
{
$match: {
language: 'english',
status: "A"
}
}, {
$sample: { size: 10 }
}, {
$sort: { _id: -1 }
}, {
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'ownerData'
}
}], (err, data) => { console.log(err,data) });
Desired Output
{
language:'english',
status:'A',
desc:'Hi there',
userId:'5b891370f43fe3302bbd8918',
ownerData:[[object]]
},{
language:'english',
status:'A',
desc:'Hi there - 3'
userId:'5b891370f43fe3302bbd8001',
ownerData:[[object]]
}
$group: will as group by of mysql. $first: will take first element of collection field from group. $lookup acts as join in mysql.
db.tempdate.aggregate([
{ $group :
{
_id : "$userId",
language : { $first: '$language' },
status : { $first: '$status' },
desc : { $first: '$desc' }
}
},
{ $lookup:
{
from: "user",
localField: "_id",
foreignField: "user_id",
as: "userData"
}
}
]).pretty();`
Output
`{
"_id" : "5b891370f43fe3302bbd8001",
"language" : "english",
"status" : "A",
"desc" : "Hi there - 3",
"userData" : [
{
"_id" : ObjectId("5ba3633a12b8613823f3056e"),
"user_id" : "5b891370f43fe3302bbd8001",
"name" : "Bhuwan"
}
]
}
{
"_id" : "5b891370f43fe3302bbd8918",
"language" : "english",
"status" : "A",
"desc" : "Hi there",
"userData" : [
{
"_id" : ObjectId("5ba3634612b8613823f3056f"),
"user_id" : "5b891370f43fe3302bbd8918",
"name" : "Harry"
}
]
}
Also, you can use group and $last
db.getCollection('posts').aggregate([
{ "$match": { "language": 'english', "status": "A" }},
{ "$group": {
"_id": "$userId",
"primaryId" : { "$last": "$_id" },
"language": { "$last": "$language" },
"status": { "$last": "$status" },
"desc": { "$last": "$desc" }
}},
{ "$lookup": {
"from": "users",
"localField": "_id",
"foreignField": "_id",
"as": "ownerData"
}},
{ $unwind:{path: '$ownerData',preserveNullAndEmptyArrays: true} //to convert ownerData to json object
}
])
You can use $group aggregation stage for the distinct userId and then use $lookup to get users data.
db.col('posts').aggregate([
{ "$match": { "language": 'english', "status": "A" }},
{ "$sample": { "size": 10 }},
{ "$sort": { "_id": -1 }},
{ "$group": {
"_id": "$userId",
"language": { "$first": "$language" },
"status": { "$first": "$status" },
"desc": { "$first": "$desc" }
}},
{ "$lookup": {
"from": "users",
"localField": "_id",
"foreignField": "_id",
"as": "ownerData"
}}
])

Resources