mongo lookup where array fields don't match - arrays

I have two collections: Users and Roles.
Both use ObjectIds as their index.
Users has a field called Roles which holds an array of Role ObjectIds.
Users:
{
"_id" : ObjectId("590253b50985e614aaa90098"),
"roles" : [
ObjectId("57d624612808daf641fafae3"),
ObjectId("5a2da7e37f1c84d172161273"),
ObjectId("5a2ede157f1c84d172161d33"),
ObjectId("5a2ede927f1c84d172161d34")
}
Roles:
{
"_id" : ObjectId("57c6371cf541a6c9457f1319"),
"name" : "Admin"
}
I'm trying to identify the Role objectIds in the roles array of the User collection that DO NOT have a reference in the Roles Collection.
Any ideas? I've tried Aggregates, lookups, foreach, nin.. and have not found the right combination. Clearly I'm new to mongo :p
Thanks in advance for any help!

You must use $unwind, $lookup, $match and $group:
db.Users.aggregate([
{
$unwind: "$roles"
},
{
$lookup:
{
from: "Roles",
localField: "roles",
foreignField: "_id",
as: "Role_info"
}
},
{
$match : {
"Role_info.0": {$exists:false}
}
},
{
$group: {
_id: "$roles"
}
}]);

Related

MongoDB $match in a aggregate lookup not working as expected

When i run this query:
db.friendRequests.aggregate([
$lookup: {
from: "users",
localField: "author",
foreignField: "_id",
pipeline: [
{
$match: {
$expr: {
friend_id: new mongoose.Types.ObjectId(userid),
},
},
},
],
as: "userdata",
}
])
It returns every entry in the collection, but theres a pipeline in it. Then why is it not working?
Can you help me? Thanks!
Playground:
https://mongoplayground.net/p/Eh2j8lU4IQl
The friend_id field is present in the friendRequests collection (source for the aggregation) not the users collection which is the target for the $lookup. Therefore that predicate should come in a $match stage that precedes the $lookup:
db.friendRequests.aggregate([
{
$match: {
"friend_id": ObjectId("636a88de3e45346191cf4257")
}
},
{
$lookup: {
from: "users",
localField: "author",
foreignField: "_id",
as: "userdata"
}
}
])
See how it works in this playground example. Note that I changed inventory to users assuming that was just a typo in the collection name in the provided playground link.
Original answer
This syntax is incorrect:
$match: {
$expr: {
friend_id: new mongoose.Types.ObjectId(userid),
},
}
You should change it to either
$match: {
friend_id: new mongoose.Types.ObjectId(userid),
}
Or
$match: {
$expr: {
$eq: [
"$friend_id", new mongoose.Types.ObjectId(userid)
]
},
}
For mongodb version under 5.0 (Thanks for the remark #user20042973):
$lookup with localField and foreignField will ignore a pipeline. Remove them and add a let key in order to enable the pipeline.

Create a new array of objects in a aggregate query in Mongodb

I'm running a query on Mongodb to get the combine data from two different collections: User and Store.
User collection has a property named as store_ids, which is an array that contains a list of ObjectIds of each store that the User has access to.
I'm trying to add the name of each store in the query result.
Example:
User Document:
{
_id: '58ebf8f24d52e9ab59b5538b',
store_ids: [
ObjectId("58dd4bb10e2898b0057be648"),
ObjectId("58ecd57d1a2f48e408ea2a30"),
ObjectId("58e7a0766de7403f5118afea"),
]
}
Store Documents:
{
_id: "58dd4bb10e2898b0057be648",
name: "Store A",
},
{
_id: "58ecd57d1a2f48e408ea2a30",
name: "Store B",
},
{
_id: "58e7a0766de7403f5118afea",
name: "Store C"
}
I'm looking for a query that returns an output like this:
{
_id: '58ebf8f24d52e9ab59b5538b',
stores: [
{
_id: ObjectId("58dd4bb10e2898b0057be648"),
name: "Store A"
},
{
id: ObjectId("58ecd57d1a2f48e408ea2a30"),
name: "Store B"
},
{
_id: ObjectId("58e7a0766de7403f5118afea"),
name: "Store C"
}
]
}
I've already tried operations like $map and $set. I don't know if I'm applying them in the right way because they didn't work for my case.
You can use an aggregate query:
db.users.aggregate([
{
$lookup: {
from: "stores", //Your store collection
localField: "store_ids",
foreignField: "_id",
as: "stores"
}
},
{
$project: {
store_ids: 0
}
}
])
You can see a working example here: https://mongoplayground.net/p/ICsEEsmRcg0
We can achieve this with a simple $lookup and with $project.
db.user.aggregate({
"$lookup": {
"from": "store",
"localField": "store_ids",
"foreignField": "_id",
"as": "stores"
}
},
{
"$project": {
store_ids: 0
}
})
$lookup will join with store table on with the store_ids array where the _id matches
$project removes the store_ids array from the resulting objects
Playground

Equivalent to Mongoose `.populate` for ObjectIds in Array that maintains array order

What’s the MongoDB aggregation equivalent to Mongoose’s .populate on arrays? Specifically, I’m wondering if there’s anything that will a) perform the functionality of $lookup but b) still keep array order with the returned objects.
I.e., looking up the items in [ ObjectId(b), ObjectId(a), ObjectId(c) ] should return the array in the same order [ ItemB, ItemA, ItemC ], and not [ItemA, ItemB, ItemC] or any other permutation.
One option is to use a regular $lookup and then order the results:
db.colA.aggregate([
{$lookup: {
from: "colB",
localField: "data",
foreignField: "_id",
as: "newData"
}},
{$set: {
newData: "$$REMOVE",
data: {
$map: {
input: "$data",
in: {$arrayElemAt: ["$newData", {$indexOfArray: ["$newData._id", "$$this"]}]}
}
}
}
}
])
See how it works on the playground example

