How to perform nested lookup in mongo query? - mongodb

This is my aggregation query
db.user.aggregate([
{ $addFields: { user_id: { $toString: "$_id" } } },
{
$lookup: {
from: "walletData",
let: { id: "$user_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: ["$userId", "$$id"]
},
{
$gt: ["$lastBalance", 0]
}
]
}
}
}
],
as: "balance"
}
}
])
I get the desired output from this result but need to join one more collection in this query. How can i achieve that?
For example consider these collections:
user : {
"_id": ObjectId("xyz")
}
walletData:{
"userId": "xyz",
"lastBalance": 5
}
AnotherWalletdata:{
"userId": "xyz",
"lastBalance": 6
}
I got the result after joining first two tables how do i join the third table only if the balance of the second table(walletData) is greater than zero?
Expected Output :
{"id":"xyz",
"walletdataBal":5,
"AnotherWalletDataBal":6
}

You can join any number of collections by using only $lookup and $unwind one after another followed by Conditional Projection for whatever that's required at last. Below is the well-tested and working solution for the same :
db.user.aggregate([
{$lookup: {from: "walletData", localField: "_id", foreignField: "userId", as: "walletDataBal"}},
{$unwind: "$walletDataBal"},
{$lookup: {from: "anotherwalletData", localField: "_id", foreignField: "userId", as: "anotherWalletDataBal"}},
{$unwind: "$anotherWalletDataBal"},
{$project: {"id": "$_id", "_id": 0, walletDataBal: "$walletDataBal.lastBalance",
anotherWalletDataBal: {$cond:
{if: { $gt: [ "$walletDataBal.lastBalance", 0 ] },
then: "$anotherWalletDataBal.lastBalance",
else: "$$REMOVE" }}}
]).pretty();

You can add another $lookup stage to achieve the output
db.user.aggregate([
{ "$addFields": { "user_id": { "$toString": "$_id" } } },
{ "$lookup": {
"from": "walletData",
"let": { "id": "$user_id" },
"pipeline": [
{ "$match": {
"$expr": {
"$and": [
{ "$eq": ["$userId", "$$id"] },
{ "$gt": ["$lastBalance", 0] }
]
}
}}
],
"as": "balance"
}},
{ "$lookup": {
"from": "anotherWalletData",
"let": { "id": "$user_id" },
"pipeline": [
{ "$match": {
"$expr": {
"$and": [
{ "$eq": ["$userId", "$$id"] },
{ "$gt": ["$lastBalance", 0] }
]
}
}}
],
"as": "anotherWalletData"
}},
{ "$project": {
"walletdataBal": { "$arrayElemAt": ["$balance.lastBalance", 0] },
"anotherwalletdataBal": {
"$arrayElemAt": ["$anotherWalletData.lastBalance", 0]
}
}}
])

{ $unwind: "$balance" },
{ $lookup: {
from: "walletData",
localField: "balance.userId",
foreignField: "userId",
as:"anotherwalletData"
}}
])
I solved my query, I had to apply unwind after the above lookup.

Related

MongoDB: how to aggregate from multiple collections with same aggregation pipeline

