$lookup when foreignField is array - mongodb

I have 2 collections, the first storing the animes watched by each user, their status etc:
const listSchema = mongoose.Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'user'
},
animes: [{
_id: false,
anime: {
type: Schema.Types.ObjectId,
ref: 'anime',
unique: true
},
status: String,
episodes: Number,
rating: Number
}]
});
and the second storing all animes and the relevant information.
I want to show that second collection but adding status and episodes fields that are filled from the list of the logged in user.
I tried the following:
Anime.aggregate([
{
$lookup: {
'from': 'lists',
localField: '_id',
foreignField: 'animes.anime',
'as': 'name'
},
},
{
$unwind: '$animes'
},
{
$project:{
status:'$lists.animes.status'
}
}
]
)
but it returns an empty array.
I also don't know how to filter by userId seeing how the _id is in the foreign collection.

You can use below aggregation
{ "$lookup": {
"from": "lists",
"let": { "id": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$$id", "$animes.anime"] }}},
{ "$unwind": "$animes" },
{ "$match": { "$expr": { "$eq": ["$animes.anime", "$$id"] }}}
],
"as": "name"
}}

Related

MongoDB - query references 2 deep of ObjectIDs

I've inherited a Azure Cosmos database with a MongoDB API. There is extensive use of "discriminators" so a single collection has many different models.
I am trying to query a document three levels deep based on document ids (ObjectId())
Parent Group
{
_id: ObjectId(),
__type: "ParentGroup",
name: "group 1",
subgroups: [
...ObjectIds,
],
}
Sub Group
{
_id: ObjectId(),
__type: "SubGroup",
name: "a text name",
members: [
...ObjectIds,
],
}
Member
{
_id: ObjectId(),
__type: "Member",
name: "string",
email: "",
induction: Date,
}
Examples I've seen deal with nested documents NOT references
Is it possible to query the Member documents and return?
[
{
parentGroup,
subgroups: [
{sub group, members: [...members]},
{sub group, members: [...members]},
{sub group, members: [...members]},
]
},
]
After reading the comments and further reading i've got this. Its almost there but I think the limitation of MongoDB will prevent the solution being in a single query. The goal is to return ParentGroups->Subgroups->Members Where Members have an "induction" value of "whatever". I am either returning ALL ParentGroups or nothing at all
db.development.aggregate([
{
$match: {
__type: "ParentGroup", $expr: {
$gt: [
{ $size: "$subgroups" }, 0
]
}
}
},
{
$lookup: {
from: "development",
localField: "subgroups",
foreignField: "_id",
as: "subgroups"
}
},
{
$unwind: {
path: "$subgroups",
// preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "development",
localField: "subgroups.members",
foreignField: "_id",
as: "subgroups.members"
}
}
])
Solution that worked for me:
db.development.aggregate([
{
$match: {
__type: "ParentGroup",
},
},
{
$lookup: {
from: "development",
localField: "subgroups",
foreignField: "_id",
as: "subgroups",
},
},
{
$unwind: {
path: "$subgroups",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "development",
localField: "subgroups.members",
foreignField: "_id",
as: "subgroups.activities_x",
},
},
{
$unwind: {
path: "$subgroups.members",
preserveNullAndEmptyArrays: true,
},
},
{
$match: { "subgroups.members.meta": { $exists: true } },
},
{
$project: {
_id: 1,
__type: 1,
name: 1,
subgroups: {
_id: 1,
__type: 1,
name: 1,
members: {
_id: 1,
__type: 1,
name: 1,
meta: 1,
},
},
},
},
]);

Mongodb one-to-many get number of children

