Join documents in array mongodb - node.js

I have collections of characters here like this.
characters collection
{
_id: objectId(1), //this is just an example id
name: 'harry'
},
{
_id: objectId(2),
name: 'ron'
},
{
_id: objectId(3),
name: 'dumbledor'
},
{
_id: objectId(4),
name: 'Super Man'
}
then i have another one
movie collection
{
_id: objectId(1),
heroes_id: [
_id: objectId(1),
_id: objectId(2),
_id: objectId(3)
],
movie: 'Harry Potter'
}
I wanted it to join them like this
{
_id: objectId(1),
heroes_id: [
_id: objectId(1),
_id: objectId(2),
_id: objectId(3)
],
movie: 'Harry Potter',
cast: [
{
{
_id: objectId(1), //this is just an example id
name: 'harry'
},
{
_id: objectId(2),
name: 'ron'
},
{
_id: objectId(3),
name: 'dumbledor'
},
}
]
}
i have a piece of code with mongodb functions
movie.aggregate([
{
$unwind: "$heroes_id"
},
{
$lookup: {
from: "characters",
localField: "heroes_id",
foreignField: "_id",
as: "cast"
}
},
{
$match: { "cast": { $ne: [] } }
}
]).exec(function(err, results) {
if(err) res.status(500).send(err);
res.status(200).send(results);
});
so i just wanted to join the two collections. it work but it get only one. please help me out thanks.

Related

Mongoose Aggregate and connect foreign Keys

I have two tables.
The first one is the master collection,
The second collection is the stat collection
Im summing up the recodes in stat table with aggregate, when I'm doing that I want to use the foreign key and get the title from the master collection. Following is the code I have used.
const totalClicks = await StatModel.aggregate([
{ $match: { campaignId: id } },
{
$group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$createAt' } },
count: { $sum: 1 },
},
},
{
$lookup: {
from: 'campaign.channels',
localField: 'channel',
foreignField: '_id',
as: 'channel',
},
},
{ $sort: { _id: 1 } },
]);
Output comes as follows
[{"_id":"2022-06-04","count":8,"channel":[]}]
Desired out put is
[{"_id":"2022-06-04","count":8,"channel":"General"}]
You can do like this to get the title from populated channel,
const totalClicks = await StatModel.aggregate([
{ $match: { campaignId: id } },
{
$group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$createAt' } },
count: { $sum: 1 },
},
},
{
$lookup: {
from: 'campaign.channels',
localField: 'channel',
foreignField: '_id',
as: 'channel',
},
},
{ $addFields: { channel: { $arrayElemAt: ['$channel', 0] } } },
{ $project: { _id, 1, count: 1, channel: '$channel.title' } },
{ $sort: { _id: 1 } },
]);

How to aggregate with many conditions on MongoDB. Double $lookup etc

How to display "hardest category" based on in which "study" size of notLearnedWords was the highest. MongoDB Aggregation
I have these 3 models:
Study
WordSet
Category
Study model has reference into WordSet, then WordSet has reference into Category.
And based on Studies i'm displaying statistics.
How i can display "The hardest category" based on size of "notLearnedWords" was the highest?
I don't know on which place i should start with that querying.
For now i display "hardestCategory" as element that is most used.
I think that condition would look something like this:
{ $max: { $size: '$notLearnedWords' } } // size of the study with most notLearnedWords
I would achieve a response like this:
"stats": [
{
"_id": null,
"numberOfStudies": 4,
"averageStudyTime": 82.5,
"allStudyTime": 330,
"longestStudy": 120,
"allLearnedWords": 8
"hardestCategory": "Work" // only this field is missing
}
]
I've tried to do it like this:
const stats = await Study.aggregate([
{ $match: { user: new ObjectID(currentUserId) } },
{
$lookup: {
from: 'users',
localField: 'user',
foreignField: '_id',
as: 'currentUser',
},
},
{
$lookup: {
from: 'wordsets',
let: { wordSetId: '$learnedWordSet' },
pipeline: [
{ $match: { $expr: { $eq: ['$_id', '$$wordSetId'] } } },
{
$project: {
_id: 0,
category: 1,
},
},
{ $unwind: '$category' },
{
$group: {
_id: '$category',
count: { $sum: 1 },
},
},
{ $sort: { count: -1 } },
{ $limit: 1 },
{
$lookup: {
from: 'categories',
localField: '_id',
foreignField: '_id',
as: 'category',
},
},
{
$project: {
_id: 0,
category: { $arrayElemAt: ['$category.name', 0] },
},
},
],
as: 'wordSet',
},
},
{
$group: {
_id: null,
numberOfStudies: { $sum: 1 },
averageStudyTime: { $avg: '$studyTime' },
allStudyTime: { $sum: '$studyTime' },
longestStudy: { $max: '$studyTime' },
allLearnedWords: {
$sum: { $size: '$learnedWords' },
},
hardestCategory: {
$first: {
$first: '$wordSet.category',
},
},
studyWithMostNotLearnedWords: { $max: { $size: '$notLearnedWords' } },
},
},
]);
Study
const studySchema = new mongoose.Schema({
name: {
type: String,
},
studyTime: {
type: Number,
},
learnedWords: [String],
notLearnedWords: [String],
learnedWordSet: {
type: mongoose.Schema.Types.ObjectId,
ref: 'WordSet',
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
});
WordSet
const wordSetSchema = new mongoose.Schema({
name: {
type: String,
},
category: {
type: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
required: true,
},
],
},
});
Category
const categorySchema = new mongoose.Schema({
name: {
type: String,
},
});

