sql >> Base de Datos >  >> NoSQL >> MongoDB

Mongoose.aggregate(pipeline) enlaza múltiples colecciones usando $unwind, $lookup, $group

Esta fue complicada para alguien nuevo en MongoDb aggregate de 's . Desglosaré mi respuesta en pasos para demostrar a otros que intentan agregar una matriz con referencias a varias colecciones.

Paso 1:$coincidencia para filtrar en la colección

El $match acepta las mismas consultas que db.collection.find({}) y devuelve una matriz de resultados coincidentes en el caso a continuación, selecciono 4 registros específicos aquí


{ '$match':
     { _id:
        { '$in':
           [
              ObjectId('5f7bdb3eea134b5a5c976285'),
              ObjectId('5f7bdb3eea134b5a5c976283'),
              ObjectId('5f7bdb3eea134b5a5c976284'),
              ObjectId('5f7bdb3eea134b5a5c976289')
           ]
        }
     }
}
$resultado del partido
[ 
  { _id: ObjectId('5f7be0b37e2bdf5b19e4724d'),
    name: 'CAPTAIN_SAIL',
    classes: [ 'sail' ],
    license: 'WC-1',
    watercraftContexts:
     [ { _id: ObjectId('5f7be0b37e2bdf5b19e47241'),
         watercraftType: 'Sailboat',
         ref: 'sailboats' } ],
    __v: 0 },
  { _id: ObjectId('5f7be0b37e2bdf5b19e4724e'),
    name: 'CAPTAIN_YATCH',
    classes: [ 'yatch' ],
    license: 'WC-2',
    watercraftContexts:
     [ { _id: ObjectId('5f7be0b37e2bdf5b19e47242'),
         watercraftType: 'Yatch',
         ref: 'yatches' } ],
    __v: 0 },
  { _id: ObjectId('5f7be0b37e2bdf5b19e4724f'),
    name: 'CAPTAIN_SHIP',
    classes: [ 'ship' ],
    license: 'WC-3',
    watercraftContexts:
     [ { _id: ObjectId('5f7be0b37e2bdf5b19e47243'),
         watercraftType: 'Ship',
         ref: 'ships' } ],
    __v: 0 },
  { _id: ObjectId('5f7be0b37e2bdf5b19e47253'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     [ { _id: ObjectId('5f7be0b37e2bdf5b19e4724a'),
         watercraftType: 'Sailboat',
         ref: 'sailboats' },
       { _id: ObjectId('5f7be0b37e2bdf5b19e4724b'),
         watercraftType: 'Yatch',
         ref: 'yatches' },
       { _id: ObjectId('5f7be0b37e2bdf5b19e4724c'),
         watercraftType: 'Ship',
         ref: 'ships' } ],
    __v: 0 }
]

Paso 2:$relájate para que podamos iterar con $loopup

En este conjunto de resultados hay una matriz de objetos con { _id: <ObjectId>, watercraftType: <ModelName> } para recorrer la matriz y unir cada uno de estos objetos con su respectivo registro de colección, tenemos que dividir la matriz en registros independientes individuales. El $unwind la característica creará un nuevo conjunto de datos para la siguiente etapa agregada

  { '$unwind': '$watercraftContexts' },
$desenrollar resultado

Como puedes ver $unwind ahora crea un registro con un solo watercraftContext ahora estamos configurados para usar $lookup

[ { _id: ObjectId('5f7be2231da37c5b5915bf9b'),
    name: 'CAPTAIN_SAIL',
    classes: [ 'sail' ],
    license: 'WC-1',
    watercraftContexts:
     { _id: ObjectId('5f7be2231da37c5b5915bf8f'),
       watercraftType: 'Sailboat',
       ref: 'sailboats' },
    __v: 0 },
  { _id: ObjectId('5f7be2231da37c5b5915bf9c'),
    name: 'CAPTAIN_YATCH',
    classes: [ 'yatch' ],
    license: 'WC-2',
    watercraftContexts:
     { _id: ObjectId('5f7be2231da37c5b5915bf90'),
       watercraftType: 'Yatch',
       ref: 'yatches' },
    __v: 0 },
  { _id: ObjectId('5f7be2231da37c5b5915bf9d'),
    name: 'CAPTAIN_SHIP',
    classes: [ 'ship' ],
    license: 'WC-3',
    watercraftContexts:
     { _id: ObjectId('5f7be2231da37c5b5915bf91'),
       watercraftType: 'Ship',
       ref: 'ships' },
    __v: 0 },
  { _id: ObjectId('5f7be2231da37c5b5915bfa1'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     { _id: ObjectId('5f7be2231da37c5b5915bf98'),
       watercraftType: 'Sailboat',
       ref: 'sailboats' },
    __v: 0 },
  { _id: ObjectId('5f7be2231da37c5b5915bfa1'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     { _id: ObjectId('5f7be2231da37c5b5915bf99'),
       watercraftType: 'Yatch',
       ref: 'yatches' },
    __v: 0 },
  { _id: ObjectId('5f7be2231da37c5b5915bfa1'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     { _id: ObjectId('5f7be2231da37c5b5915bf9a'),
       watercraftType: 'Ship',
       ref: 'ships' },
    __v: 0 } ]
Paso 4 $búsqueda:une cada registro de la colección extranjera

Es importante tener en cuenta que debemos $unwind antes de llamar a $lookup para cada colección diferente que necesitamos unir. Dado que queremos unir varias colecciones, debemos almacenar el resultado en un objeto con clave de la colección para su posterior agregación.

  // Only performs $lookup on 'ships' collection
  { '$lookup':
     { from: 'ships',  // Collection Name - Note: repeat for each collection
       localField: 'watercraftContexts._id', // The field with id to link
       foreignField: '_id',  // The field on the foreign collection to match
       as: 'watercrafts.ships' // The path where to store the lookup result
     }
  }

Paso 5:repita $unwind y $busque las otras uniones

Repita los pasos anteriores para las uniones adicionales y escriba el nombre de la colección. He combinado las etapas agregadas para demostrar la repetición.

  { '$unwind': '$watercraftContexts' },
  { '$lookup':
     { from: 'yatches',
       localField: 'watercraftContexts._id',
       foreignField: '_id',
       as: 'watercrafts.yatches' } },
  { '$unwind': '$watercraftContexts' },
  { '$lookup':
     { from: 'sailboats',
       localField: 'watercraftContexts._id',
       foreignField: '_id',
       as: 'watercrafts.sailboats' } }
Resultados de los pasos 4 y 5

Si miras detenidamente, notas que uno de los Captain los registros existen 3 veces con un watercraftType diferente . $lookup solo devolverá registros que coincidan con un nombre de colección específico. Es por eso que almacenarlos en un Object clave por collectionName

[
  { _id: ObjectId('5f7be7145320a65b942bb450'),
    name: 'CAPTAIN_SAIL',
    classes: [ 'sail' ],
    license: 'WC-1',
    watercraftContexts:
     { _id: ObjectId('5f7be7145320a65b942bb444'),
       watercraftType: 'Sailboat',
       ref: 'sailboats' },
    __v: 0,
    watercrafts:
     { ships: [],
       yatches: [],
       sailboats:
        [ { _id: ObjectId('5f7be7145320a65b942bb444'),
            class: 'sail',
            name: 'Gone with the Wind',
            __v: 0 } ] } },
  { _id: ObjectId('5f7be7145320a65b942bb451'),
    name: 'CAPTAIN_YATCH',
    classes: [ 'yatch' ],
    license: 'WC-2',
    watercraftContexts:
     { _id: ObjectId('5f7be7145320a65b942bb445'),
       watercraftType: 'Yatch',
       ref: 'yatches' },
    __v: 0,
    watercrafts:
     { ships: [],
       yatches:
        [ { _id: ObjectId('5f7be7145320a65b942bb445'),
            class: 'yatch',
            name: 'Liquid Gold',
            __v: 0 } ],
       sailboats: [] } },
  { _id: ObjectId('5f7be7145320a65b942bb452'),
    name: 'CAPTAIN_SHIP',
    classes: [ 'ship' ],
    license: 'WC-3',
    watercraftContexts:
     { _id: ObjectId('5f7be7145320a65b942bb446'),
       watercraftType: 'Ship',
       ref: 'ships' },
    __v: 0,
    watercrafts:
     { ships:
        [ { _id: ObjectId('5f7be7145320a65b942bb446'),
            class: 'ship',
            name: 'Jenny',
            __v: 0 } ],
       yatches: [],
       sailboats: [] } },
  { _id: ObjectId('5f7be7145320a65b942bb456'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     { _id: ObjectId('5f7be7145320a65b942bb44d'),
       watercraftType: 'Sailboat',
       ref: 'sailboats' },
    __v: 0,
    watercrafts:
     { ships: [],
       yatches: [],
       sailboats:
        [ { _id: ObjectId('5f7be7145320a65b942bb44d'),
            class: 'sail',
            name: 'Swell Shredder',
            __v: 0 } ] } },
  { _id: ObjectId('5f7be7145320a65b942bb456'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     { _id: ObjectId('5f7be7145320a65b942bb44e'),
       watercraftType: 'Yatch',
       ref: 'yatches' },
    __v: 0,
    watercrafts:
     { ships: [],
       yatches:
        [ { _id: ObjectId('5f7be7145320a65b942bb44e'),
            class: 'yatch',
            name: 'Audrey',
            __v: 0 } ],
       sailboats: [] } },
  { _id: ObjectId('5f7be7145320a65b942bb456'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     { _id: ObjectId('5f7be7145320a65b942bb44f'),
       watercraftType: 'Ship',
       ref: 'ships' },
    __v: 0,
    watercrafts:
     { ships:
        [ { _id: ObjectId('5f7be7145320a65b942bb44f'),
            class: 'ship',
            name: 'Jenny IV',
            __v: 0 } ],
       yatches: [],
       sailboats: [] } } ]

Paso 6 $proyecto:utilice el proyecto para aplanar el mapa de objetos de las uniones

Podemos usar el proyecto para seleccionar todos los datos existentes y aplanar el Mapa de objetos de los resultados de unión en una sola matriz.

  { '$project':
     // keys with the value 'true' will be included
     { name: true,
       license: true,
       classes: true,
       _id: true,
       watercraftContexts: true,
       __v: true,
       watercrafts:            // Re-assigns value of watercrafts
        { '$setUnion':         // Accepts an array of arrays to flatten
           [
             '$watercrafts.ships',
             '$watercrafts.yatches',
             '$watercrafts.sailboats'
           ]
        }
     }
  }
$resultado del proyecto

Los resultados del $project anterior reemplazará a las watercrafts objeto con una matriz aplanada de watercrafts , pero es importante tener en cuenta que todavía hay registros duplicados de Captain donde coinciden muchas búsquedas diferentes. Los volveremos a unir en el siguiente paso.

[ { _id: ObjectId('5f7bea8d79dfe25bf3cb9695'),
    name: 'CAPTAIN_SAIL',
    classes: [ 'sail' ],
    license: 'WC-1',
    watercraftContexts:
     { _id: ObjectId('5f7bea8d79dfe25bf3cb9689'),
       watercraftType: 'Sailboat',
       ref: 'sailboats' },
    __v: 0,
    watercrafts:
     [ { _id: ObjectId('5f7bea8d79dfe25bf3cb9689'),
         class: 'sail',
         name: 'Gone with the Wind',
         __v: 0 } ] },
  { _id: ObjectId('5f7bea8d79dfe25bf3cb9696'),
    name: 'CAPTAIN_YATCH',
    classes: [ 'yatch' ],
    license: 'WC-2',
    watercraftContexts:
     { _id: ObjectId('5f7bea8d79dfe25bf3cb968a'),
       watercraftType: 'Yatch',
       ref: 'yatches' },
    __v: 0,
    watercrafts:
     [ { _id: ObjectId('5f7bea8d79dfe25bf3cb968a'),
         class: 'yatch',
         name: 'Liquid Gold',
         __v: 0 } ] },
  { _id: ObjectId('5f7bea8d79dfe25bf3cb9697'),
    name: 'CAPTAIN_SHIP',
    classes: [ 'ship' ],
    license: 'WC-3',
    watercraftContexts:
     { _id: ObjectId('5f7bea8d79dfe25bf3cb968b'),
       watercraftType: 'Ship',
       ref: 'ships' },
    __v: 0,
    watercrafts:
     [ { _id: ObjectId('5f7bea8d79dfe25bf3cb968b'),
         class: 'ship',
         name: 'Jenny',
         __v: 0 } ] },
  { _id: ObjectId('5f7bea8d79dfe25bf3cb969b'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     { _id: ObjectId('5f7bea8d79dfe25bf3cb9692'),
       watercraftType: 'Sailboat',
       ref: 'sailboats' },
    __v: 0,
    watercrafts:
     [ { _id: ObjectId('5f7bea8d79dfe25bf3cb9692'),
         class: 'sail',
         name: 'Swell Shredder',
         __v: 0 } ] },
  { _id: ObjectId('5f7bea8d79dfe25bf3cb969b'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     { _id: ObjectId('5f7bea8d79dfe25bf3cb9693'),
       watercraftType: 'Yatch',
       ref: 'yatches' },
    __v: 0,
    watercrafts:
     [ { _id: ObjectId('5f7bea8d79dfe25bf3cb9693'),
         class: 'yatch',
         name: 'Audrey',
         __v: 0 } ] },
  { _id: ObjectId('5f7bea8d79dfe25bf3cb969b'),
    name: 'CAPTAIN_SAIL_YATCH_SHIP',
    classes: [ 'sail', 'yatch', 'ship' ],
    license: 'WC-7',
    watercraftContexts:
     { _id: ObjectId('5f7bea8d79dfe25bf3cb9694'),
       watercraftType: 'Ship',
       ref: 'ships' },
    __v: 0,
    watercrafts:
     [ { _id: ObjectId('5f7bea8d79dfe25bf3cb9694'),
         class: 'ship',
         name: 'Jenny IV',
         __v: 0 } ] } ]

Paso 7 $relajarse y $agruparse

Nosotros $unwind para que ahora podamos agrupar todas las watercrafts perteneciente al mismo Captain . También tenemos que usar $mergeObjects para almacenar temporalmente los datos adicionales del Captain colección bajo una nueva variable temporal para prepararse para las etapas finales.

  { '$unwind': '$watercrafts' },
  { '$group':
     { _id: '$_id',
       data:
        { '$mergeObjects':
           { name: '$name',
             license: '$license',
             classes: '$classes',
             watercraftContexts: '$watercraftContexts',
             __v: '$__v' } },
       watercrafts: { '$push': '$watercrafts' } } }
$unwind y $group Resultado

Ahora realmente estamos llegando a alguna parte. Hemos reducido nuestra transformación a nuestros 4 Captain iniciales s y aplanamos nuestras uniones en una sola matriz.

[ { _id: ObjectId('5f7bed5e271dd95c306c25a4'),
    data:
     { name: 'CAPTAIN_SHIP',
       license: 'WC-3',
       classes: [ 'ship' ],
       watercraftContexts:
        { _id: ObjectId('5f7bed5e271dd95c306c2598'),
          watercraftType: 'Ship',
          ref: 'ships' },
       __v: 0 },
    watercrafts:
     [ { _id: ObjectId('5f7bed5e271dd95c306c2598'),
         class: 'ship',
         name: 'Jenny',
         __v: 0 } ] },
  { _id: ObjectId('5f7bed5e271dd95c306c25a8'),
    data:
     { name: 'CAPTAIN_SAIL_YATCH_SHIP',
       license: 'WC-7',
       classes: [ 'sail', 'yatch', 'ship' ],
       watercraftContexts:
        { _id: ObjectId('5f7bed5e271dd95c306c25a1'),
          watercraftType: 'Ship',
          ref: 'ships' },
       __v: 0 },
    watercrafts:
     [ { _id: ObjectId('5f7bed5e271dd95c306c259f'),
         class: 'sail',
         name: 'Swell Shredder',
         __v: 0 },
       { _id: ObjectId('5f7bed5e271dd95c306c25a0'),
         class: 'yatch',
         name: 'Audrey',
         __v: 0 },
       { _id: ObjectId('5f7bed5e271dd95c306c25a1'),
         class: 'ship',
         name: 'Jenny IV',
         __v: 0 } ] },
  { _id: ObjectId('5f7bed5e271dd95c306c25a2'),
    data:
     { name: 'CAPTAIN_SAIL',
       license: 'WC-1',
       classes: [ 'sail' ],
       watercraftContexts:
        { _id: Object('5f7bed5e271dd95c306c2596'),
          watercraftType: 'Sailboat',
          ref: 'sailboats' },
       __v: 0 },
    watercrafts:
     [ { _id: ObjectId('5f7bed5e271dd95c306c2596'),
         class: 'sail',
         name: 'Gone with the Wind',
         __v: 0 } ] },
  { _id: ObjectId('5f7bed5e271dd95c306c25a3'),
    data:
     { name: 'CAPTAIN_YATCH',
       license: 'WC-2',
       classes: [ 'yatch' ],
       watercraftContexts:
        { _id: ObjectId('5f7bed5e271dd95c306c2597'),
          watercraftType: 'Yatch',
          ref: 'yatches' },
       __v: 0 },
    watercrafts:
     [ { _id: ObjectId('5f7bed5e271dd95c306c2597'),
         class: 'yatch',
         name: 'Liquid Gold',
         __v: 0 } ] } ]

Paso 8 $replaceRoot y $proyecto

Todo lo que nos queda es fusionar nuestros data en la raíz de cada registro y elimine la variable temporal data

  // Merges 'data' into the root of each record
  { '$replaceRoot': { newRoot: { '$mergeObjects': [ '$data', '$$ROOT' ] } } },
  // Use $project to remove data (include only the fields we want)
  { '$project':
     { name: true,
       license: true,
       classes: true,
       _id: true,
       watercraftContexts: true,
       __v: true,
       watercrafts: true } 
  }
$replaceRoot y $resultado del proyecto

Ahora tenemos el resultado que nos propusimos... Un Captain con una variedad de tipos mixtos asociados watercrafts

[ 
  { name: 'CAPTAIN_SAIL_YATCH_SHIP',
    license: 'WC-7',
    classes: [ 'sail', 'yatch', 'ship' ],
    watercraftContexts:
     { _id: ObjectId('5f7bf3b3680b375ca1755ea6'),
       watercraftType: 'Ship',
       ref: 'ships' },
    __v: 0,
    _id: ObjectId('5f7bf3b3680b375ca1755ead'),
    watercrafts:
     [ { _id: ObjectId('5f7bf3b3680b375ca1755ea4'),
         class: 'sail',
         name: 'Swell Shredder',
         __v: 0 },
       { _id: ObjectId('5f7bf3b3680b375ca1755ea5'),
         class: 'yatch',
         name: 'Audrey',
         __v: 0 },
       { _id: ObjectId('5f7bf3b3680b375ca1755ea6'),
         class: 'ship',
         name: 'Jenny IV',
         __v: 0 } ] },
  { name: 'CAPTAIN_SAIL',
    license: 'WC-1',
    classes: [ 'sail' ],
    watercraftContexts:
     { _id: ObjectId('5f7bf3b3680b375ca1755e9b'),
       watercraftType: 'Sailboat',
       ref: 'sailboats' },
    __v: 0,
    _id: ObjectId('5f7bf3b3680b375ca1755ea7'),
    watercrafts:
     [ { _id: ObjectId('5f7bf3b3680b375ca1755e9b'),
         class: 'sail',
         name: 'Gone with the Wind',
         __v: 0 } ] },
  { name: 'CAPTAIN_YATCH',
    license: 'WC-2',
    classes: [ 'yatch' ],
    watercraftContexts:
     { _id: ObjectId('5f7bf3b3680b375ca1755e9c'),
       watercraftType: 'Yatch',
       ref: 'yatches' },
    __v: 0,
    _id: ObjectId('5f7bf3b3680b375ca1755ea8'),
    watercrafts:
     [ { _id: ObjectId('5f7bf3b3680b375ca1755e9c'),
         class: 'yatch',
         name: 'Liquid Gold',
         __v: 0 } ] },
  { name: 'CAPTAIN_SHIP',
    license: 'WC-3',
    classes: [ 'ship' ],
    watercraftContexts:
     { _id: ObjectId('5f7bf3b3680b375ca1755e9d'),
       watercraftType: 'Ship',
       ref: 'ships' },
    __v: 0,
    _id: ObjectId('5f7bf3b3680b375ca1755ea9'),
    watercrafts:
     [ { _id: ObjectId('5f7bf3b3680b375ca1755e9d'),
         class: 'ship',
         name: 'Jenny',
         __v: 0 } ] } ]

Y ahí lo tienes... sólo tomó 2 días para resolver esto. Espero que le ahorre algo de tiempo si está intentando una asociación agregada similar. ¡Feliz codificación!

Canalización final

[ 
  { '$match':
     { _id:
        { '$in':
           [ ObjectId('5f7bf3b3680b375ca1755ea9'),
             ObjectId('5f7bf3b3680b375ca1755ea7'),
             ObjectId('5f7bf3b3680b375ca1755ea8'),
             ObjectId('5f7bf3b3680b375ca1755ead')
           ]
        }
     }
  },
  { '$unwind': '$watercraftContexts' },
  { '$lookup':
     { from: 'ships',
       localField: 'watercraftContexts._id',
       foreignField: '_id',
       as: 'watercrafts.ships' } },
  { '$unwind': '$watercraftContexts' },
  { '$lookup':
     { from: 'yatches',
       localField: 'watercraftContexts._id',
       foreignField: '_id',
       as: 'watercrafts.yatches' } },
  { '$unwind': '$watercraftContexts' },
  { '$lookup':
     { from: 'sailboats',
       localField: 'watercraftContexts._id',
       foreignField: '_id',
       as: 'watercrafts.sailboats' } },
  { '$project':
     { name: true,
       license: true,
       classes: true,
       _id: true,
       watercraftContexts: true,
       __v: true,
       watercrafts:
        { '$setUnion':
           [ '$watercrafts.ships',
             '$watercrafts.yatches',
             '$watercrafts.sailboats' ] } } },
  { '$unwind': '$watercrafts' },
  { '$group':
     { _id: '$_id',
       data:
        { '$mergeObjects':
           { name: '$name',
             license: '$license',
             classes: '$classes',
             watercraftContexts: '$watercraftContexts',
             __v: '$__v' } },
       watercrafts: { '$push': '$watercrafts' } } },
  { '$replaceRoot': { newRoot: { '$mergeObjects': [ '$data', '$$ROOT' ] } } },
  { '$project':
     { name: true,
       license: true,
       classes: true,
       _id: true,
       watercraftContexts: true,
       __v: true,
       watercrafts: true } }
]