I have two collections:
Users: [{
_id: 'xxx',
name: 'xxx',
},
...
]
Posts: [{
_id: 'xxx',
userId: 'xxx',
},
...
]
So a user can have multiple posts. I want to get users with the number of posts that each user has. If the user doesn't have any post, it should load 0.
So the result will be:
[{
_id: 'xxx',
name: 'xxx',
numberOfPosts: 'xxx'
},
...
]
Below query is what I have wrote:
$lookup: {
from: 'Posts',
let: {
userId: '$_id',
},
pipeline: [{
$match: {
$expr: {
$eq: ["$userId", "$$userId"]
}
}
}, {
$count: 'posts'
}, {
$project: {
posts: {
$cond: [{
$ifNull: ['$posts', true]
},
'$posts',
0
]},
}
}],
as: 'posts'
}
It doesn't give me 0 if a user doesn't have any post. What was wrong in the query?
You can try simple way,
use $lookup without pipeline
$addFields to count the number of posts using $size
db.Users.aggregate([
{
$lookup: {
from: "Posts",
localField: "_id",
foreignField: "userId",
as: "numberOfPosts"
}
},
{
$addFields: {
numberOfPosts: {
$size: "$numberOfPosts"
}
}
}
])
Playground

MongoDB aggregate returning only specific fields

I have the following code:
const profiles = await Profile.aggregate([
{
$lookup: {
from: "users",
localField: "user",
foreignField: "_id",
as: "user",
},
},
{
$unwind: "$user",
},
{
$match: {
"user.name": {
$regex: q.trim(),
$options: "i",
},
},
},
{
$skip: req.params.page ? (req.params.page - 1) * 10 : 0,
},
{
$limit: 11,
},
{
$group: {
_id: "$_id",
skills:{skills}
user: { name: "$name" },
user: { avatar: "$avatar" },
},
},
]);
I want to return only specific fields like skills _id and user.name and user.avatar, but this doesn't work. I'm pretty sure that the problem is in $group. I want to receive only these fields
[
{
_id: 5ef78d005d23020ca847aa76,
skills: [ 'asd' ],
user: {
_id: 5ef78c7c5d23020ca847aa75,
name: 'Simeon Lazarov',
avatar: 'uploads\\1593286096227 - background.jpg',
}
}
]
You can make use of $project to get specific fields.
After grouping add the below:
{
$project: {_id:1, skills:1, user:1}
}
Projection value of 0 means that the field needs to be excluded, Value 1 represents inclusion of the field.
Document reference: https://docs.mongodb.com/manual/reference/operator/aggregation/project/

How to find by id after lookup aggregation

I have a collection of news articles in mongodb and another collection that maps a user's ID to an article's ID and has a "like" state, which can be either "like" or "dislike" or "none" if no entry with the user and article exists.
Here are both schemas:
// news collection
const articleSchema = new Schema({
title: String,
content: String,
})
// newslikes collection
const articleLikeSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'Client' },
article: { type: Schema.Types.ObjectId, ref: 'News' },
state: { type: String, enum: ['like', 'dislike'] }
})
I'm trying to write an aggregation query which joins these two collection using $lookup and then finds the state of a specific user's like on all articles. This is what I have so far:
const results = await News.aggregate([
{ $match: query },
{ $sort: { date: -1 } },
{ $skip: page * pageLength },
{ $limit: pageLength },
{ $lookup: {
from: 'newslikes',
localField: '_id',
foreignField: 'article',
as: 'likes'
} },
{ $project: {
title: 1,
likes: 1,
content: 1,
// numLikes: { $size: '$likes' }
userLikeStatus: {
$filter: {
input: '$likes',
as: 'like',
cond: {
$eq: ['$user._id', '5ccf13adcec5e6d84f940417']
}
}
}
} }
])
However this is not working. Is what I'm doing even the correct approach or is there a better way to do this rather than $filter?
You can use below aggregation with mongodb 3.6 and above
News.aggregate([
{ "$match": query },
{ "$sort": { "date": -1 } },
{ "$skip": page * pageLength },
{ "$limit": pageLength },
{ "$lookup": {
"from": "newslikes",
"let": { "articleId": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$article", "$$articleId" ] },
"user": mongoose.Types.ObjectId("5ccf13adcec5e6d84f940417")
}}
],
"as": "likes"
}},
{ "$addFields": {
"userLikeStatus": { "$ifNull": [{ "$arrayElemAt": ["$likes.state", 0] }, "none"] }
}}
])
Or the way you are trying
Basically here you need to put $cond for the field userLikeStatus i.e if the $size of the array after $filter is $gte 1 then user likes it else does not.
News.aggregate([
{ "$match": query },
{ "$sort": { "date": -1 } },
{ "$skip": page * pageLength },
{ "$limit": pageLength },
{ "$lookup": {
"from": "newslikes",
"localField": "_id",
"foreignField": "article",
"as": "likes"
}},
{ "$project": {
"title": 1,
"likes": 1,
"content": 1,
// numLikes: { $size: '$likes' }
"userLikeStatus": {
"$let": {
"vars": {
"array": {
"$filter": {
"input": "$likes",
"as": "like",
"cond": { "$eq": ["$$like.user", mongoose.Types.ObjectId("5ccf13adcec5e6d84f940417")] }
}
}
},
"in": {
"$ifNull": [{ "$arrayElemAt": ["$$array.state", 0] }, "none"]
}
}
}
}}
])

