I've a collection that has a self-referencing structure between Employees and Managers, linked through _id and managerIDs fields. Managers are employees themselves:
Employee:
{
"_id": "61b9f07300127afb99f8c1ea",
"title": "Developer",
"firstName": "Jack",
"lastName": "Strauss",
"managerIDs": [
"61cedf84800749316306c6da"
],
"deptID": "61b9f073267500832f5d94d0"
},
{
"_id": "61cedf84800749316306c6da",
"title": "Sr. Developer",
"firstName": "Richard",
"lastName": "Haris",
"managerIDs": null,
"deptID": "61b9f073267500832f5d94d0"
},
{
"_id": "61cedf17800749316306c6cf",
"title": "Manager App Development",
"firstName": "Arnold",
"lastName": "Cliff",
"deptID": "61b9f073267500832f5d94d0"
},
{
"_id": "61d4503e1223496ab8a5ae3c",
"title": "Developer",
"firstName": "Andrew",
"lastName": "Turner",
"managerIDs": [
"61cedf17800749316306c6cf",
"61cedf84800749316306c6da"
],
"deptID": "61b9f073267500832f5d94d0"
}
- i.e. an employee can be managed by none, one or more managers (hence the array).
managerIDsfield can have eithernull,undefined, one or more IDs.Developerscan't be managers, so they will not be included in the list of managers. Also, developers are the only ones getting managed.- All non-developer employees are managers and they'll have a count
of developers they managed (i.e. their
idwill appear inmanagerIDsarray for developers).
I've to write a query that will list all managers with their name, title and a count of developers they manage. I've tried $lookup in MongoDB aggregation without much luck.
How can I write a query for a self-referencing structure like this in MongoDB or Mongoose?
CodePudding user response:
If I understand who can be a manager, here's one way you could do it.
db.employees.aggregate([
{ // managers won't have any managerIDs
"$match": {
"managerIDs.0": {"$exists": false}
}
},
{ // find managed Developers
"$graphLookup": {
"from": "employees",
"startWith": "$_id",
"connectFromField": "_id",
"connectToField": "managerIDs",
"as": "managedDevs",
"maxDepth": 0
}
},
{ // format output
"$project": {
"_id": 0,
"title": 1,
"firstName": 1,
"lastName": 1,
"devCount": {"$size": "$managedDevs"}
}
}
])
Sample output:
[
{
"devCount": 2,
"firstName": "Richard",
"lastName": "Haris",
"title": "Sr. Developer"
},
{
"devCount": 1,
"firstName": "Arnold",
"lastName": "Cliff",
"title": "Manager App Development"
}
]
Try it on mongoplayground.net.
Mostly out of curiosity, I "inverted" the query by getting a histogram of the manager "_id"s present in the "managerIDs" arrays and then "$lookup" the manager details.
N.B.: I'm a javascript noob so my "$accumulator" could probably be improved.
db.employees.aggregate([
{// managers won't have any managerIDs
"$match": {
"managerIDs.0": {"$exists": true}
}
},
{ // get histogram of counts per managerID
"$group": {
"_id": null,
"managerIDs": {
"$accumulator": {
"init": "function() {return new Object()}",
"accumulate": "function(state, mgrArray) {mgrArray.forEach((elem) => {state[elem] = state[elem] 1 || 1}); return state}",
"accumulateArgs": ["$managerIDs"],
"merge": "function(state1, state2) {Object.entries(state2).forEach((key, val) => {state1[key] = state1[key] val || val}); return state1}",
"finalize": "function(state) {const obj = Object.assign({},state); return Object.entries(obj).map(([key, val]) => {return {'_id':key, 'count':val}})}",
"lang": "js"
}
}
}
},
{"$unwind": "$managerIDs"},
{
"$lookup": {
"from": "employees",
"localField": "managerIDs._id",
"foreignField": "_id",
"as": "manager"
}
},
{
"$set": {
"manager": {"$first": "$manager"}
}
},
{ // format output
"$project": {
"_id": 0,
"title": "$manager.title",
"firstName": "$manager.firstName",
"lastName": "$manager.lastName",
"devCount": "$managerIDs.count"
}
}
])
Try it on mongoplayground.net.
