mongodb - Consultas jerárquicas con Mongo usando $ graphLookup
(3)
Tengo una colección de empleados con medio millón de registros. Cada registro tendrá los siguientes detalles.
El documento mongo es el siguiente.
{
"_id": "234463456453643563456",
"name": "Mike",
"empId": "10",
"managerId": "8",
"projects" : [ "123", "456", "789"]
}
- Cuando doy un vacío, debería devolver las jerarquías completas de ese administrador al nivel inferior junto con el siguiente filtro.
a. filtrar en la ubicación
si. filtrar en proyectos
El resultado debería ser como,
10 ->>> Manager
//
/ /
8 6 ---->> 8 & 6 reporting to manager 10
// //
/ / / /
4 5 2 1 ---->> 4 & 5 reporting to manager 8 ...
¿Se apreciará alguna ayuda para obtener los resultados jerárquicos con el nivel?
No puedo obtener el resultado como se esperaba.
Data de muestra :-
db.getCollection("employees").insert({"_id":"10","empId": "10","name":"Employee10","managerId":"15" });
db.getCollection("employees").insert({"_id":"8","empId": "8","name":"Employee8","managerId":"10" });
db.getCollection("employees").insert({"_id":"6","empId": "6","name":"Employee6","managerId":"10" });
db.getCollection("employees").insert({"_id":"4","empId": "4","name":"Employee4","managerId":"8" });
db.getCollection("employees").insert({"_id":"5","empId": "5","name":"Employee5","managerId":"8" });
db.getCollection("employees").insert({"_id":"2","empId": "2","name":"Employee2","managerId":"6" });
db.getCollection("employees").insert({"_id":"1","empId": "1","name":"Employee1","managerId":"6" });
Consulta :-
db.getCollection(''employees'').aggregate([
{
$match: {
empId : "10"
}
},
{
$graphLookup: {
from: "employees",
startWith: "$empId",
connectFromField: "empId",
connectToField: "managerId",
as: "reportees",
maxDepth: 4,
depthField: "level"
}
},
{
$project: {
"empId":1,
"managerId":1,
"reportees.empId":1,
"reportees.name":1,
"reportees.managerId":1,
"reportees.level":1
}
}
]);
Resultado actual :-
{
"_id" : "10",
"empId" : "10",
"managerId" : "15",
"reportees" : [
{
"empId" : "1",
"name" : "Employee1",
"managerId" : "6",
"level" : NumberLong(1)
},
{
"empId" : "4",
"name" : "Employee4",
"managerId" : "8",
"level" : NumberLong(1)
},
{
"empId" : "2",
"name" : "Employee2",
"managerId" : "6",
"level" : NumberLong(1)
},
{
"empId" : "5",
"name" : "Employee5",
"managerId" : "8",
"level" : NumberLong(1)
},
{
"empId" : "6",
"name" : "Employee6",
"managerId" : "10",
"level" : NumberLong(0)
},
{
"empId" : "8",
"name" : "Employee8",
"managerId" : "10",
"level" : NumberLong(0)
}
]
}
Resultado Esperado :-
{
"_id" : "10",
"empId" : "10",
"managerId" : "15",
"reportees" : [
{
"empId" : "6",
"name" : "Employee6",
"managerId" : "10",
"level" : NumberLong(0),
"reportees" : [
{
"empId" : "1",
"name" : "Employee1",
"managerId" : "6",
"level" : NumberLong(1)
},
{
"empId" : "2",
"name" : "Employee2",
"managerId" : "6",
"level" : NumberLong(1)
}
]
},
{
"empId" : "8",
"name" : "Employee8",
"managerId" : "10",
"level" : NumberLong(0),
"reportees" : [
{
"empId" : "5",
"name" : "Employee5",
"managerId" : "8",
"level" : NumberLong(1)
},
{
"empId" : "4",
"name" : "Employee4",
"managerId" : "8",
"level" : NumberLong(1)
}
]
}
]
}
Preguntas:
- ¿Es posible obtener el resultado esperado con $ graphLookup?
- Además, ¿es posible obtener el recuento en el nivel superior y también para cada nivel secundario?
- ¿Cómo aplicar la proyección a todos los niveles?
- ¿Cómo aplicar el filtro encima de esto?
Creo que tener
un
campo de
nivel
podemos construir una estructura jerárquica a partir de una matriz usando
$reduce
.
Para lograr eso, necesitamos obtener
reportees
ordenados por nivel descendente después de
$graphLookup
.
Desafortunadamente, la única forma de hacerlo actualmente es usar
$unwind
+
$sort
+
$group
que hace que la agregación sea bastante larga.
Entonces podemos procesar esa matriz ordenada usando
$reduce
.
En cada paso solo tenemos que agregar un empleado al conjunto de resultados, incluidos sus
reportees
del nivel anterior.
Además, debemos detectar cuándo cambia el
level
durante nuestro procesamiento y reorganizar las matrices auxiliares en ese caso.
$addFields
simplemente reemplaza el campo de
reportees
existente en este caso.
$concatArrays
nos permite agregar al empleado actual (
$$this
) al resultado.
Usando
$filter
podemos obtener
reportees
del nivel inferior.
db.getCollection(''employees'').aggregate([
{
$match: {
empId : "10"
}
},
{
$graphLookup: {
from: "employees",
startWith: "$empId",
connectFromField: "empId",
connectToField: "managerId",
as: "reportees",
maxDepth: 4,
depthField: "level"
}
},
{
$project: {
"empId":1,
"managerId":1,
"reportees.empId":1,
"reportees.name":1,
"reportees.managerId":1,
"reportees.level":1
}
},
{
$unwind: "$reportees"
},
{
$sort: { "reportees.level": -1 }
},
{
$group: {
_id: "$_id",
empId: { $first: "$empId" },
managerId: { $first: "$managerId" },
reportees: { $push: "$reportees" }
}
},
{
$addFields: {
reportees: {
$reduce: {
input: "$reportees",
initialValue: {
currentLevel: -1,
currentLevelEmployees: [],
previousLevelEmployees: []
},
in: {
$let: {
vars: {
prev: {
$cond: [
{ $eq: [ "$$value.currentLevel", "$$this.level" ] },
"$$value.previousLevelEmployees",
"$$value.currentLevelEmployees"
]
},
current: {
$cond: [
{ $eq: [ "$$value.currentLevel", "$$this.level" ] },
"$$value.currentLevelEmployees",
[]
]
}
},
in: {
currentLevel: "$$this.level",
previousLevelEmployees: "$$prev",
currentLevelEmployees: {
$concatArrays: [
"$$current",
[
{ $mergeObjects: [
"$$this",
{ reportees: { $filter: { input: "$$prev", as: "e", cond: { $eq: [ "$$e.managerId", "$$this.empId" ] } } } }
] }
]
]
}
}
}
}
}
}
}
},
{
$addFields: { reportees: "$reportees.currentLevelEmployees" }
}
]).pretty()
La solución anterior debería funcionar para múltiples niveles. Salidas:
{
"_id" : "10",
"empId" : "10",
"managerId" : "15",
"reportees" : [
{
"empId" : "6",
"name" : "Employee6",
"managerId" : "10",
"level" : NumberLong(0),
"reportees" : [
{
"empId" : "1",
"name" : "Employee1",
"managerId" : "6",
"level" : NumberLong(1),
"reportees" : [ ]
},
{
"empId" : "2",
"name" : "Employee2",
"managerId" : "6",
"level" : NumberLong(1),
"reportees" : [ ]
}
]
},
{
"empId" : "8",
"name" : "Employee8",
"managerId" : "10",
"level" : NumberLong(0),
"reportees" : [
{
"empId" : "5",
"name" : "Employee5",
"managerId" : "8",
"level" : NumberLong(1),
"reportees" : [ ]
},
{
"empId" : "4",
"name" : "Employee4",
"managerId" : "8",
"level" : NumberLong(1),
"reportees" : [ ]
}
]
}
]
}
La documentación oficial en $ graphLookup puede proporcionar ayuda más o menos.
Solo un amable recordatorio.
Precisamente para eso sería $graphLookup (el bit transversal al menos). Para la parte de filtrado, simplemente puede usar $filter o $match dependiendo de cómo desea filtrar exactamente.
Eche un vistazo a los resultados de esta consulta:
db.employees.aggregate({
$graphLookup: {
from: "employees",
startWith: "$managerId",
connectFromField: "managerId",
connectToField: "empId",
as: "managers",
}
})
ACTUALIZACIÓN 1 según su aclaración:
Para obtener la estructura jerárquica que le gustaría obtener, puede hacer lo siguiente. Sin embargo, no lo llamaría una solución bonita, ya que requiere que defina estáticamente el número de niveles que desea bajar y también que repita las secciones, pero hace el trabajo para su ejemplo. No estoy seguro, si / con qué facilidad esto se puede extender a más niveles, tampoco. Personalmente, creo que una solución de bucle del lado del cliente sería más adecuada para este tipo de trabajo:
db.employees.aggregate([
{
$match: {
empId : "10"
}
},
// level 0
{
$graphLookup: {
from: "employees",
startWith: "$empId",
connectFromField: "empId",
connectToField: "managerId",
as: "reportees",
maxDepth: 0
}
},
{
$unwind: "$reportees" // flatten
},
{
$addFields: {
"reportees.level": 0 // add level field
}
},
// level 1
{
$graphLookup: {
from: "employees",
startWith: "$reportees.empId",
connectFromField: "reportees.empId",
connectToField: "managerId",
as: "reportees.reportees",
maxDepth: 0
}
},
{
$group: { // group previously flattened documents back together
_id: "$_id",
empId: { $first: "$empId" },
name: { $first: "$name" },
managerId: { $first: "$managerId" },
reportees: { $push: "$reportees" },
}
},
{
$addFields: {
"reportees.reportees.level": 1 // add level field
}
}
])
ACTUALIZACIÓN 2:
La siguiente consulta lo lleva a donde desea estar desde el punto de vista de la estructura de salida (omití el campo de
level
, pero debería ser fácil de agregar).
Sin embargo, no es particularmente bonito y, nuevamente, requiere que defina una profundidad organizacional máxima por adelantado.
db.employees.aggregate([
{
$match: {
empId : "10"
}
},
{
$graphLookup: { // get the relevant documents out of our universe of employees
from: "employees",
startWith: "$empId",
connectFromField: "empId",
connectToField: "managerId",
as: "reportees"
}
},
{
$project: { // add the employee we are interested in into the array of employees we''re looking at
_id: 0,
reportees: { $concatArrays: [ "$reportees", [ { _id: "$_id", empId: "$empId", name: "$name", managerId: "$managerId" } ] ] }
}
},
{
$project: {
reportees: {
$let: {
vars: {
managers: {
$filter: { // remove employees with no reportess so keep managers only
input: {
$map: {
input: "$reportees",
as: "this",
in: {
$mergeObjects: [
"$$this",
{
reportees: {
$filter: { // extract reportees from list of employees
input: "$reportees",
as: "that",
cond: {
$eq: [ "$$this._id", "$$that.managerId" ]
}
}
}
}
]
}
}
},
as: "this",
cond: { $ne: [ "$$this.reportees", [] ] }
}
}
},
in: {
$cond: [ // this is to break the processing once we have reached a top level manager
{ $eq: [ "$$managers", [] ] },
"$reportees",
"$$managers"
]
}
}
}
}
},
// second level: exactly identical to the previous stage
// third level: exactly identical to the previous stage
// basically, from here onwards you would need to repeat an exact copy of the previous stage to go one level deeper
]);