How to order mongodb $lookup query

Overview
A few devices are collecting data and sending it to a Node/MongoDb endpoint. Then, the user would use an endpoint to get all that data into a json.
Models
Device Model
const deviceSchema = new Schema({
group: { type: Schema.Types.ObjectId, ref: 'Group' },
deviceId: { type: String, unique: true },
name: String,
notes: String,
pac: String,
endCertificate: String,
lat: Number,
lng: Number
});
Message Model
const messageSchema = new Schema({
deviceId: { type: String, required: true },
raw: { type: String, required: true },
receivedAt: { type: Date, default: Date.now() }
});
One device can have N messages
Problem to solve
I want to get a json that has all the devices and have an array containing
all the messages that belongs to that device.
[
{
"id":"5b86c323e95759603ad7ea54",
"deviceId":"Device 01",
"name":"Device bla",
"notes":"...",
"pac":"pac",
"lat":-20.817396,
"endCertificate":"cert",
"lng":-27.031321,
"messages":[
{
"id":"5b869a42e0b94041b5f21eed",
"deviceId":"Device 01",
"raw":"1111111",
"receivedAt":"2018-08-29T13:04:43.641Z",
"__v":0
},
{
"id":"5b8c782fef4f8e98783f6f35",
"deviceId":"Device 01",
"raw":"2222222",
"receivedAt":"2018-09-01T09:04:43.641Z",
"__v":0
},
{
"id":"5b8c7840ef4f8e98783f6f3e",
"deviceId":"Device 01",
"raw":"3333333",
"receivedAt":"2018-09-02T09:04:43.641Z",
"__v":0
}
]
},
{
"id":"5b8c28ec38c51813cd159bac",
"deviceId":"Device 02",
"name":"Device ...",
"notes":"...",
"lat":-27.812296,
"lng":-27.073314,
"__v":0,
"messages":[
{
"id":"5b8c784cef4f8e98783f6f43",
"deviceId":"Device 02",
"raw":"1111111",
"receivedAt":"2018-09-01T09:04:43.641Z",
"__v":0
}
]
}
]
My solution
To get a json as the one above I have:
const [results, itemCount] = await Promise.all([
Device.aggregate([
{ $match: {} },
{
$lookup: {
from: 'messageschemas',
localField: 'deviceId',
foreignField: 'deviceId',
as: 'messages'
}
}
]).limit(req.query.limit).skip(req.skip)
.exec(),
Device.countDocuments(match)
]);
res.setHeader('X-Total-Count', itemCount);
res.send(results);
My question
How can I order the messages I get from the $lookup into messages[] by
'receivedAt'?
You need to $unwind the messages array and can apply $sort with receivedAt field and finally $group to rollback again into the array.
Device.aggregate([
{ "$match": { }},
{ "$lookup": {
"from": "messageschemas",
"localField": "deviceId",
"foreignField": "deviceId",
"as": "messages"
}},
{ "$unwind": "$messages" },
{ "$sort": { "messages.receivedAt": 1 }},
{ "$group": {
"_id": "$_id",
"messages": { "$push": "$messages" }
}}
])
Which can be simply done with the 3.6 $lookup syntax
Device.aggregate([
{ "$match": { }},
{ "$lookup": {
"from": "messageschemas",
"let": { "deviceId": "$deviceId" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$deviceId", "$$deviceId" ] } } },
{ "$sort": { "receivedAt": 1 }}
],
"as": "messages"
}}
])