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

Rellenar objeto en una matriz

Lo que básicamente te perdiste aquí es la "ruta" al campo que deseas populate() es en realidad 'portfolio.formatType' y no solo 'portfolio' como ha escrito. Sin embargo, debido a ese error y la estructura, es posible que tenga algunos conceptos erróneos generales.

Rellenar corrección

La corrección básica simplemente necesita la ruta correcta y no necesita el model argumento ya que esto ya está implícito en el esquema:

User.findById(req.params.id).populate('portfolio.formatType');

Sin embargo, generalmente no es una buena idea "mezclar" datos "incrustados" y datos "referenciados" dentro de matrices, y realmente debería estar incrustando todo o simplemente haciendo referencia a todo. También es un poco un "antipatrón" en general para mantener una serie de referencias en el documento si su intención es hacer referencia, ya que su razón debería ser no hacer que el documento crezca más allá del límite de BSON de 16 MB. Y cuando sus datos nunca alcancen ese límite, generalmente es mejor "incrustarlo completamente". Esa es realmente una discusión más amplia, pero es algo que debe tener en cuenta.

El siguiente punto general aquí es populate() en sí mismo es algo "viejo", y realmente no es lo "mágico" que la mayoría de los nuevos usuarios perciben. Para ser claros populate() NO ES UN ÚNICO , y todo lo que hace es ejecutar otra consulta al servidor para devolver los elementos "relacionados", luego fusionar ese contenido con los documentos devueltos por la consulta anterior.

Alternativa $búsqueda

Si está buscando "uniones", entonces probablemente quiera "incrustar" como se mencionó anteriormente. Este es realmente el "Modo MongoDB" de tratar con "relaciones" pero manteniendo todos los datos "relacionados" juntos en un documento. El otro medio de una "unión" donde los datos están en colecciones separadas es a través de $lookup operador en versiones modernas.

Esto se vuelve un poco más complejo debido a su forma de matriz de contenido "mixto", pero generalmente se puede representar como:

// Aggregation pipeline don't "autocast" from schema
const { Types: { ObjectId } } = require("mongoose");

User.aggregate([
  { "$match": { _id: ObjectId(req.params.id)  } },
  { "$lookup": {
    "from": FormatType.collection.name,
    "localField": "portfolio.formatType",
    "foreignField": "_id",
    "as": "formats"
  }},
  { "$project": {
    "name": 1,
    "portfolio": {
      "$map": {
        "input": "$portfolio",
        "in": {
          "name": "$$this.name",
          "formatType": {
            "$arrayElemAt": [
              "$formats",
              { "$indexOfArray": [ "$formats._id", "$$this.formatType" ] }
            ]
          }
        }
      }
    }
  }}
]);

O con la forma más expresiva de $lookup desde MongoDB 3.6:

User.aggregate([
  { "$match": { _id: ObjectId(req.params.id)  } },
  { "$lookup": {
    "from": FormatType.collection.name,
    "let": { "portfolio": "$portfolio" },
    "as": "portfolio",
    "pipeline": [
      { "$match": {
        "$expr": {
          "$in": [ "$_id", "$$portfolio.formatType" ]
        }
      }},
      { "$project": {
        "_id": {
          "$arrayElemAt": [
            "$$portfolio._id",
            { "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
          ]
        },
        "name": {
          "$arrayElemAt": [
            "$$portfolio.name",
            { "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
          ]
        },
        "formatType": "$$ROOT",
      }}
    ]
  }}
]);

Los dos enfoques funcionan de manera ligeramente diferente, pero ambos funcionan esencialmente con el concepto de devolver las entradas "relacionadas" coincidentes y luego "volver a mapearlas" en el contenido de la matriz existente para fusionarlas con el "name" propiedades "incrustadas" dentro de la matriz. Esa es en realidad la principal complicación que, de lo contrario, es un método de recuperación bastante sencillo.

Es más o menos el mismo proceso que populate() en realidad lo hace en el "cliente" pero se ejecuta en el "servidor". Entonces, las comparaciones usan $indexOfArray operador para encontrar dónde se encuentra el ObjectId coincidente los valores son y luego devuelven una propiedad de la matriz en ese "índice" coincidente a través de $arrayElemAt operación.

La única diferencia es que en la versión compatible con MongoDB 3.6, hacemos esa "sustitución" dentro del contenido "extranjero" "antes" los resultados combinados se devuelven al padre. En versiones anteriores, devolvíamos la matriz externa coincidente completa y luego "uníamos" las dos para formar una matriz "fusionada" singular usando $map .

Si bien estos pueden parecer inicialmente "más complejos", la gran ventaja aquí es que constituyen una "solicitud única" al servidor con una "respuesta única" y no emitir ni recibir solicitudes "múltiples" como populate() lo hace. De hecho, esto ahorra una gran cantidad de sobrecarga en el tráfico de red y aumenta considerablemente el tiempo de respuesta.

Además, se trata de "uniones reales", por lo que hay mucho más que puede hacer que no se puede lograr con "consultas múltiples". Por ejemplo, puede "ordenar" los resultados en "unirse" y solo devolver los mejores resultados, mientras usa populate() necesita atraer a "todos los padres" antes de que pueda buscar qué "hijos" devolver como resultado. Lo mismo ocurre con las condiciones de "filtrado" en el niño "unirse".

Hay más detalles sobre esto en Consultar después de poblar en Mongoose sobre las limitaciones generales y lo que realmente puede hacer prácticamente para "automatizar" la generación de declaraciones de canalización de agregación "complejas" cuando sea necesario.

Demostración

Otro problema común al hacer estas "uniones" y comprender el esquema de referencia en general es que las personas a menudo entienden mal los conceptos sobre dónde y cuándo almacenar las referencias y cómo funciona todo. Por lo tanto, los siguientes listados sirven como demostración tanto del almacenamiento como de la recuperación de dichos datos.

En una implementación nativa de Promises para versiones anteriores de NodeJS:

const { Schema } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/usertest';

mongoose.Promise = global.Promise;
mongoose.set('debug',true);

const formatTypeSchema = new Schema({
  name: String
});

const portfolioSchema = new Schema({
  name: String,
  formatType: { type: Schema.Types.ObjectId, ref: 'FormatType' }
});

const userSchema = new Schema({
  name: String,
  portfolio: [portfolioSchema]
});

const FormatType = mongoose.model('FormatType', formatTypeSchema);
const User = mongoose.model('User', userSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(function() {

  mongoose.connect(uri).then(conn => {

    let db = conn.connections[0].db;

    return db.command({ buildInfo: 1 }).then(({ version }) => {
      version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);

      return Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()))
        .then(() => FormatType.insertMany(
          [ 'A', 'B', 'C' ].map(name => ({ name }))
        )
        .then(([A, B, C]) => User.insertMany(
          [
            {
              name: 'User 1',
              portfolio: [
                { name: 'Port A', formatType: A },
                { name: 'Port B', formatType: B }
              ]
            },
            {
              name: 'User 2',
              portfolio: [
                { name: 'Port C', formatType: C }
              ]
            }
          ]
        ))
        .then(() => User.find())
        .then(users => log({ users }))
        .then(() => User.findOne({ name: 'User 1' })
          .populate('portfolio.formatType')
        )
        .then(user1 => log({ user1 }))
        .then(() => User.aggregate([
          { "$match": { "name": "User 2" } },
          { "$lookup": {
            "from": FormatType.collection.name,
            "localField": "portfolio.formatType",
            "foreignField": "_id",
            "as": "formats"
          }},
          { "$project": {
            "name": 1,
            "portfolio": {
              "$map": {
                "input": "$portfolio",
                "in": {
                  "name": "$$this.name",
                  "formatType": {
                    "$arrayElemAt": [
                      "$formats",
                      { "$indexOfArray": [ "$formats._id", "$$this.formatType" ] }
                    ]
                  }
                }
              }
            }
          }}
        ]))
        .then(user2 => log({ user2 }))
        .then(() =>
          ( version >= 3.6 ) ?
            User.aggregate([
              { "$lookup": {
                "from": FormatType.collection.name,
                "let": { "portfolio": "$portfolio" },
                "as": "portfolio",
                "pipeline": [
                  { "$match": {
                    "$expr": {
                      "$in": [ "$_id", "$$portfolio.formatType" ]
                    }
                  }},
                  { "$project": {
                    "_id": {
                      "$arrayElemAt": [
                        "$$portfolio._id",
                        { "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
                      ]
                    },
                    "name": {
                      "$arrayElemAt": [
                        "$$portfolio.name",
                        { "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
                      ]
                    },
                    "formatType": "$$ROOT",
                  }}
                ]
              }}
            ]).then(users => log({ users })) : ''
        );
  })
  .catch(e => console.error(e))
  .then(() => mongoose.disconnect());

})()

Y con async/await sintaxis para versiones más recientes de NodeJS, incluida la serie LTS v.8.x actual:

const { Schema } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/usertest';

mongoose.Promise = global.Promise;
mongoose.set('debug',true);

const formatTypeSchema = new Schema({
  name: String
});

const portfolioSchema = new Schema({
  name: String,
  formatType: { type: Schema.Types.ObjectId, ref: 'FormatType' }
});

const userSchema = new Schema({
  name: String,
  portfolio: [portfolioSchema]
});

const FormatType = mongoose.model('FormatType', formatTypeSchema);
const User = mongoose.model('User', userSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);
    let db = conn.connections[0].db;

    let { version } = await db.command({ buildInfo: 1 });
    version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);
    log(version);

    // Clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Insert some things
    let [ A, B, C ] = await FormatType.insertMany(
      [ 'A', 'B', 'C' ].map(name => ({ name }))
    );

    await User.insertMany(
      [
        {
          name: 'User 1',
          portfolio: [
            { name: 'Port A', formatType: A },
            { name: 'Port B', formatType: B }
          ]
        },
        {
          name: 'User 2',
          portfolio: [
            { name: 'Port C', formatType: C }
          ]
        }
      ]
    );


    // Show plain users
    let users = await User.find();
    log({ users });

    // Get user with populate

    let user1 = await User.findOne({ name: 'User 1' })
      .populate('portfolio.formatType');

    log({ user1 });

    // Get user with $lookup
    let user2 = await User.aggregate([
      { "$match": { "name": "User 2" } },
      { "$lookup": {
        "from": FormatType.collection.name,
        "localField": "portfolio.formatType",
        "foreignField": "_id",
        "as": "formats"
      }},
      { "$project": {
        "name": 1,
        "portfolio": {
          "$map": {
            "input": "$portfolio",
            "in": {
              "name": "$$this.name",
              "formatType": {
                "$arrayElemAt": [
                  "$formats",
                  { "$indexOfArray": [ "$formats._id", "$$this.formatType" ] }
                ]
              }
            }
          }
        }
      }}
    ]);

    log({ user2 });

    // Expressive $lookup
    if ( version >= 3.6 ) {
      let users = await User.aggregate([
        { "$lookup": {
          "from": FormatType.collection.name,
          "let": { "portfolio": "$portfolio" },
          "as": "portfolio",
          "pipeline": [
            { "$match": {
              "$expr": {
                "$in": [ "$_id", "$$portfolio.formatType" ]
              }
            }},
            { "$project": {
              "_id": {
                "$arrayElemAt": [
                  "$$portfolio._id",
                  { "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
                ]
              },
              "name": {
                "$arrayElemAt": [
                  "$$portfolio.name",
                  { "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
                ]
              },
              "formatType": "$$ROOT",
            }}
          ]
        }}
      ]);
      log({ users })
    }

    mongoose.disconnect();    
  } catch(e) {
    console.log(e)
  } finally {
    process.exit()
  }

})()

La última lista se comenta en cada etapa para explicar las partes, y al menos puede ver por comparación cómo ambas formas de sintaxis se relacionan entre sí.

Tenga en cuenta que el "expresivo" $lookup El ejemplo solo se ejecuta donde el servidor MongoDB conectado realmente admite la sintaxis.

Y el "resultado" para aquellos que no se molestan en ejecutar el código ellos mismos:

Mongoose: formattypes.remove({}, {})
Mongoose: users.remove({}, {})
Mongoose: formattypes.insertMany([ { _id: 5b1601d8be9bf225554783f5, name: 'A', __v: 0 }, { _id: 5b1601d8be9bf225554783f6, name: 'B', __v: 0 }, { _id: 5b1601d8be9bf225554783f7, name: 'C', __v: 0 } ], {})
Mongoose: users.insertMany([ { _id: 5b1601d8be9bf225554783f8, name: 'User 1', portfolio: [ { _id: 5b1601d8be9bf225554783fa, name: 'Port A', formatType: 5b1601d8be9bf225554783f5 }, { _id: 5b1601d8be9bf225554783f9, name: 'Port B', formatType: 5b1601d8be9bf225554783f6 } ], __v: 0 }, { _id: 5b1601d8be9bf225554783fb, name: 'User 2', portfolio: [ { _id: 5b1601d8be9bf225554783fc, name: 'Port C', formatType: 5b1601d8be9bf225554783f7 } ], __v: 0 } ], {})
Mongoose: users.find({}, { fields: {} })
{
  "users": [
    {
      "_id": "5b1601d8be9bf225554783f8",
      "name": "User 1",
      "portfolio": [
        {
          "_id": "5b1601d8be9bf225554783fa",
          "name": "Port A",
          "formatType": "5b1601d8be9bf225554783f5"
        },
        {
          "_id": "5b1601d8be9bf225554783f9",
          "name": "Port B",
          "formatType": "5b1601d8be9bf225554783f6"
        }
      ],
      "__v": 0
    },
    {
      "_id": "5b1601d8be9bf225554783fb",
      "name": "User 2",
      "portfolio": [
        {
          "_id": "5b1601d8be9bf225554783fc",
          "name": "Port C",
          "formatType": "5b1601d8be9bf225554783f7"
        }
      ],
      "__v": 0
    }
  ]
}
Mongoose: users.findOne({ name: 'User 1' }, { fields: {} })
Mongoose: formattypes.find({ _id: { '$in': [ ObjectId("5b1601d8be9bf225554783f5"), ObjectId("5b1601d8be9bf225554783f6") ] } }, { fields: {} })
{
  "user1": {
    "_id": "5b1601d8be9bf225554783f8",
    "name": "User 1",
    "portfolio": [
      {
        "_id": "5b1601d8be9bf225554783fa",
        "name": "Port A",
        "formatType": {
          "_id": "5b1601d8be9bf225554783f5",
          "name": "A",
          "__v": 0
        }
      },
      {
        "_id": "5b1601d8be9bf225554783f9",
        "name": "Port B",
        "formatType": {
          "_id": "5b1601d8be9bf225554783f6",
          "name": "B",
          "__v": 0
        }
      }
    ],
    "__v": 0
  }
}
Mongoose: users.aggregate([ { '$match': { name: 'User 2' } }, { '$lookup': { from: 'formattypes', localField: 'portfolio.formatType', foreignField: '_id', as: 'formats' } }, { '$project': { name: 1, portfolio: { '$map': { input: '$portfolio', in: { name: '$$this.name', formatType: { '$arrayElemAt': [ '$formats', { '$indexOfArray': [ '$formats._id', '$$this.formatType' ] } ] } } } } } } ], {})
{
  "user2": [
    {
      "_id": "5b1601d8be9bf225554783fb",
      "name": "User 2",
      "portfolio": [
        {
          "name": "Port C",
          "formatType": {
            "_id": "5b1601d8be9bf225554783f7",
            "name": "C",
            "__v": 0
          }
        }
      ]
    }
  ]
}
Mongoose: users.aggregate([ { '$lookup': { from: 'formattypes', let: { portfolio: '$portfolio' }, as: 'portfolio', pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$portfolio.formatType' ] } } }, { '$project': { _id: { '$arrayElemAt': [ '$$portfolio._id', { '$indexOfArray': [ '$$portfolio.formatType', '$_id' ] } ] }, name: { '$arrayElemAt': [ '$$portfolio.name', { '$indexOfArray': [ '$$portfolio.formatType', '$_id' ] } ] }, formatType: '$$ROOT' } } ] } } ], {})
{
  "users": [
    {
      "_id": "5b1601d8be9bf225554783f8",
      "name": "User 1",
      "portfolio": [
        {
          "_id": "5b1601d8be9bf225554783fa",
          "name": "Port A",
          "formatType": {
            "_id": "5b1601d8be9bf225554783f5",
            "name": "A",
            "__v": 0
          }
        },
        {
          "_id": "5b1601d8be9bf225554783f9",
          "name": "Port B",
          "formatType": {
            "_id": "5b1601d8be9bf225554783f6",
            "name": "B",
            "__v": 0
          }
        }
      ],
      "__v": 0
    },
    {
      "_id": "5b1601d8be9bf225554783fb",
      "name": "User 2",
      "portfolio": [
        {
          "_id": "5b1601d8be9bf225554783fc",
          "name": "Port C",
          "formatType": {
            "_id": "5b1601d8be9bf225554783f7",
            "name": "C",
            "__v": 0
          }
        }
      ],
      "__v": 0
    }
  ]
}