Problem in Mongoose $lookup for nested array of objects

Suppose that I have the following three collections:
product: [
{
_id: ObjectId('p1'),
name: 'Box1'
}
]
// ----------
order: [
{
_id: ObjectId('o1'),
productId: ObjectId('p1'),
quantity: 10
},
{
_id: ObjectId('o2'),
productId: ObjectId('p1'),
quantity: 20
}
]
// ----------
status: [
{
_id: ObjectId('s1'),
orderId: ObjectId('o1'),
title: 'in-progress'
},
{
_id: ObjectId('s2'),
orderId: ObjectId('o2'),
title: 'succeeded'
}
]
I need to join these three to get following result:
{
_id: ObjectId('p1'),
name: 'Box1',
order: [
{
_id: ObjectId('o1'),
quantity: 10,
status: {
_id: ObjectId('s1'),
title: 'in-progress'
}
},
{
_id: ObjectId('o2'),
quantity: 20
status: {
_id: ObjectId('s2'),
title: 'succeeded'
}
}
]
}
Actually the problem is inside the order array which has 2 objects in it to correlate with the relevant status collection.
Here what I did something like:
db.getCollection('product').aggregate([
{
$lookup: {
from: 'order',
localField: 'productId',
foreignField: '_id',
as: 'product.order'
},
},
{
$lookup: {
??? // Make status inside each element of order array
},
},
]);
Does anyboady have idea?
You will need to $unwind the result of the first lookup so that you can consider each separately, do a $lookup from the status collection on orderId, and then $group by the product _id and $push the orders back into an array.
The answer:
db.getCollection('product').aggregate([{
$lookup: {
from: 'order',
localField: '_id',
foreignField: 'productId',
as: 'orders'
}
},
{
$unwind: '$orders'
},
{
$lookup: {
from: 'status',
localField: 'orders._id',
foreignField: 'orderId',
as: 'orders.status'
}
},
{
$unwind: '$orders.status'
},
{
$group: {
_id: '$_id',
name: {
$first: '$name'
},
order: {
$push: '$orders'
}
}
}
]);
Then result would be something like this:
[{
_id: ObjectId('p1'),
name: 'Box1',
order: [{
_id: ObjectId('o1'),
productId: ObjectId('p1'),
quantity: 10,
status: {
_id: ObjectId('s1'),
orderId: ObjectId('o1'),
title: 'in-progress'
}
},
{
_id: ObjectId('o2'),
productId: ObjectId('p1'),
quantity: 20,
status: {
_id: ObjectId('s2'),
orderId: ObjectId('o2'),
title: 'succeeded'
}
}
]
}]

How to get array of objects from current collection and data from another collection in same query

