I'm trying to populate a collection with lookup. The thing is I need to join three collections at once using mongoose in express js.
I have three collections namely, users, skills, userskills.
User and UserSkills are connected. Skills and UserSkills are connected. But not User and Skills.
My model is like
users
{
_id: 5ec6d940b98e8f2c3cea5f22,
name: "test one",
email: "test#example.com"
}
skills
{
_id: 5ec786b21cea7d8c8c186a54,
title: "java"
}
user-skills
{
_id: 5ec7879c1cea7d8c8c186a56,
skillId: "5ec786b21cea7d8c8c186a54",
userId: "5ec6d940b98e8f2c3cea5f22",
level: "good"
}
I tried
user = await User.aggregate([{
$lookup: {
from: "userskills",
localField: "_id",
foreignField: "userId",
as: "userskills"
}
}, {
$unwind: {
path: "$userskills",
preserveNullAndEmptyArrays: true
}
}, {
$lookup: {
from: "skills",
localField: "userskills.skillId",
foreignField: "_id",
as: "userskills.skill",
}
}, {
$group: {
_id : "$_id",
name: { $first: "$name" },
skill: { $push: "$skills" }
}
}, {
$project: {
_id: 1,
name: 1,
skills: {
$filter: { input: "$skills", as: "a", cond: { $ifNull: ["$$a._id", false] } }
}
}
}]);
Required result:
{
"users" : [
{
_id: "5ec6d940b98e8f2c3cea5f22"
name: "test one",
email: "testone#example.com",
"skills" : [
{
_id: "5ec7879c1cea7d8c8c186a56",
level: "good",
"skill" : {
_id: "5ec786b21cea7d8c8c186a54",
title: "java"
}
}
]
},
{
_id: "5ec6d940b98e8f2c3cea5f23"
name: "test two",
email: "testtwo#example.com",
"skills" : [
{
_id: "5ec7879c1cea7d8c8c186a57",
level: "good",
"skill" : {
_id: "5ec786b21cea7d8c8c186a55",
title: "php"
}
}
]
}
]
}
when using
user = await User.find().populate('Skills").exec()
The result is coming like
{
"users" : [
{
_id: "5ec6d940b98e8f2c3cea5f22"
name: "test one",
email: "testone#example.com",
"skills" : [
{
_id: "5ec7879c1cea7d8c8c186a56",
level: "good",
skillId: "5ec786b21cea7d8c8c186a55"
}
]
}
]
}
The problem is I need the skill name should also be fetched. Please help me to solve this issue. I'm writing a backend API in nodejs and mongodb.
You can embed that second $lookup as part of custom pipeline:
await User.aggregate([
{
$lookup: {
from: "userskills",
let: { user_id: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: [ "$$user_id", "$userId" ] } } },
{
$lookup: {
from: "skills",
localField: "skillId",
foreignField: "_id",
as: "skill"
}
},
{ $unwind: "$skill" }
],
as: "skills"
}
}
])
Mongo Playground
Related
I have these fields in the document,
doc: {
"id": "632ac8cba7723378033fef10",
"question": 1,
"text": "aasdfghjk,mnbvcxswertyuikmnbvcxsrtyuiknbvcdrtyujnbvcddtyjnbvfty",
"slug": "xcvbnrddfghjktdxjjydcvbyrsxcvbhytrsxggvbjkytrdgc",
"subject": 25866,
"tutorInfo": {
"tutorId": "632ac8cba7723378033fa0fe",
"tutorIncrementalId": 95947
}
}
the same tutorInfo can Occur in multiple documents.
const allQuestionBySubject = await QuestionParts.aggregate([
{
$match: {
$and: [
{
subject: subjectIncrementalId
},
{tutorInfo: {$exists: true}}
]
}
},
{ "$skip": page * limit },
{ "$limit": limit },
{
$lookup: {
from: "profiles",
localField: "tutorInfo.tutorIncrementalId",
foreignField: "incrementalId",
as: "tutorDetails"
}
}
])
Code to get a list of questions as per subject.
I am filtering documents based on subject and as I mentioned the same tutorInfo can be present in multiple documents so in the result same tutor can be present in multiple documents, How can I get a unique list of documents in which tutorInfo shouldn't be repeated.
Since the same tutorInfo is present in multiple records, You can use $group to group the document on the tutorInfo.tutorId field.
const allQuestionBySubject = await QuestionParts.aggregate(
[
{
$match: {
$and: [
{
subject: subjectIncrementalId
},
{ tutorInfo: { $exists: true } }
]
}
},
{ "$skip": page * limit },
{ "$limit": limit },
{
"$group": {
_id: "$tutorInfo.tutorId",
question: { $first: "$question" },
text: { $first: "$text" },
slug: { $first: "$slug" },
subject: { $first: "$orderId" },
tutorInfo: { $first: "$tutorInfo" },
}
},
{
$lookup: {
from: "profiles",
localField: "tutorInfo.tutorIncrementalId",
foreignField: "incrementalId",
as: "tutorDetails"
}
}
]
)
My Aggregate query:
const categoryId = req.query.categoryId
const results = await Question.aggregate([
{
$match:{
$and : [
{ category : mongoose.Types.ObjectId(categoryId) },
{ category : {$ne : null} }
]
}
},{
$lookup: {
from: "answer",
let: { questionId: "$_id" },
pipeline: [{ $match: { $expr: { $eq: ["$$questionId", "$questionId"] } } }],
as: "answerCount"
}
},{ $addFields: { answerCount: { $size: "$answerCount" }}}, {
$lookup: {
from: "users",
let : {id : "$creator"},
as : "creator",
pipeline : [
{$match : {$expr : {$eq: ["$$id","$_id"]}}},
{$project : {name : 1, profilePhoto : 1}}
]
}
}, {$unwind: "$creator"},{
$lookup: {
from: "categories",
let : { id: "$category" },
as : "category",
pipeline: [
{ $match : { $expr: { $eq: ["$_id", "$$id"] } }},
{ $project: { name: 1}}
]
}
}, {$unwind : "$category"},{
$unset: ["createdAt", "updatedAt", "__v"]
}
])
Now using $match query I fetch only the Questions belonging to a specific category. What I want to do is if the categoryId is null, it should return all the results. Right now it returns an empty array. How do i go about doing that?
Try This:
const categoryId = req.query.categoryId
let conditions = {
// You can also have some default condition that always results true
};
if (categoryId) {
conditions = {
"category": mongoose.Types.ObjectId(categoryId)
// More conditions in future...
}
}
const results = await Question.aggregate([
{
$match: conditions
},
{
$lookup: {
from: "answer",
let: { questionId: "$_id" },
pipeline: [{ $match: { $expr: { $eq: ["$$questionId", "$questionId"] } } }],
as: "answerCount"
}
},
{ $addFields: { answerCount: { $size: "$answerCount" } } },
{
$lookup: {
from: "users",
let: { id: "$creator" },
as: "creator",
pipeline: [
{ $match: { $expr: { $eq: ["$$id", "$_id"] } } },
{ $project: { name: 1, profilePhoto: 1 } }
]
}
},
{ $unwind: "$creator" },
{
$lookup: {
from: "categories",
let: { id: "$category" },
as: "category",
pipeline: [
{ $match: { $expr: { $eq: ["$_id", "$$id"] } } },
{ $project: { name: 1 } }
]
}
},
{ $unwind: "$category" },
{
$unset: ["createdAt", "updatedAt", "__v"]
}
]);
also read about preserveNullAndEmptyArrays property in $unwind operator if required.
https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/
I have a simple 3 collections. This bellow is their pseudocode. I want to get all shipments and for each shipment, I want to have all bids for that shipment and in each bid, I need userDetails object.
User: {
name: string,
}
Shipment: {
from: string,
to: string
}
Bid: {
amount: number,
shipmentId: Ref_to_Shipment
userId: Ref_to_User
}
This is what I have tried:
const shipments = await ShipmentModel.aggregate([
{
$lookup: {
from: "bids",
localField: "_id",
foreignField: "shipmentId",
as: "bids"
}
},
{
$lookup: {
from: "users",
localField: "bids.userId",
foreignField: "_id",
as: "bids.user"
}
}
])
And I got the following result:
[
{
"_id": "5fad2fc04458ac156531d1b1",
"from": "Belgrade",
"to": "London",
"__v": 0,
"bids": {
"user": [
{
"_id": "5fad2cdb4d19c80d1b6abcb7",
"name": "Amel",
"email": "Muminovic",
"password": "d2d2d2",
"__v": 0
}
]
}
}
]
I am trying to get all Shipments with their bids and users within bids. Data should look like:
[
{
"_id": "5fad2fc04458ac156531d1b1",
"from": "Belgrade",
"to": "London",
"__v": 0,
"bids": [
{
"_id": "5fad341887c2ae1feff73402",
"amount": 400,
"userId": "5fad2cdb4d19c80d1b6abcb7",
"shipmentId": "5fad2fc04458ac156531d1b1",
"user": {
"name": "Amel",
}
"__v": 0
}
]
}
]
Try with the following code:
const shipments = await ShipmentModel.aggregate([
{
$lookup: {
from: "bids",
localField: "_id",
foreignField: "shipmentId",
as: "bids"
}
},
{
$unwind: {
path: "$bids",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "users",
localField: "bids.userId",
foreignField: "_id",
as: "bids.user"
}
}
])
If you want to prevent null and empty arrays then set
preserveNullAndEmptyArrays: false
Try this query and chek if works and the behaviour is as you expected:
db.Shipment.aggregate([
{
$lookup: {
from: "Bid",
localField: "id",
foreignField: "shipmentId",
as: "bids"
}
},
{
$lookup: {
from: "user",
localField: "id",
foreignField: "id",
as: "newBids"
}
},
{
$project: {
"newBids.id": 0,
"newBids._id": 0,
}
},
{
$match: {
"bids.userId": 1
}
},
{
$addFields: {
"newBids": {
"$arrayElemAt": [
"$newBids",
0
]
}
}
},
{
$set: {
"bids.user": "$newBids"
}
},
{
$project: {
"newBids": 0
}
}
])
This query do your double $lookup and then a $project to delete the fields you don't want, and look for the userId to add the field user. As $lookup generate an array, is necessary use arrayElemAt to get the first position.
Then $set this value generated into the object as bids.user and remove the old value.
Note that I have used a new field id instead of _id to read easier the data.
Try this
I figured out it based on MongoDB $lookup on array of objects with reference objectId and in the answer from J.F. (data organization). Note that he used id instead of _id
The code is
db.Shipment.aggregate([
{
$lookup: {
from: "Bid",
localField: "id",
foreignField: "shipmentId",
as: "bids"
}
},
{
$lookup: {
from: "user",
localField: "bids.userId",
foreignField: "id",
as: "allUsers"
}
},
{
$set: {
"bids": {
$map: {
input: "$bids",
in: {
$mergeObjects: [
"$$this",
{
user: {
$arrayElemAt: [
"$allUsers",
{
$indexOfArray: [
"$allUsers.id",
"$$this.userId"
]
}
]
}
}
]
}
}
}
}
},
{
$unset: [
"allUsers"
]
},
// to get just one
//{
// $match: {
// "id": 1
// }
// },
])
I'm doing a simple follow friend functionality.
Please see my codes below:
Following schema:
{
"userId": { type: String },
"followers": [{ "followerId": type: String }],
"followings": [{ "followingId": type: String }],
}
User schema:
{
"fullName": { type: String }
}
Note: user 8 has 1 follower and 2 followings.
Now, my expected output should be like this:
"userId": 8,
"followers": [
{
"followerId": 4,
"fullName": "Rose Marriott",
},
{
"followerId": 5,
"fullName": "James Naismith",
}
],
"followings": [
{
"followingId": 1,
"fullName": "Russell Oakham",
},
{
"followingId": 5,
"fullName": "James Naismith",
}
]
This is what I tried so far:
db.followings.aggregate([
{ $unwind: "$followers" },
{
$lookup: {
from: "users",
localField: "followers.followerId",
foreignField: "_id",
as: "users"
}
},
{
$addFields:
{
users: { $arrayElemAt: ["$users", 0] },
},
},
{ $unwind: "$followings" },
{
$lookup: {
from: "users",
localField: "followings.followingId",
foreignField: "_id",
as: "users2"
}
},
{
$addFields:
{
users2: { $arrayElemAt: ["$users2", 0] },
},
},
{ $match: {"userId": mongoose.Types.ObjectId(userId) } },
{
$group: {
_id: "$_id",
userId: { $first: "$userId" },
followers: {
$push: {
followerId: "$followers.followerId",
fullName: "$users.fullName",
}
},
followings: {
$push: {
followingId: "$followings.followingId",
fullName: "$users2.fullName",
}
}
}
}
]);
But I'm getting 2 followers and 2 followings. I wonder what's causing this issue. Appreciate any help. Thanks!
You can try,
$addFields to make a unique array called userIds form both arrays followers and followings, $setUnion to get unique ids,
$lookup with users collection
$project to show fields,
followers get fullName, $map to iterate loop of followers and get the name of followerId from users array using $reduce and $cond
followings get fullName, $map to iterate loop of followings and get the name of followingId from users array using $reduce and $cond
db.followings.aggregate([
{
$addFields: {
userIds: {
$setUnion: [
{
$map: {
input: "$followers",
in: "$$this.followerId"
}
},
{
$map: {
input: "$followings",
in: "$$this.followingId"
}
}
]
}
}
},
{
$lookup: {
from: "users",
localField: "userIds",
foreignField: "_id",
as: "users"
}
},
{
$project: {
userId: 1,
followers: {
$map: {
input: "$followers",
as: "f",
in: {
$mergeObjects: [
"$$f",
{
fullName: {
$reduce: {
input: "$users",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this._id", "$$f.followerId"] },
"$$this.fullName",
"$$value"
]
}
}
}
}
]
}
}
},
followings: {
$map: {
input: "$followings",
as: "f",
in: {
$mergeObjects: [
"$$f",
{
fullName: {
$reduce: {
input: "$users",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this._id", "$$f.followingId"] },
"$$this.fullName",
"$$value"
]
}
}
}
}
]
}
}
}
}
}
])
Playground
when working with relations on Mongoose you should create the relationship based on a unique id, and then reference the document. In your case, it would be:
followingSchema = new mongoose.Schema({
{
"followers": [{type: mongoose.Schema.types.ObjectId, ref="User"}],
"followings": [{type: mongoose.Schema.types.ObjectId, ref="User"}],
}
})
userSchema = new mongoose.Schema({
fullname: String,
})
be aware that the userId will be created automatically by Mongoose with a field called _id. so, the end result of creating a new following documents would be:
{
_id: "klajdsfñalkdjf" //random characters created by mongoose,
followers: ["adfadf134234", "adfadte345"] //same as before, these are Ids of the users randomly generated by mongoose
followers: ["adfadf134234", "adfadte345"]
}
{
_id: adfadf134234,
fullName: "alex",
}
now, because there is no use for us to have a random number as information in the fields of following and followers in the following object, we can now use the method .populate() that can be used over the document itself to transform those Ids into actual information. You can see more about it here: mongoose documentation
our final result would be something like this:
{
_id: "añfakdlsfja",
followers : [{_id: "adlfadsfj", fullName: "alex"}],
following : [{_id: "adfadfad" , fullName: "other"}, {_id: "dagadga", fullName: "another"}]
}
This is my user document
{
"_id":"02a33b9a-284c-4869-885e-d46981fdd679",
"context":{
"email":"someemail#gmail.com",
"firstName":"John",
"lastName":"Smith",
"company":[
"e2467c93-114b-4613-a842-f311a8c537b3"
],
},
}
and a company document
{
"_id":"e2467c93-114b-4613-a842-f311a8c537b3",
"context":{
"name":"Coca Cola",
"image":"someimage",
},
};
This is my query for users
let users = await Persons.aggregate(
[{$project:
{
name: {$concat: ['$context.firstName', ' ', '$context.lastName']},
companyId: {$arrayElemAt: ['$context.company', 0]}}
},
{$match: {name: searchRegExp}},
{$lookup: {from: 'companies', let: {company_id: {$arrayElemAt: ['$context.company', 0]}}, pipeline:
[
{
$match: {
$expr: {
$eq: ['$_id', '$$company_id']
}
}
},
{
$project: {name: '$context.name'}
}
],
as: 'company'}}
]).toArray()
When I run this query I get company field as an empty array, what am I doing wrong here?
Your first pipeline stage $project only outputs _id, name and companyId so then when you're trying to refer to $context.company in your $lookup there will be an empty value. You can use $addFields instead:
{
$addFields: {
name: {
$concat: [
"$context.firstName",
" ",
"$context.lastName"
]
},
companyId: {
$arrayElemAt: [
"$context.company",
0
]
}
}
}
Mongo Playground
When you add field companyId: {$arrayElemAt: ['$context.company', 0]}}, then you can use the simple version of $lookup. There is no need to set it twice, once as companyId: ... and in let: {company_id: ...}
db.user.aggregate([
{
$addFields: {
name: { $concat: ["$context.firstName", " ", "$context.lastName"] },
companyId: { $arrayElemAt: ["$context.company", 0] }
}
},
{
$lookup: {
from: "company",
localField: "companyId",
foreignField: "_id",
as: "company"
}
}
])