Combine Mongo Documents after multiple lookups in single aggregation

I'm stuck trying to combine my document results. Here is my query and data
{"_id":"5c21ab13d03013b384f0de26",
"roles":["5c21ab31d497a61195ce224c","5c21ab4ad497a6f348ce224d","5c21ab5cd497a644b6ce224e"],
"agency":"5b4ab7afd6ca361cb38d6a60","agents":["5b4ab5e897b24f1c4c8e3de3"]}
Here is the query
return db.collection('projects').aggregate([
{
$match: {
agents: ObjectId(agent)
}
},
{
$unwind: "$agents"
},
{
$lookup: {
from: "agents",
localField: "agents",
foreignField: "_id",
as: "agents"
}
},
{
$unwind: {
path: "$roles",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "roles",
localField: "roles",
foreignField: "_id",
as: "roles"
}
},
{
$lookup: {
from: "agencies",
localField: "agency",
foreignField: "_id",
as: "agency"
}
}
])
As you can see, an entry in the project collection has two arrays that are unwound before a lookup on each entry is performed and then a final lookup is performed on the "agency" field.
However when I get the results from this query I am getting a document count equal to the number of roles. For example the project I am aggregating has 3 roles and 1 agent. So I am getting back an array of 3 objects, one for each role rather than a single document with the roles array containing all three roles. There is also a chance the agents array can have more than one value.
So lost...
You don't have to run $unwind before $lookup. The localField section states that:
If your localField is an array, you may want to add an $unwind stage to your pipeline. Otherwise, the equality condition between the localField and foreignField is foreignField: { $in: [ localField.elem1, localField.elem2, ... ] }
So basically if you don't run $unwind for instance on roles then instead of document per role you will get an array of roles as ObjectIds replaced by an array of objects from that second collection.
So you can try following aggregation:
db.collection('projects').aggregate([
{
$match: {
agents: ObjectId(agent)
}
},
{
$lookup: {
from: "agents",
localField: "agents",
foreignField: "_id",
as: "agents"
}
},
{
$lookup: {
from: "roles",
localField: "roles",
foreignField: "_id",
as: "roles"
}
},
{
$lookup: {
from: "agencies",
localField: "agency",
foreignField: "_id",
as: "agency"
}
}
])

$lookup result sorted using a key from another array