I have a users collection. Each user can have multiple friends whose _id values are stored in an array in their user document.
I want to render a list which has a user's list of friend names and under each individual name I want to have a location but the locations are in another document because each user can have multiple locations with no limit on the number, like their visited/favourite places.
These are the 2 queries in isolation but I need to somehow combine them.
This is the query which looks at the user ID values in the user's array of friends and then gets the relevant user info based on the ID.
friends = await User.findOne({ _id: userId })
.populate("friends", "name mobile", null, { sort: { name: 1 } })
.exec();
Then, to get the places for a particular user:
const place = await Place.find({ creator: userId });
So, I basically want to list the friends in a loop, each with their places under their name like:
Joe Soap
- London Bridge
- Eiffel Tower
Bob Builder
- Marienplatz
The data in mongoDb look like this:
Users:
{
"_id": {
"$oid": "5f2d9ec5053d4a754d6790e8"
},
"friends": [{
"$oid": "5f2da51e053e4a754d5790ec"
}, {
"$oid": "5f2da52e053d4a754d6790ed"
}],
"name": "Bob",
"email": "bob#gmail.com",
"created_at": {
"$date": "2020-08-07T18:34:45.781Z"
}
}
Places:
{
"_id": {
"$oid": "5f3695d79864bd6c7c94e38a"
},
"location": {
"latitude": -12.345678,
"longitude": 12.345678
},
"creator": {
"$oid": "5f2da51e053e4a754d5790ec"
},
}
The first join is basically getting data from the array inside the same user collection. The second one is getting data from another collection, places.
UPDATE: almost working
friends = await User.aggregate([
{ $match: { _id: new mongoose.Types.ObjectId(userId) } },
{
$lookup: {
from: "users",
localField: "friends",
foreignField: "_id",
as: "friends_names",
},
},
{ $unwind: "$friends_names" },
{
$lookup: {
from: "places",
localField: "friends",
foreignField: "creator",
as: "friends_places",
},
},
{ $unwind: "$friends_places" },
{
$project: {
"friends_names.name": 1,
"friends_places.saved_at": 1,
},
},
]);
Data returned:
[
{
_id: 5f2d9ec5053d4a754d6790e8,
friends_names: { name: 'Bob' },
friends_places: { saved_at: 2020-08-17T13:40:28.334Z }
},
{
_id: 5f2d9ec5053d4a754d6790e8,
friends_names: { name: 'Natalie' },
friends_places: { saved_at: 2020-08-17T13:40:28.334Z }
}
]
Edit Okay, I believe I have reverse engineered your minimal schema correctly:
const mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/test", { useNewUrlParser: true });
mongoose.set("debug", true);
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open", async function () {
await mongoose.connection.db.dropDatabase();
// we're connected!
console.log("Connected");
const userSchema = new mongoose.Schema({
friends: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
name: String,
});
const placesSchema = new mongoose.Schema({
latitude: String,
creator: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
});
const User = mongoose.model("User", userSchema);
const Place = mongoose.model("Place", placesSchema);
const bob = new User({ name: "Bob", friends: [] });
await bob.save();
const natalie = new User({ name: "Natalie", friends: [bob] });
await natalie.save();
//const chris = new User({ name: "Chris", friends: [] });
//await chris.save();
const john = new User({ name: "John", friends: [natalie, bob] });
await john.save();
const place1 = new Place({ latitude: "Place1", creator: bob });
const place3 = new Place({ latitude: "Place3", creator: bob });
const place2 = new Place({ latitude: "Place2", creator: natalie });
await place1.save();
await place2.save();
await place3.save();
await User.find(function (err, users) {
if (err) return console.error(err);
console.log(users);
});
await Place.find(function (err, places) {
if (err) return console.error(err);
//console.log(places);
});
var cc = await mongoose
.model("User")
.aggregate([
{ $match: { _id: john._id } },
{ $unwind: "$friends" },
{
$lookup: {
from: "places",
localField: "friends",
foreignField: "creator",
as: "friends_places",
}
}, { $lookup: {from: 'users', localField: 'friends', foreignField: '_id', as: 'friend_name'} },//, { $include: 'friends' }
{ $unwind: "$friends_places" }, { $unwind: "$friend_name" }//, { $skip: 1}, {$limit: 1}
])
.exec();
console.log(cc);
});
This is the most relevant part:
var cc = await mongoose
.model("User")
.aggregate([
{ $match: { _id: john._id } },
{ $unwind: "$friends" },
{
$lookup: {
from: "places",
localField: "friends",
foreignField: "creator",
as: "friends_places",
}
}, { $lookup: {from: 'users', localField: 'friends', foreignField: '_id', as: 'friend_name'} },//, { $include: 'friends' }
{ $unwind: "$friends_places" }, { $unwind: "$friend_name" }//, { $skip: 1}, {$limit: 1}
])
.exec();
The first unwind: friends 'flattens' the collections initially. So, basically we've got 'userId | friendId' for each user and friend. Then, for each row, we simply look up places created by him ($lookup). Finally, we unwind friends_places because we don't want them to be rendered as [object] in console output. Additionally, there this $match, because we only want to check one user's friends' places. Considering we want to know friend's name as well, we have to do one more join - this is why there's this second $lookup. After that a simple $unwind to get friend's detail and that's it.
Code yields the following:
[ { _id: 5f3aa3a9c6140e3344c78a45,
friends: 5f3aa3a9c6140e3344c78a44,
name: 'John',
__v: 0,
friends_places:
{ _id: 5f3aa3a9c6140e3344c78a48,
latitude: 'Place2',
creator: 5f3aa3a9c6140e3344c78a44,
__v: 0 },
friend_name:
{ _id: 5f3aa3a9c6140e3344c78a44,
friends: [Array],
name: 'Natalie',
__v: 0 } },
{ _id: 5f3aa3a9c6140e3344c78a45,
friends: 5f3aa3a9c6140e3344c78a43,
name: 'John',
__v: 0,
friends_places:
{ _id: 5f3aa3a9c6140e3344c78a46,
latitude: 'Place1',
creator: 5f3aa3a9c6140e3344c78a43,
__v: 0 },
friend_name:
{ _id: 5f3aa3a9c6140e3344c78a43,
friends: [],
name: 'Bob',
__v: 0 } },
{ _id: 5f3aa3a9c6140e3344c78a45,
friends: 5f3aa3a9c6140e3344c78a43,
name: 'John',
__v: 0,
friends_places:
{ _id: 5f3aa3a9c6140e3344c78a47,
latitude: 'Place3',
creator: 5f3aa3a9c6140e3344c78a43,
__v: 0 },
friend_name:
{ _id: 5f3aa3a9c6140e3344c78a43,
friends: [],
name: 'Bob',
__v: 0 } } ]
So, we've got a flat list of John's friends' places.
Important bit: from: places in $lookup is critical, as we have to use MongoDB's name of the collection, not model name 1.

