Select documents in collection that match foreign key value - node.js

I want to use MongoDB aggregate to grab some documents in collection Events that reference the collection Program with the constraint of Program.type
Events
{
_id: ObjectId,
programId: ObjectId
}
Programs
{
_id: ObjectId,
type: "Type A"
}
The pseudo sql-like query would be like select * from events where event.id = 1234 and where program.type = "Type A"
I've got this and I have no idea what I'm doing.
const pipeline = [
{
$match: {_id: id}
},
{
$lookup: {
from: 'programs',
localField: '_id',
foreignField: 'programId',
as: 'program'
}
},
{
$unwind: '$program'
},
{
$match: {'program.type': 'Type A'}
}
]
I actually thought this worked but it failed when I tried different types.

If I'm not wrong, this query:
select * from events where event.id = 1234 and where program.type = "Type A"
Is this query:
db.events.aggregate([
{
"$match": {
"_id": 1
}
},
{
"$lookup": {
"from": "programs",
"as": "programs",
"pipeline": [
{
"$match": {
"$expr": {
"$eq": [
"$type",
"A"
]
}
}
}
]
}
}
])
Where:
select * -> is get all fields by default in mongo db
from events -> db.events.aggregate
where event.id = 1234 -> is the $match
program.type = "Type A" -> $lookup with pipeline where {"$eq": ["$type","A"]}
Example here

Related

How get all objects from an array of Id's in mongoose with express nodejs?

I'm developing a Chat application where I'm saving the conversation list in the DB like this;
Conversations Collection:
{
members: [ "123", "456" ]
...rest
}
and User Collection:
{
_id: ObjectId( "123" ), name: "anyone"
},
{
_id: ObjectId( "456" ), name: "someone"
}
I want the result to be like:
{
members: [
{_id: ObjectId( "123" ), name: "anyone"},
{_id: ObjectId( "456" ), name: someone"}
]
}
I know aggregation with lookup is the rescue but can't find a way how to fetch all ids from that member's array because in the future it can be 20 to 30 ids. If it is a simple field then I'm able to fetch but with an array, I can't.
I have tried this
db.conversations.aggregate([
{
"$lookup": {
"from": "users",
"localField": "members",
"foreignField": "_id",
"as": "members_details"
}
}
])
but it returns members_details: [ ]
You could use the following query to accomplish what you want. You'll need to use $lookup since you are wanting to gather data from a different collection then the one you are currently querying.
You can check out a live demo here
Here is an updated live demo
Database
db={
"conversations": [
{
members: [
123,
456
]
}
],
"users": [
{
"_id": 123,
"name": "foo"
},
{
"_id": 456,
"name": "bar"
}
]
}
Query
db.conversations.aggregate([
{
"$lookup": {
"from": "users",
"localField": "members",
"foreignField": "_id",
"as": "members"
}
},
{
$project: {
_id: 0,
members: 1
}
}
])
Result
[
{
"members": [
{
"_id": 123,
"name": "foo"
},
{
"_id": 456,
"name": "bar"
}
]
}
]
Update
See new live demo here
New Query
db.conversations.aggregate([
{
$unwind: "$members"
},
{
"$lookup": {
"from": "users",
"as": "membersFlat",
"let": {
memberObjectId: {
"$toObjectId": "$members"
}
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$memberObjectId",
"$_id"
]
}
}
}
]
}
},
{
$group: {
_id: null,
members: {
$push: {
"_id": {
$first: "$membersFlat._id"
},
"name": {
$first: "$membersFlat.name"
}
}
}
}
},
{
$project: {
_id: 0
}
}
])
New Result
[
{
"members": [
{
"_id": ObjectId("124578987898787845658574"),
"name": "foo"
},
{
"_id": ObjectId("124578986532124578986532"),
"name": "bar"
}
]
}
]
Since your members id is string in conversation collection, you can convert it to object id and the join to users collection,
$addFields to update members array
$map to iterate loop of members array
$toObjectId to convert string object id type to object id type
db.conversations.aggregate([
{
$addFields: {
members: {
$map: {
input: "$members",
in: {
$toObjectId: "$$this"
}
}
}
}
},
{
$lookup: {
from: "users",
localField: "members",
foreignField: "_id",
as: "members"
}
}
])
Playground
You can use a normal .find() like this:
const members = [
ObjectId('4ed3ede8844f0f351100000c'),
ObjectId('4ed3f117a844e0471100000d'),
ObjectId('4ed3f18132f50c491100000e')
]
const docs = await model.find({
'_id': { $in: members }
}).exec()
Install mongoose-autopopulate. And in your model code
import {Schema, model} from 'mongoose';
const modelSchema = new Schema({
...
chats: [{type: Schema.Types.ObjectId, ref: "Chat", autopopulate: true}]
})
modelSchema.plugin(require('mongoose-autopopulate'));
...

lookup with add extra field in mongodb