I'm trying to get aggregations with same aggregation pipeline including $match and $group operations from multiple collections.
For example,
with a users collection and collections of questions, answers and comments where every document has authorId and created_at field,
db = [
'users': [{ _id: 123 }, { _id: 456} ],
'questions': [
{ authorId: ObjectId('123'), createdAt: ISODate('2022-09-01T00:00:00Z') },
{ authorId: ObjectId('456'), createdAt: ISODate('2022-09-05T00:00:00Z') },
],
'answers': [
{ authorId: ObjectId('123'), createdAt: ISODate('2022-09-05T08:00:00Z') },
{ authorId: ObjectId('456'), createdAt: ISODate('2022-09-01T08:00:00Z') },
],
'comments': [
{ authorId: ObjectId('123'), createdAt: ISODate('2022-09-01T16:00:00Z') },
{ authorId: ObjectId('456'), createdAt: ISODate('2022-09-05T16:00:00Z') },
],
]
I want to get counts of documents from each collections with created_at between a given range and grouped by authorId.
A desired aggregation result may look like below. The _ids here are ObjectIds of documents in users collection.
\\ match: { createdAt: { $gt: ISODate('2022-09-03T00:00:00Z) } }
[
{ _id: ObjectId('123'), questionCount: 0, answerCount: 1, commentCount: 0 },
{ _id: ObjectId('456'), questionCount: 1, answerCount: 0, commentCount: 1 }
]
Currently, I am running aggregation below for each collection, combining the results in the backend service. (I am using Spring Data MongoDB Reactive.) This seems very inefficient.
db.collection.aggregate([
{ $match: {
created_at: { $gt: ISODate('2022-09-03T00:00:00Z') }
}},
{ $group : {
_id: '$authorId',
count: {$sum: 1}
}}
])
How can I get the desired result with one aggregation?
I thought $unionWith or $lookup may help but I'm stuck here.
You can try something like this, using $lookup, here we join users, with all the three collections one-by-one, and then calculate the count:
db.users.aggregate([
{
"$lookup": {
"from": "questions",
"let": {
id: "$_id"
},
"pipeline": [
{
"$match": {
$expr: {
"$and": [
{
"$gt": [
"$createdAt",
ISODate("2022-09-03T00:00:00Z")
]
},
{
"$eq": [
"$$id",
"$authorId"
]
}
]
}
}
}
],
"as": "questions"
}
},
{
"$lookup": {
"from": "answers",
"let": {
id: "$_id"
},
"pipeline": [
{
"$match": {
$expr: {
"$and": [
{
"$gt": [
"$createdAt",
ISODate("2022-09-03T00:00:00Z")
]
},
{
"$eq": [
"$$id",
"$authorId"
]
}
]
}
}
}
],
"as": "answers"
}
},
{
"$lookup": {
"from": "comments",
"let": {
id: "$_id"
},
"pipeline": [
{
"$match": {
$expr: {
"$and": [
{
"$gt": [
"$createdAt",
ISODate("2022-09-03T00:00:00Z")
]
},
{
"$eq": [
"$$id",
"$authorId"
]
}
]
}
}
}
],
"as": "comments"
}
},
{
"$project": {
"questionCount": {
"$size": "$questions"
},
"answersCount": {
"$size": "$answers"
},
"commentsCount": {
"$size": "$comments"
}
}
}
])
Playground link. In the above query, we use pipelined form of $lookup, to perform join on some custom logic. Learn more about $lookup here.
Another way is this, perform normal lookup and then filter out the elements:
db.users.aggregate([
{
"$lookup": {
"from": "questions",
"localField": "_id",
"foreignField": "authorId",
"as": "questions"
}
},
{
"$lookup": {
"from": "answers",
"localField": "_id",
"foreignField": "authorId",
"as": "answers"
}
},
{
"$lookup": {
"from": "comments",
"localField": "_id",
"foreignField": "authorId",
"as": "comments"
}
},
{
"$project": {
questionCount: {
"$size": {
"$filter": {
"input": "$questions",
"as": "item",
"cond": {
"$gt": [
"$$item.createdAt",
ISODate("2022-09-03T00:00:00Z")
]
}
}
}
},
answerCount: {
"$size": {
"$filter": {
"input": "$answers",
"as": "item",
"cond": {
"$gt": [
"$$item.createdAt",
ISODate("2022-09-03T00:00:00Z")
]
}
}
}
},
commentsCount: {
"$size": {
"$filter": {
"input": "$comments",
"as": "item",
"cond": {
"$gt": [
"$$item.createdAt",
ISODate("2022-09-03T00:00:00Z")
]
}
}
}
}
}
}
])
Playground link.

Request to check all array elements

I have a problem with MongoDB syntax.
I have two documents:
alley(the "tree" field is the ID of the tree):
{
"_id": {"$oid": "62572d82cc40164fef7f1a56"},
"name": "good alley",
"tree": [
{"$oid": "626976eb4b93122bc617d701"},
{"$oid": "626976eb4b93122bc617d702"}
]
},
.......
tree:
{
"_id": {"$oid": "626976eb4b93122bc617d701"},
"dateInstall": {"$date": "2021-02-27T00:00:00.000Z"},
"species": [
{"$oid": "62585a63edfc726a4ff24fb8"}
]
},
.......
I need to write a query "an alley where trees were not planted last year"
My Code
db.alley.aggregate([
{
$lookup: {
from: "tree",
localField: "tree",
foreignField: "_id",
as: "tree"
}
},
{
$match: {{$not:{$and:[
{"tree.dateInstall": {$gt: new ISODate("2020-12-31")}},
{"tree.dateInstall": {$lt: new ISODate("2022-01-01")}}
]
}}}
}
]);
You should first $unwind trees in alleys so you can properly $lookup the trees in tree collection. Use pipeline inside lookup to query only trees planted last year. Finally $group trees into alleys again and use $match to filter out those alleys without trees.
db.getCollection("alley").aggregate([
{
$unwind: "$tree",
},
{
$lookup: {
from: "tree",
let: { tree: "$tree" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$$tree", "$_id" ] },
{ $gt: [ "$dateInstall", new ISODate("2020-12-31") ] },
{ $lt: [ "$dateInstall", new ISODate("2022-01-01") ] },
]
}
}
}
],
localField: "tree",
foreignField: "_id",
as: "tree"
}
},
{
$group: {
_id: { id: "$_id", name: "$name" },
trees: { $addToSet: { $first: "$tree" } }
}
},
{
$match: {
trees: { $size: 0 }
}
}
])