How to use aggregate to group mongodb

I'm trying to populate my user document with his talents and talent media. But I'm getting repeated objects. I want to get talent as an object and the medias against that talent inside an array.
my user model:
{
_id: '5f1acd6e6985114f2c1567ea',
name: 'test user',
email: 'test#email.com'
}
talent model
{
_id: '5f1acd6e6985114f2c1567fa',
categoryId: '5f1acd6e6985114f2c1567ga',
userId: '5f1acd6e6985114f2c1567ea'
level: '5',
}
talent-media model
{
_id: 5f1acd6e6985114f2c156710',
talentId: '5f1acd6e6985114f2c1567fa',
media: 'file.jpg',
fileType: 'image'
}
I have another model for storing the category
{
_id: '5f1acd6e6985114f2c1567ga',
title: 'java'
}
I want the result as follows
user: {
_id: '',
name: 'test user',
email: 'test#email.com',
talents: [
{
_id: '',
level: '5',
categoryId: {
_id: '',
title: 'java'
},
medias: [
{
_id: '',
file: 'file1.jpg',
fileType: 'image'
},
{
_id: '',
file: 'file2.jpg',
fileType: 'image'
},
]
}
]
}
And I also tried adding talent-medias embedded in talent documents. But in MongoDB document found is not mostly recommended.
Is it better to have talent model like this,
{
_id: '5f1acd6e6985114f2c1567fa',
categoryId: '5f1acd6e6985114f2c1567ga',
userId: '5f1acd6e6985114f2c1567ea'
level: '5',
medias: [
{
_id: '',
file: 'file1.jpg',
fileType: 'image'
},
{
_id: '',
file: 'file2.jpg',
fileType: 'image'
},
]
}
You can achieve this by using $lookup multiple times:
db.users.aggregate([
{
$lookup: {
from: "talents",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$userId",
"$userId"
]
}
}
},
{
$lookup: {
from: "categories",
let: {
categoryId: "$categoryId"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$categoryId",
"$_id"
]
}
}
},
{
$project: {
_id: "",
title: 1
}
}
],
as: "categoryId"
}
},
{
$lookup: {
from: "talent_media",
let: {
talentId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$talentId",
"$talentId"
]
}
}
},
{
$project: {
_id: "",
file: "$media",
fileType: "$fileType"
}
}
],
as: "medias"
}
},
{
$unwind: {
path: "$categoryId",
preserveNullAndEmptyArrays: true
}
},
{
$project: {
_id: "",
categoryId: 1,
level: 1,
medias: 1
}
}
],
as: "talents"
}
},
{
$project: {
_id: "",
email: 1,
name: 1,
talents: 1
}
}
])
*You'll probably have to adapt the collection names.
MongoPlayground
You have to use nested $lookup,
lookup with Talent Collection and match userId
$match for specific user _id document, commented here
db.User.aggregate([
/** add here
Optional for single user
{
$match: {
_id: "XXXXXXXXXXXX"
}
},
*/
{
$lookup: {
from: "Talent",
as: "talents",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$userId",
"$userId"
]
}
}
},
we are now inside Talent Document and lookup with Category Collection and match with categoryId
$unwind category because it will return array and we need object
{
$lookup: {
from: "Category",
as: "categoryId",
localField: "categoryId",
foreignField: "_id"
}
},
{
$unwind: {
path: "$categoryId",
preserveNullAndEmptyArrays: true
}
},
again we are inside Talent Document and lookup with Talent Media Collection and match with talentId
$project for add/remove not needed fields, here removed talentID from TalentMedia nad removed userId from Talent
{
$lookup: {
from: "TalentMedia",
as: "medias",
let: {
talentId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$talentId",
"$talentId"
]
}
}
},
{
$project: {
talentId: 0
}
}
]
}
},
{
$project: {
userId: 0
}
}
]
}
}
])
Separated query in parts for explanation, You can combine as they are in sequence.
Working Playground: https://mongoplayground.net/p/poIbvFUMvT4

Resources