I have the following data structures:
playlist collection:
{
_id:123,
artistId:959789,
title:'playlist1',
tracks:[{trackId:123456,createdDate:03.02.2017},
{trackId:213556,createdDate:04.02.2017},
{trackId:956125,createdDate:05.02.2017}]
},
{
_id:456,
artistId:456456,
title:'playlist2',
tracks:[{trackId:956336,createdDate:03.02.2017},
{trackId:213556,createdDate:09.02.2017},
{trackId:785556,createdDate:011.02.2017}]
},
{
_id:456,
artistId:456456,
title:'playlist3',
tracks:[{trackId:636985,createdDate:01.02.2017},
{trackId:456585,createdDate:06.02.2017},
{trackId:785556,createdDate:09.02.2017}]
}
The trackId in the tracks array of a playlist is the _id of a track in track collection
tracks collection:
{_id:956336,title:'abc'},
{_id:785556,title:'cdf'},
{_id:456585,title:'ghi'},
{_id:213556,title:'xyz'},
{_id:636985,title:'lmn'}
What i did was an aggregate $lookup using the trackId in the tracks array and i got the result. But the playlistTracks was sorted in some other order not in the order of the tracks array order.
{
$match: {artistId: 456}
},
{
$lookup: {
from: 'tracks',
localField: 'tracks.trackId',
foreignField: '_id',
as: 'playlistTracks'
}
},
Now what I need is to get the list of playlists by a particular artist having the following structure :
The playlistTracks should be sorted in the order on the createdDate in the tracks array.
{
_id:456,
title:'playlist2',
tracks:[{trackId:636985,createdDate:01.02.2017},
{trackId:456585,createdDate:06.02.2017},
{trackId:785556,createdDate:09.02.2017}]
playlistTracks:[{_id:956336,title:'abc'},
{_id:213556,title:'xyz'},
{_id:785556,title:'cdf'}]
},
{
_id:456,
title:'playlist2',
tracks:[{trackId:636985,createdDate:01.02.2017},
{trackId:456585,createdDate:06.02.2017},
{trackId:785556,createdDate:09.02.2017}]
playlistTracks:[{_id:636985,title:'lmn'},
{_id:456585,title:'ghi'},
{_id:785556,title:'cdf'}]
}
Follow below steps
1 unwind the tracks array in playlist collection
2 $lookup match with tracks collection
3 add createddate of tracks array to lookup result as a new key
4 sort based on new key
5 group the results for your requirements
So these are the documents I added, to reproduce your use case:
Playlist collection
{
"_id" : NumberInt(123),
"artistId" : NumberInt(959789),
"title" : "playlist1",
"tracks" : [
{
"trackId" : NumberInt(123456),
"createdDate" : "03.02.2017"
},
{
"trackId" : NumberInt(213556),
"createdDate" : "04.02.2017"
},
{
"trackId" : NumberInt(956125),
"createdDate" : "05.02.2017"
}
]
}
{
"_id" : NumberInt(456),
"artistId" : NumberInt(456456),
"title" : "playlist2",
"tracks" : [
{
"trackId" : NumberInt(956336),
"createdDate" : "03.02.2017"
},
{
"trackId" : NumberInt(213556),
"createdDate" : "09.02.2017"
},
{
"trackId" : NumberInt(785556),
"createdDate" : "11.02.2017"
}
]
}
{
"_id" : NumberInt(457),
"artistId" : NumberInt(456456),
"title" : "playlist3",
"tracks" : [
{
"trackId" : NumberInt(636985),
"createdDate" : "01.02.2017"
},
{
"trackId" : NumberInt(456585),
"createdDate" : "06.02.2017"
},
{
"trackId" : NumberInt(785556),
"createdDate" : "09.02.2017"
}
]
}
I changed the last duplicate _id on the playlist collection with _id: 457. I don't know how you could have two documents with same _id. _id field has to be unique. And I'm not sure I understand correct your desired result, because in your $match query your write the following: $match: {artistId: 456} but in your data there is no artiseId with 456.
and this date
{trackId:785556,createdDate:011.02.2017}
from document id_ 456 I changed to
{trackId:785556,createdDate:"11.02.2017"}
cause the date looked weird. It also looks like your date fields are strings, cause it certainly doesn't look like a date field. Either way the $sort works for both usecases.
The tracks collection I left as in your example.
So this seems to be what you need?
db.playlist.aggregate([
{
$match: {_id: {$in: [456]}}
},
{ $unwind: "$tracks"},
{$sort: {"tracks.createdDate": 1}},
{
$lookup: {
from: 'tracks',
localField: 'tracks.trackId',
foreignField: '_id',
as: 'playlistTracks'
}
},
{
$group:{
_id: "$_id",
artistId: {$first: "$artistId"},
title: {$first: "$title"},
tracks: { $push: { item: "$tracks.trackId", quantity: "$tracks.createdDate" } },
playlistTracks: { $push: "$playlistTracks" }
}
}
])
This puts both arrays into same order. You can specify here {$sort: {"tracks.createdDate": 1}} if you want ascending or descending -1 order
So before looking up the fields you can unwind and sort you playlist array.
Hope this works

Resources