Best way to retrieve a reference field using $lookup in Mongoose? - mongodb

I have two models Book and Author, Book has reference of Author, suppose an Author got deleted then I only want to retrieve those Books who have an author:
BookSchema with these fields
name: String,
author: {
type: Schema.Types.ObjectId,
ref: 'Author',
required: [true, 'A book must have an author']
}
AuthorSchema with these fields
name: String
I have to do it using $lookup operator. I am able to get the desired result but I don't know if it's a good way. This is my solution:
const books = await Book.aggregate([
{ $lookup: {
from: 'authors',
localField: 'author',
foreignField: '_id',
as: 'bookAuthor'
}
},
{ $match: { bookAuthor: { $not: { $size: 0 } } } },
{ $unwind: '$bookAuthor' },
{ $project: {
name: 1,
bookAuthor: { name: 1 }
}
}
]);

You have done everything correct, but you don't need to use the $match in second phase of aggregation pipeline. $unwind will automatically remove the documents with empty bookAuthor array, so if there is no author, it will be removed after $unwind stage.
Try this:
const books = await Book.aggregate([
{ $lookup: {
from: 'authors',
localField: 'author',
foreignField: '_id',
as: 'bookAuthor'
}
},
{ $unwind: '$bookAuthor' },
{ $project: {
name: 1,
bookAuthor: { name: 1 }
}
}
]);
Have a look at this Mongo Playground to see it working

Related

Applying $exists to MongoDB aggregation

I have two mongo collections structured like so:
customers
{
_id: ObjectId,
name: String,
companyId: ObjectId
}
companies
{
_id: ObjectId,
name: String,
rights: [
add: boolean,
edit: boolean,
shop: boolean
]
}
So each customer has a companyId that lets us look up the companies.rights available. I need to get a list of which companies have customers but don't have the shop property at all.
So far I have this:
db.getCollection('customers').aggregate([
{
$match: {}
},
{
$lookup: {
from: 'companies',
localField: 'companyId',
foreignField: '_id',
as: 'company'
}
},
{
$project: {
companyId: '$company._id',
companyName: '$company.name',
shopSetting: '$company.rights.shop'
}
}
])
This seems to be working ok to give me all of the companies with their shop value (true or false). But what if I only want to see the companies that don't have the shop field existing at all? How would I modify this query to accomplish that? I've tried reading up on the $exists field in mongo, but this is all pretty new to me so I'm not sure where to apply it here.
Note: I need to query the companies from the customers collection because there are some companies without customers and I need this result to only be companies that are assigned to customers but don't have the rights.shop property existing
db.customers.aggregate([
{ $match: {} },
{
$lookup: {
from: "companies",
localField: "companyId",
foreignField: "_id",
as: "company",
pipeline: [
{
$match: {
$expr: {
$eq: [
{
$filter: {
input: "$rights",
as: "r",
cond: "$$r.shop"
}
},
[]
]
}
}
}
]
}
},
{
$project: {
companyId: "$company._id",
companyName: "$company.name",
shopSetting: "$company.rights.shop"
}
}
])
mongoplayground

MongoDB Aggregate - How to check if a specific field value exists in array of documents

