Mongoose GroupBy with Populate - mongodb

i have User document and and Category document which is connected to shop document which look like
var ShopSchema = mongoose.Schema(
{
shopName: {
type: String,
required: [true, 'A Shop must have name'],
},
phone: {
type: String,
validate: [validator.isMobilePhone,"Phone number is not a valid number"],
required: [true, 'A Shop must have hepline number'],
},
shopStatus: {
type: Boolean,
default: false,
select: false
},
managerId:{
type: mongoose.Schema.ObjectId,
ref:'User',
require: [true,'Shop must have to a manager!']
},
createdAt:{
type:Date,
default: Date.now()
},
shopCategory:{
type: mongoose.Schema.ObjectId,
ref:'Category',
require: [true,'Shop must have a Category!']
},
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
const Shop = mongoose.model("Shop", ShopSchema);
Now i am writing a query to get managers shop(groupBy managerId) with populate of Manager and Category data. but i am getting empty array everytime.
const doc = await Shop.aggregate([
{
$match: { shopStatus: {$ne: false} }
},
{
$group: {
_id:'$managerId',
numofShop: {$sum: 1},
shopCategory: {$first: "$shopCategory"}
}
},
{
$lookup: {
from: "User",
localField : "_id",
foreignField: "_id",
as: "Manager"
}
},
{
$lookup: {
from: "Category",
localField : "shopCategory",
foreignField: "_id",
as: "Category"
}
},
])
here is how my final result look like but this is an empty array.
{
"_id": "5f467660f630e804ec07fad8",
"numofShop": 2,
"Manager": [],
"Category": []
},
{
"_id": "5f44d2f4ff04993b40684bf9",
"numofShop": 1,
"Manager": [],
"Category": []
}
I want to find All shops groupBy managerId(Who own it). there is field of managerId referencing to User document who created this shop. i want data like this
{
"_id": "5f44d2f4ff04993b40684bf9",
"Manager": {},//this is populated through User Schema
"numofShop": 1,
"Shop": [
{
"shopName":"Grocery Shop",
"phone" : "111111",
"shopCategory" :{"_id": "","name":"Electronics"}
}
]
}
.....
.....
Find Example Data here
Shops- http://txt.do/1fwi0
3 shops created by two users
User - http://txt.do/1fwio
Categories- http://txt.do/1fwij

There are 2 fixes,
in second lookup you have no shopCategory field in localField because you have not defined shopCategory in $group,
{
$group: {
_id:'$managerId',
numofShop: {$sum: 1},
shopCategory: { $first: "$shopCategory" } // define here
}
},
in first lookup with user collection, there is no managerId field and you have assigned in _id field, then you can use _id in localField,
{
$lookup: {
from: "User",
localField : "_id", // change this from managerId
foreignField: "_id",
as: "Manager"
}
},
Updated things after updated question, Playground1, Playground2
for your second edit, you can't put condition for populate in find method, how to put condition in populate, you can follow the answer1 and answer2

Related

how to filter documents from populate in mongoose

I have booking schema with ref state and city. I want to get only documents from only specific pair of city,state( but only names of city, state is given)
const bookingSchema = new Schema({
id: {
type: String
},
service: {
type: String
},
city: {
type: Schema.Types.ObjectId,
ref: 'City'
},
state: {
type: Schema.Types.ObjectId,
ref: 'State',
}
})
const stateSchema=new Schema({
name: {
type: String
}
})
const citySchema=new Schema({
name: {
type: String
}
})
I need output (documents) containing id and service from bookingSchema, but only those documents (can be more than one) with a certain city name (for example name="New York")
In SQL it can be done by query :
SELECT * FROM bookings bk
INNER JOIN city ct ON bk.city_id=ct.id
INNER JOIN state st ON bk.state_id=st.id
WHERE ct.name='city name' AND st.name='state name'
I also did use populate with match, but the problem is that it doesn't filter bookingSchema, but it actually work in way that it only populate that field who has state name that is given and for other documents it gives null.
PLEASE TELL ME HOW TO FILTER DOCUMENTS BASED ON REF
use aggregate on booking collection like this
lookup : lookup city from city collection
lookup : lookup state from state collection
unwind : destruct array of city
unwind : destruct array of state
project : create final result
example : https://mongoplayground.net/p/hGQFhkoujsC
db.booking.aggregate([
{
"$lookup": {
"from": "city",
"localField": "city",
"foreignField": "_id",
"as": "city"
}
},
{
"$lookup": {
"from": "state",
"localField": "state",
"foreignField": "_id",
"as": "state"
}
},
{
"$unwind": "$city"
},
{
"$unwind": "$state"
},
{
"$match": {
"city.name": "New York",
"state.name": "Test State"
}
},
{
"$project": {
_id: 1,
city: "$city.name",
service: 1,
state: "$state.name"
}
}
])

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 .

Mongodb 3.2 aggregation $lookup for nested document

I have two collections: memberships and categories.
In the categories I have stored array of objects called ageGroups.
memberships : [{
_id: ObjectId,
ageCategory: ObjectId,
ageGroup: ObjectId
}]
categories: {
_id: ObjectId,
ageGroups: [{
_id: ObjectId,
name: String
}, {
_id: ObjectId,
name: String
}, {
_id: ObjectId,
name: String
}]
}
There are ageCategory and ageGroup properties in memberships collection.
Now i need to aggregate these two and get result like this
[{
id: 'membership id here',
ageGroup: 'Age Group name here'
},{
id: '2 membership id here',
ageGroup: '2 membership age Group name here'
}]
So I tried
db.memberships.aggregate([
{
$lookup: {
from: 'categories', localField: 'ageCategory', foreignField: '_id', as: 'ageCategory'
}
},
{
$unwind: '$ageCategory'
},
{
$project: {
'group': {
$filter: {
input : '$ageCategory.ageGroups',
as : 'group',
cond : 'group._id' == '$_id'
}
}
}
},
{
$unwind: '$group'
},
{
$project: {
'group' : '$group.name'
}
}
]);
This query return empty result. As much as I understand, problem in filter condition
$filter: {
input : '$ageCategory.ageGroups',
as : 'group',
cond : 'group._id' == '$_id'
}
when I tried to set true in cond, it pushed new document to result, for each group, even if ageGroup not that, which is assigned to membership. Thanks for help!
UPD
I found solution,
{
$project: {
'group': {
$filter: {
input : '$ageCategory.ageGroups',
as : 'group',
cond : { $eq:['$$group._id', '$ageGroup']}
}
}
}
},
I have added double dollar sign with $eq operator and problem has been solved. Now I search better solution and I would like have a query which get similar result
{
"_id" : // member id,
"group" : "1-3 members"
}
{
"_id" : //member id,
"group" : "17-19 members"
}
without storing ageCategory in memberships table, only with ageGroup id.