My OBJ
[{
_id:XXXXXXXXXX,
role:admin
},
{
_id:XXXXXXXXXX,
role:superUser
}]
and need results using aggregation how to solve this using aggregation
[{
name:'username'
role:'test'
}
]
I suppose you need the following
let db1 = db.get().collection(`temp1`);
let db2 = db.get().collection(`temp2`);
await db1.aggregate([
{
$lookup: {
from: "temp2",
localField: "_id", // field in the orders collection
foreignField: "_id", // field in the items collection
as: "users"
}
},
{
$replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ["$users", 0] }, "$$ROOT"] } }
},
{ $project: { users: 0 } }
]).toArray()

MongoDB $lookup in different collections by _id

I have 3 mongoDB collections
I need to aggregate them with $lookup operator but I didn't find anything/**or I'm bad looking **
1st one is suppliers
{
"_id" : ObjectId("111"), //for example, in db is mongodb ids
"name" : "supplier 1",
}
{
"_id" : ObjectId("222"),
"name" : "supplier 1",
}
2nd one is clients
{
"_id" : ObjectId("333"), //for example, in db is mongodb ids
"name" : "clients 1",
}
{
"_id" : ObjectId("444"),
"name" : "clients 2",
}
and 3rd is moves
{
"_id" : ObjectId("..."), //for example, in db is mongodb ids
"moveName" : "move 1",
"agent": ObjectId("111") // this is from suppliers collection
}
{
"_id" : ObjectId("..."),
"moveName" : "move 2",
"agent": ObjectId("333") // this one is from CLIENTS collection
}
so like output I need data like this
{
"_id" : ObjectId("..."), //for example, in db is mongodb ids
"moveName" : "move 1",
**"agent": supplier 1** // this is from suppliers collection
}
{
"_id" : ObjectId("..."),
"moveName" : "move 2",
**"agent": clients 1** // this one is from CLIENTS collection
}
back end is nodejs, I`m using mongoose, how I can search in 2nd collection if noresult in 1st?
const moves = await Move.aggregate([
{ $match: query }, // here all wokrs good
{
$lookup: {
from: 'clients',
localField: 'agent',
foreignField: '_id',
as: 'agent'
}
},{ $unwind: {path: "$agent" , preserveNullAndEmptyArrays: true} },
{
$lookup: {
from: 'suppliers',
localField: 'agent',
foreignField: '_id',
as: 'agent2'
}
},
{
$project: {
operationName: 1,
agent: {$ifNull: ['$agent.name', '$agent2.name']}
}
}
])
Thank You!
As suggested by #hhharsha36, we can use $facet operator which allows to run several pipelines within a single stage.
Explanation
facet
suppliers = $lookup suppliers collection and filter only matched results
clientes = $lookup clientes collection and filter only matched results
concatArrays = We concat suppliers and clients results into a single movies array
unwind = We flatten movies array [a, b, c] -> a
b
c
replaceWith = We replace the root element [movies:a, movies:b -> a, b]
mergeObject = allows us to pick the agent name (this way we avoid 1 more stage)
db.moves.aggregate([
{
$facet: {
suppliers: [
{
$lookup: {
from: "suppliers",
localField: "agent",
foreignField: "_id",
as: "agent"
}
},
{
$match: {
agent: {
$not: {
$size: 0
}
}
}
}
],
clients: [
{
$lookup: {
from: "clients",
localField: "agent",
foreignField: "_id",
as: "agent"
}
},
{
$match: {
agent: {
$not: {
$size: 0
}
}
}
}
]
}
},
{
$project: {
movies: {
"$concatArrays": [
"$clients",
"$suppliers"
]
}
}
},
{
$unwind: "$movies"
},
{
$replaceWith: {
"$mergeObjects": [
"$movies",
{
agent: {
"$arrayElemAt": [
"$movies.agent.name",
0
]
}
}
]
}
}
])
MongoPlayground
This aggregation query gives the desired result:
db.moves.aggregate([
{
$lookup: {
from: "suppliers",
localField: "agent",
foreignField: "_id",
as: "moves_sup"
}
},
{
$unwind: { path: "$moves_sup" , preserveNullAndEmptyArrays: true }
},
{
$lookup: {
from: "clients",
localField: "agent",
foreignField: "_id",
as: "moves_client"
}
},
{
$unwind: { path: "$moves_client" , preserveNullAndEmptyArrays: true }
},
{
$addFields: {
agent: {
$cond: [ { $eq: [ { $type: "$moves_sup" }, "object" ] },
"$moves_sup.name",
{ $cond: [ { $eq: [ { $type: "$moves_client" }, "object" ] }, "$moves_client.name", "undefined" ] }
] },
moves_client: "$$REMOVE",
moves_sup: "$$REMOVE"
}
},
])

Display array of documents as objects with id as key

Is there a way to use project or group in mongo to convert an array of documents or subdocuments into an object with documents ObjectId as key and document/subdocument as value?
For example:
const book = Schema({
name: {type: String},
authors: [{type: mongoose.Schema.Types.ObjectId, ref: 'Author'}]
})
const author = Schema({name: {type: String}})
If you query for all books:
Book.find({}).populate('authors').lean().exec()
then you get:
[{
id: 10,
name: 'Book 1',
authors: [{
id: 1,
name: 'Author1'
}]
},...]
but I would like to have this:
[{
id: 10,
name: 'Book 1',
authors: {
1: {id: 1, name: 'Author 1'}
}
},...]
I know that iterating over the objects after querying from mongo will do it but I guess that running the query at mongo is more efficient.
The main consideration here is that the "keys" you want are actually ObjectId values as defined in your schema and not really a "string", which is actually a requirement for a JavaScript Object since all "keys" must be a "string". For JavaScript this really is not much of an issue since JavScript will "stringify" any argument specified as a "key", but it does matter for BSON, which is what MongoDB actually "speaks"
So you can do this with MongoDB, but you need MongoDB 4.x at least in order to support the $convert aggregation operator or it's shortcut method $toString. This also means that rather than populate(), you actually use MongoDB $lookup:
let results = await Books.aggregate([
{ "$lookup": {
"from": Author.collection.name,
"localField": "authors",
"foreignField": "_id",
"as": "authors"
}},
{ "$addFields": {
"authors": {
"$arrayToObject": {
"$map": {
"input": "$authors",
"in": { "k": { "$toString": "$$this._id" }, "v": "$$this" }
}
}
}
}}
])
Or if you prefer the alternate syntax:
let results = await Books.aggregate([
{ "$lookup": {
"from": "authors",
"let": { "authors": "$authors" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$authors" ] } } },
{ "$project": {
"_id": 0,
"k": { "$toString": "$_id" },
"v": "$$ROOT"
}}
],
"as": "authors"
}},
{ "$addFields": {
"authors": { "$arrayToObject": "$authors" }
}}
])
Which would return something like:
{
"_id" : ObjectId("5c7f046a7cefb8bff9304af8"),
"name" : "Book 1",
"authors" : {
"5c7f042e7cefb8bff9304af7" : {
"_id" : ObjectId("5c7f042e7cefb8bff9304af7"),
"name" : "Author 1"
}
}
}
So the $arrayToObject does the actual "Object" output where you supply it an array of objects with k and v properties corresponding to key and value. But it must have a valid "string" in "k" which is why you $map over the array content to reformat it first.
Or as the alternate, you can $project within the pipeline argument of $lookup instead of using $map later for exactly the same thing.
With client side JavaScript, the translation is a similar process:
let results = await Books.aggregate([
{ "$lookup": {
"from": Author.collection.name,
"localField": "authors",
"foreignField": "_id",
"as": "authors"
}},
/*
{ "$addFields": {
"authors": {
"$arrayToObject": {
"$map": {
"input": "$authors",
"in": { "k": { "$toString": "$$this._id" }, "v": "$$this" }
}
}
}
}}
*/
])
results = results.map(({ authors, ...rest }) =>
({
...rest,
"authors": d.authors.reduce((o,e) => ({ ...o, [e._id.valueOf()]: e }),{})
})
)
Or with populate()
let results = await Book.find({}).populate("authors");
results = results.map(({ authors, ...rest }) =>
({
...rest,
"authors": (!authors) ? {} : authors.reduce((o,e) => ({ ...o, [e._id.toString()]: e }),{})
})
)
NOTE however that populate() and $lookup are really quite different. MongoDB $lookup is a single request to the server that returns one response. Using populate() actually invokes multiple queries and does the "joining" in client side JavaScript code even if it hides what it is doing from you.

mongo use $lookup after $group

I have a visits collection where I successfully count the number of visits per location
Visits model:
{
"_id": {
"$oid": "5a3969e2f4ea3e33ac5a523d"
},
"locationId": "5a395ccf210a1d35d0df4a58"
}
locationId above is of type 'Object' as I learned lookup localField and foreignField must be of same type
nodejs code =>
let sort = { "count": -1, "locationId": 1 };
Visit.aggregate([
{
$match:{
$and: [
{ accountId: req.session.passport.user },
{
'details.dateTimeIn': {
$gte: new Date(dateFrom), $lte: new Date(dateTo)
}
}
]
}
},
{
"$group": {
//_id: name,
_id: "$locationId",
count: { $sum: 1 }
}
},
{ $sort: sort }
])
Output is half ok:
[
{
"_id":"5a395ccf210a1d35d0df4a58",
"count":20
}
]
Instead of showing location id id like to show location name. Schema for locations collection is:
{
"_id": {
"$oid": "5a395ccf210a1d35d0df4a58"
"name": "Tower A",
"__v": 0
}
}
Research suggests I need to use $lookup to get that JOIN effect
So I tried
{
"$lookup": {
from: "locations",
localField: "_id",
foreignField: "_id",
as: "locationdetails"
}
}
but the match seems broken. The closest I got was a list of all locations in 'locationdetails'
But with code above here is the empty locationdetails
[
{
"_id":"5a395ddf1d221918d0041313",
"count":20,
"locationdetails":[
]
}
]
What am I missing ?

Resources