I have this LikeSchema for Posts and what i want to achieve is check whether the user id exists from these array of Likes Document's _user_id
Supposed I have this array of Likes document
[
{
id: 'a',
_user_id: 'u1',
_post_id: 'p1'
},
{
id: 'b',
_user_id: 'u2',
_post_id: 'p1'
}
]
How do I check if user id u1 exists in likes array documents using aggregation?
I have this Like Schema
const LikeSchema = new Schema({
_post_id: {
type: Schema.Types.ObjectId,
ref: 'Post',
required: true
},
_user_id: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
})
And my aggregation
const result = await Post.aggregate([
{
$match: { privacy: 'public' }
},
{
$lookup: {
from: 'likes',
localField: '_id',
foreignField: '_post_id',
as: 'likes'
}
},
{
$addFields: {
isLiked: {
$in: [req.user._id, '$likes'] // WONT WORK SINCE I STILL HAVE TO MAP AS ARRAY OF _user_id
}
}
}
])
The solution I have in mind is to map first the array to only have user id values then perform the $in expression.
How do I map the likes array from lookup in aggregation stages so that it will only contain arrays of user id values to make the $in expression match? Or maybe there is a better way to check for value that exists in array of objects?
Finally found the answer.
Turns out, there exists a $map operator in aggregation. This is how I used it
const result = await Post.aggregate([
{
$match: { privacy: 'public' }
},
{
$lookup: {
from: 'likes',
localField: '_id',
foreignField: '_post_id',
as: 'likes'
}
},
{
$addFields: {
likeIDs: { // map out array of like id's
$map: {
input: "$likes",
as: "postLike",
in: '$$postLike._id'
}
}
}
},
{
$addFields: {
isLiked: {
$in: [req.user._id, '$likeIDs'] // it works now
}
}
}
])
It seems you can accomplish this in a cleaner way just using one addFields.
const result = await Post.aggregate([
{
$match: { privacy: 'public' }
},
{
$lookup: {
from: 'likes',
localField: '_id',
foreignField: '_post_id',
as: 'likes'
}
},
{
$addFields: {
isLiked: {
$in: [req.user._id, "$likes._id"]
}
}
}
])

Mongodb lookup with array

I have two collections first one is
user_profile collection
const userProfileSchema = mongoose.Schema({
phone_number: {
type: String,
required: false,
},
primary_skills: [
{
skill_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Skill'
},
years: Number,
}
]
});
sample data
{
"phone_number":"222",
"primary_skills":[{skill_id:1,years:12},{skill_id:2,years:13}]
}
in the primary_skills the key skill_id is mapped with another collection named skills
skills collection
const skillSchema = mongoose.Schema({
name: {
type: String,
required: true,
unique:true,
},
});
sample data
[
{
id:1,
name:'php'
},
{
id:2,
name:'java'
}
]
I want to fetch all values in the user_profile collection along with the respective skills name
expected output:
{
"phone_number":"222",
"primary_skills":[{
name:"php",skill_id:1,years:12
},{
name:"java",skill_id:2,years:13}
]
}
I found a similar thread to my question MongoDB lookup when foreign field is an array of objects but it's doing the opposite of what I want
This is the query I tried
profile.aggregate([{
$lookup:{
from:'skills',
localField:'primary_skills.skill_id',
foreignField:'_id',
'as':'primary_skills'
}
}])
This works fine but it didn't contain the years key
You need to do it with $unwind and $group,
$unwind primary_skills because its an array and we need to lookup sub document wise
db.user_profile.aggregate([
{
$unwind: "$primary_skills"
},
$lookup to join primary_skills, that you have already did
{
$lookup: {
from: "skills",
localField: "primary_skills.skill_id",
foreignField: "id",
as: "primary_skills.name"
}
},
$unwind primary_skills.name that we have stored join result, its array and we are unwinding to do object
{
$unwind: {
path: "$primary_skills.name"
}
},
$addFields replace field name that we have object and we need only name
{
$addFields: {
"primary_skills.name": "$primary_skills.name.name"
}
},
$group by _id because we have unwind and we need to combine all documents
{
$group: {
_id: "$_id",
phone_number: {
$first: "$phone_number"
},
primary_skills: {
$push: "$primary_skills"
}
}
}
])
Playground: https://mongoplayground.net/p/bDmrOwmASn5

$lookup using multiple criteria mongodb java aggregation