how to sort aggregate - Mongodb

db.getCollection('shows').aggregate([
{ $match: { _id: ObjectId("5d622cecbbe890f60ccd1ca4") } },
{ $lookup: { from: "episode", // collection name in db
localField: "_id",
foreignField: "show_id",
as: "episode"
}
},
{ $sort: { 'episode._id': 1 } }
])
So the below works fine however it seems that the sort is not sorting the collection episode in the correct order. It is still putting it oldest to newest when I want to have it newest to oldest.
I am wondering how this is done?
You can use below aggregation
db.getCollection("shows").aggregate([
{ "$match": { "_id": ObjectId("5d622cecbbe890f60ccd1ca4") } },
{ "$lookup": {
"from": "episode",
"let": { "episodeId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$show_id", "$$episodeId"] }}},
{ "$sort": { "_id": 1 }}
],
"as": "episode"
}}
])

How to perform lookup in aggregation when foreignField is in array?

I have two collections:
// users
{
_id: "5cc7c8773861275845167f7a",
name: "John",
accounts: [
{
"_id": "5cc7c8773861275845167f76",
"name": "Name1",
},
{
"_id": "5cc7c8773861275845167f77",
"name": "Name2",
}
]
}
// transactions
{
"_id": "5cc7c8773861275845167f75",
"_account": "5cc7c8773861275845167f76",
}
Using lookup I want to populate _account field in transactions collection with respective element from users.accounts array.
So, I want the final result as:
{
"_id": "5cc7c8773861275845167f75",
"_account": {
"_id": "5cc7c8773861275845167f76",
"name": "Name1",
},
}
I have already tried using this code:
db.transactions.aggregate([
{
$lookup:
{
from: "users.accounts",
localField: "_account",
foreignField: "_id",
as: "account"
}
}
])
In the result account array comes as empty.
What is the correct way to do it ?
You can use below aggregation with mongodb 3.6 and above
db.transactions.aggregate([
{ "$lookup": {
"from": "users",
"let": { "account": "$_account" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$$account", "$accounts._id"] } } },
{ "$unwind": "$accounts" },
{ "$match": { "$expr": { "$eq": ["$$account", "$accounts._id"] } } }
],
"as": "_account"
}},
{ '$unwind': '$_account' }
])
Try with this
I think case 1 is better.
1)-
db.getCollection('transactions').aggregate([
{
$lookup:{
from:"user",
localField:"_account",
foreignField:"accounts._id",
as:"trans"
}
},
{
$unwind:{
path:"$trans",
preserveNullAndEmptyArrays:true
}
},
{
$unwind:{
path:"$trans.accounts",
preserveNullAndEmptyArrays:true
}
},
{$match: {$expr: {$eq: ["$trans.accounts._id", "$_account"]}}},
{$project:{
_id:"$_id",
_account:"$trans.accounts"
}}
])
2)-
db.getCollection('users').aggregate([
{
$unwind:{
path:"$accounts",
preserveNullAndEmptyArrays:true
}
},
{
$lookup:
{
from: "transactions",
localField: "accounts._id",
foreignField: "_account",
as: "trans"
}
},
{$unwind:"$trans"},
{
$project:{
_id:"$trans._id",
_account:"$accounts"
}
}
])

Return the last Document From a Lookup

db.groups.aggregate([
{
$lookup:
{
from: "posts",
localField: "_id",
foreignField: "group",
as: "post"
}
}
])
I'm getting response for groups and all post like.. [{geoup1,[post's array]}, {group2,[post's array]}]
If there is any post I just want last added post into post collection
You can either use $slice
db.groups.aggregate([
{ "$lookup": {
"from": "posts",
"localField": "_id",
"foreignField": "group",
"as": "post"
}},
{ "$addFields": {
"post": { "$slice": ["$post", -1] }
}}
])
Or with MongoDB 3.6, just return the last post using $lookup in it's non-correlated form:
db.groups.aggregate([
{ "$lookup": {
"from": "posts",
"as": "post",
"let": { "id": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$$id", "$group" ] }
}},
{ "$sort": { "_id": -1 } },
{ "$limit": 1 }
]
}}
])
The latter is better because you only return the document from the foreign collection that you actually want.
If you are certain you want "singular" then $arrayElemAt is interchangeable with $slice in the initial example but returns the last element instead of the array of the last element only. You can also add it to the second form to just take one element from the pipeline, which is "always" an array:
db.groups.aggregate([
{ "$lookup": {
"from": "posts",
"as": "post",
"let": { "id": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$$id", "$group" ] }
}},
{ "$sort": { "_id": -1 } },
{ "$limit": 1 }
]
}},
{ "$addFields": {
"post": { "$arrayElemAt": [ "$post", 0 ] }
}}
])
And that way around it's the 0 index rather than -1 for last.