Have 2 following collections:
user collection
{
userId:user1,
creationTimeStamp:2019-11-05T08:15:30
status:active
},
{
userId:user2,
creationTimeStamp:2019-10-05T08:15:30
status:active
}
document collection
{
userId:user1,
category:Development
published:true
},
{
userId:user2,
category:Development
published:false
}
I want to join these two collections and filter users such that documents which are of development category and are not published from active users between creationtimestamp
How can I write a mongodb java aggregation in order to get a result like this:
{
userId: user2,
status:active,
category:Development,
published:false
}
You could run below aggregation query on the document collection to get the expected result
[{$match: {
category:'development',
published: false
}}, {$lookup: {
from: 'user',
localField: 'userId',
foreignField: 'userId',
as: 'JoinedTable'
}}, {$unwind: {
path: '$JoinedTable'
}}, {$group: {
_id: '$_id',
userId: {
$first: '$userId'
},
status: {
$first: '$JoinedTable.status'
},
category: {
$first: '$category'
},
published: {
$first: '$published'
},
}}]
Explanation:
1. filter documents using match for criteria category: 'development' & published: false
2. join document collection with user collection with key userId
3. unwind the joined collection field to convert array to object
4. project the fields needed using groups.
Hope this helps!
You haven't mentioned about the duplicate of userId in User collection.
So the script is
[{
$match: {
category: "Development",
published: false
}
}, {
$lookup: {
from: 'user',
localField: 'userId',
foreignField: 'userId',
as: 'joinUser'
}
}, {
$unwind: {
path: "$joinUser",
preserveNullAndEmptyArrays: true
}
}, {
$match: {
"joinUser.status": "active"
}
}, {
$addFields: {
"status": "$joinUser.status"
}
}, {
$project: {
_id: 0,
userId: 1,
category: 1,
published: 1,
status: 1
}
}]
And the java code,
include these imports
import static org.springframework.data.mongodb.core.aggregation.Aggregation.match;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.lookup;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.unwind;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.project;
method is,
public Object findAllwithVideos() {
Aggregation aggregation=Aggregation.newAggregation(
match(Criteria.where("category").is("Development").and("published").is(false)),
lookup("user","userId","userId","joinUser"),
unwind("joinUser",true),
new AggregationOperation(){
#Override
public Document toDocument(AggregationOperationContext aggregationOperationContext){
return new Document("$addFields",
new Document("status","$joinUser.status")
);
}
},
project("userId","category","published","status")
).withOptions(AggregationOptions.builder().allowDiskUse(Boolean.TRUE).build());
return mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Document.class), Object.class);
}

How can I perform a $lookup on a property value in a nested array?

I have an articles collection that contains an array of comments, and this array contains an array of sub_comments. Here is what it looks like:
let articleSchema = mongoose.Schema({
title: {type: String},
comments: [{
comment: {type: String},
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
sub_comments: [{
comment: {type: String},
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
}]
}]
});
I'm trying to run an aggregate query that does a $lookup on the comments.creator and sub_comments.creator fields. Here's what I've tried so far, but it doesn't work:
this.model('Article')
.aggregate([
{
$match: {_id: article_id}
},
{
$lookup: {
from: "users",
localField: "comments.creator",
foreignField: "_id",
as: "comments.creator"
}
},
{
$unwind: {
path: "$comments.creator",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "users",
localField: "comments.sub_comments.creator",
foreignField: "_id",
as: "comments.sub_comments.creator"
}
},
{
$unwind: {
path: "$comments.sub_comments.creator",
preserveNullAndEmptyArrays: true
}
},
{
$project: {
_id: 1,
title: 1,
"comments.comment": 1,
"comments.creator._id": 1,
"comments.creator.name": 1,
"comments.sub_comments.comment": 1,
"comments.sub_comments.creator._id": 1,
"comments.sub_comments.creator.name": 1
}
}
])
.exec(function (err, data) {
...
});
Here's an example of the data response:
[{
"_id": "5b7e1629d00a170018e11234",
"article": "My Article",
"comments": {
"comment": "This is the comment.",
"sub_comments": {},
"creator": {
"_id": "541g2dfeab1e470b00411234",
"name": "John Doe"
}
},
....
}]
Comments should be an array, not an object. This particular comment didn't have any sub-comments, but obviously, I want that to work too. Any ideas on how to get this to work?
the as params in mongodb $lookup with your query affect your result .
as Specifies the name of the new array field to add to the input
documents. The new array field contains the matching documents from
the from collection. If the specified name already exists in the input
document, the existing field is overwritten.
it will overwrite the filed . not add to it .
if your outter is an array ,it will make it to Object and delete the current content .
so the second $lookup in aggregate will not work . in your query
if you want to keep the current data structure intac . you can as a new fieldname and use $project to change the format .