Переназначить массив ObjectIds в каждом элементе вложенного массива - PullRequest
0 голосов
/ 06 июня 2018

У меня есть один документ, сгенерированный пользователем tags, а также entries, в котором есть массив идентификаторов тегов для каждой записи (или, возможно, ни одного):

// Doc (with redacted items I would like to project too)
{
    "_id": ObjectId("5ae5afc93e1d0d2965a4f2d7"),
    "entries" : [
        {
            "_id" : ObjectId("5b159ebb0ed51064925dff24"),
            // Desired:
            // tags: {[
            //   "_id" : ObjectId("5b142ab7e419614016b8992d"),
            //   "name" : "Shit",
            //   "color" : "#95a5a6"
            // ]}
            "tags" : [
                ObjectId("5b142ab7e419614016b8992d")
            ]
        },
    ],
    "tags" : [
        {
            "_id" : ObjectId("5b142608e419614016b89925"),
            "name" : "Outdated",
            "color" : "#3498db"
        },
        {
            "_id" : ObjectId("5b142ab7e419614016b8992d"),
            "name" : "Shit",
            "color" : "#95a5a6"
        },
    ],
}

Как я могу "заполнитьвверх "массив тегов для каждой записи с соответствующим значением в массиве тегов?Я пробовал $ lookup и aggregate, но это было слишком сложно, чтобы получить права.

1 Ответ

0 голосов
/ 09 июня 2018

Судя по вашим фактическим данным, нет необходимости populate() или $lookup здесь, поскольку данные, к которым вы хотите "присоединиться", находятся не только в одной коллекции, но на самом деле находятся втот же документ.Вместо этого вы хотите $map или даже Array.map(), чтобы просто взять значения в одном массиве документа и объединить их с другим.

Агрегировать$ map transform

Базовый случай того, что вам нужно сделать здесь, это $map для преобразования каждого массива в выводе.Это "entries", и в каждой «записи» преобразуется "tags" путем сопоставления значений со значениями в массиве "tags" родительского документа:

Project.aggregate([
  { "$project": {
    "entries": {
      "$map": {
        "input": "$entries",
        "as": "e",
        "in": {
          "someField": "$$e.someField",
          "otherField": "$$e.otherField",
          "tags": {
            "$map": {
              "input": "$$e.tags",
              "as": "t",
              "in": {
                "$arrayElemAt": [
                  "$tags",
                  { "$indexOfArray": [ "$tags._id", "$$t" ] }
                ]
              }
            }
          }
        }
      }
    }
  }}
])

Обратите внимание на "someField" и "otherField" в качестве заполнителей для полей, которые «могут» присутствовать на этом уровне в каждом «элементе» документа массива.Единственный улов с $map заключается в том, что в аргументе "in" указывается вывод only , который вы фактически получаете, поэтому необходимоЯвно назовите каждое потенциальное поле, которое будет в вашей структуре «переменных ключей», включая "tags".

Счетчик этого в современных выпусках, поскольку MongoDB 3.6 должен использовать $mergeObjects*Вместо этого 1041 *, который допускает «слияние» «переназначенного» внутреннего массива "tags" в «запись» каждого элемента массива:

Project.aggregate([
  { "$project": {
    "entries": {
      "$map": {
        "input": "$entries",
        "as": "e",
        "in": {
          "$mergeObjects": [
            "$$e",
            { "tags": {
              "$map": {
                "input": "$$e.tags",
                "as": "t",
                "in": {
                  "$arrayElemAt": [
                    "$tags",
                    { "$indexOfArray": [ "$tags._id", "$$t" ] }
                  ]
                }
              }
            }}
          ]
        }
      }
    }
  }}
])

Что касается фактического $map для «внутреннего» массива "tags", здесь вы можете использовать оператор $indexOfArray для сравнения с полем «корневого уровня» "tags" в зависимости от того, гдеСвойство _id соответствует значению текущей записи этого «внутреннего» массива.После возврата этого «индекса» оператор $arrayElemAt затем «извлекает» фактическую запись массива из соответствующей позиции «индекса» и трансплантирует текущую запись массива в $map с этим элементом.

Единственная забота здесь в том случае, когда два массива фактически не имеют совпадающих записей по какой-то причине.Если вы уже позаботились об этом, то код здесь в порядке.Если есть несоответствие, вам вместо этого может потребоваться $filter, чтобы соответствовать элементам, и вместо этого взять $arrayElemAt по индексу 0:

    "in": {
      "$arrayElemAt": [
        { "$filter": {
          "input": "$tags",
          "cond": { "$eq": [ "$$this._id", "$$t" ] }
        }},
        0
      ]
    }

Причина в том, что выполнение этого допускает null там, где нет совпадений, но $indexOfArray вернет -1, а при использовании $arrayElemAt возвращается "последний "элемент массива.И «последний» элемент в этом сценарии, конечно, не является «совпадающим» результатом, поскольку не было совпадений.

Преобразование на стороне клиента

Так что с точки зрения «там, где вы находитесь», только"возвращая содержимое "entries"" повторно сопоставленным "и отбрасывая "tags" из корня документа, процесс агрегирования, где это возможно, является лучшим вариантом, поскольку сервер возвращает только те элементы, которые вам действительно нужны.

Если вы не можете этого сделать, или по-другому все равно, будет ли возвращен существующий элемент "tags", то преобразование агрегирования здесь вообще не нужно.На самом деле «серверу» не нужно ничего делать, и, вероятно, «не должен» , учитывая, что все данные уже есть в документе, а «дополнительные» преобразования просто увеличивают размер документа.

Таким образом, все это на самом деле возможно сделать с результатом, однажды возвращенным клиенту, и для простого преобразования документа, точно так же, как было продемонстрировано в приведенных выше примерах конвейера агрегации, единственный код, который вам действительно нужен:

let results = await Project.find().lean();

results = results.map(({ entries, tags, ...r }) =>
  ({
    ...r,
    entries: entries.map(({ tags: etags, ...e }) =>
      ({
        ...e,
        tags: etags.map( tid => tags.find(t => t._id.equals(tid)) )
      })
    ),
    // tags
  })
);

Это дает точно такие же результаты и даже при желании может оставить tags, удалив комментарий.Это даже «абсолютно одинаковый процесс» использования Array.map() в каждом массиве для преобразования каждого из них.

Синтаксис "слияния" намного проще с современными JavaScript операциями распространения объектов , и в целом язык гораздо менее лаконичен.Вы используете Array.find() для того, чтобы «искать» совпадающее содержимое двух массивов для tags, и единственное, что нужно знать, - это метод ObjectId.equals(),который необходим для фактического сравнения этих двух значений и встроенных в возвращаемые типы в любом случае.

Конечно, поскольку вы «преобразовываете» документы, чтобы сделать это возможным, вы используете lean() в любой операции mongoose, возвращающей результаты для манипуляции, поэтому возвращаемые данные на самом деле представляют собой простые объекты JavaScript, а не типы Mongoose Document, связанные со схемой, что является возвращением по умолчанию.

Заключение и демонстрация

Общий урок здесь заключается в том, что если вы хотите «уменьшить данные» в возвращаемом ответе, то метод aggregate() для вас.Однако, если вы решите, что вам все равно нужны «целые» данные документа и просто хотите «дополнить» эти другие записи массива в ответе, то просто верните данные обратно «клиенту» и вместо этого преобразуйте их там.В идеале, насколько это возможно, «вперед», учитывая, что «дополнения» просто добавляют вес к ответу полезной нагрузки в этом случае.

Полный демонстрационный листинг будет:

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

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

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

const tagSchema = new Schema({
  name: String,
  color: String
});

const projectSchema = new Schema({
  entries: [],
  tags: [tagSchema]
});

const Project = mongoose.model('Project', projectSchema);

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]);

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

    await Project.insertMany(data);

    let pipeline = [
      { "$project": {
        "entries": {
          "$map": {
            "input": "$entries",
            "as": "e",
            "in": {
              "someField": "$$e.someField",
              "otherField": "$$e.otherField",
              "tags": {
                "$map": {
                  "input": "$$e.tags",
                  "as": "t",
                  "in": {
                    "$arrayElemAt": [
                      "$tags",
                      { "$indexOfArray": [ "$tags._id", "$$t" ] }
                    ]
                  }
                }
              }
            }
          }
        }
      }}
    ];

    let other = [
      {
        ...(({ $project: { entries: { $map: { input, as, ...o } } } }) =>
          ({
            $project: {
              entries: {
                $map: {
                  input,
                  as,
                  in: {
                    "$mergeObjects": [ "$$e", { tags: o.in.tags } ]
                  }
                }
              }
            }
          })
        )(pipeline[0])
      }
    ];

    let tests = [
      { name: 'Standard $project $map', pipeline },
      ...(version >= 3.6) ?
        [{ name: 'With $mergeObjects', pipeline: other }] : []
    ];

    for ( let { name, pipeline } of tests ) {
      let results = await Project.aggregate(pipeline);
      log({ name, results });
    }


    // Client Manipulation

    let results = await Project.find().lean();

    results = results.map(({ entries, tags, ...r }) =>
      ({
        ...r,
        entries: entries.map(({ tags: etags, ...e }) =>
          ({
            ...e,
            tags: etags.map( tid => tags.find(t => t._id.equals(tid)) )
          })
        )
      })
    );

    log({ name: 'Client re-map', results });

    mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})();

// Data

const data =[
  {
    "_id": ObjectId("5ae5afc93e1d0d2965a4f2d7"),
    "entries" : [
      {
        "_id" : ObjectId("5b159ebb0ed51064925dff24"),
        "someField": "someData",
        "tags" : [
          ObjectId("5b142ab7e419614016b8992d")
        ]
      },
    ],
    "tags" : [
      {
        "_id" : ObjectId("5b142608e419614016b89925"),
        "name" : "Outdated",
        "color" : "#3498db"
      },
      {
        "_id" : ObjectId("5b142ab7e419614016b8992d"),
        "name" : "Shitake",
        "color" : "#95a5a6"
      },
    ]
  },
  {
    "_id": ObjectId("5b1b1ad07325c4c541e8a972"),
    "entries" : [
      {
        "_id" : ObjectId("5b1b1b267325c4c541e8a973"),
        "otherField": "otherData",
        "tags" : [
          ObjectId("5b142608e419614016b89925"),
          ObjectId("5b142ab7e419614016b8992d")
        ]
      },
    ],
    "tags" : [
      {
        "_id" : ObjectId("5b142608e419614016b89925"),
        "name" : "Outdated",
        "color" : "#3498db"
      },
      {
        "_id" : ObjectId("5b142ab7e419614016b8992d"),
        "name" : "Shitake",
        "color" : "#95a5a6"
      },
    ]
  }
];

И это дастполный вывод (с дополнительным выводом из вспомогательного экземпляра MongoDB 3.6) в виде:

Mongoose: projects.remove({}, {})
Mongoose: projects.insertMany([ { entries: [ { _id: 5b159ebb0ed51064925dff24, someField: 'someData', tags: [ 5b142ab7e419614016b8992d ] } ], _id: 5ae5afc93e1d0d2965a4f2d7, tags: [ { _id: 5b142608e419614016b89925, name: 'Outdated', color: '#3498db' }, { _id: 5b142ab7e419614016b8992d, name: 'Shitake', color: '#95a5a6' } ], __v: 0 }, { entries: [ { _id: 5b1b1b267325c4c541e8a973, otherField: 'otherData', tags: [ 5b142608e419614016b89925, 5b142ab7e419614016b8992d ] } ], _id: 5b1b1ad07325c4c541e8a972, tags: [ { _id: 5b142608e419614016b89925, name: 'Outdated', color: '#3498db' }, { _id: 5b142ab7e419614016b8992d, name: 'Shitake', color: '#95a5a6' } ], __v: 0 } ], {})
Mongoose: projects.aggregate([ { '$project': { entries: { '$map': { input: '$entries', as: 'e', in: { someField: '$$e.someField', otherField: '$$e.otherField', tags: { '$map': { input: '$$e.tags', as: 't', in: { '$arrayElemAt': [ '$tags', { '$indexOfArray': [Array] } ] } } } } } } } } ], {})
{
  "name": "Standard $project $map",
  "results": [
    {
      "_id": "5ae5afc93e1d0d2965a4f2d7",
      "entries": [
        {
          "someField": "someData",
          "tags": [
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    },
    {
      "_id": "5b1b1ad07325c4c541e8a972",
      "entries": [
        {
          "otherField": "otherData",
          "tags": [
            {
              "_id": "5b142608e419614016b89925",
              "name": "Outdated",
              "color": "#3498db"
            },
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    }
  ]
}
Mongoose: projects.aggregate([ { '$project': { entries: { '$map': { input: '$entries', as: 'e', in: { '$mergeObjects': [ '$$e', { tags: { '$map': { input: '$$e.tags', as: 't', in: { '$arrayElemAt': [Array] } } } } ] } } } } } ], {})
{
  "name": "With $mergeObjects",
  "results": [
    {
      "_id": "5ae5afc93e1d0d2965a4f2d7",
      "entries": [
        {
          "_id": "5b159ebb0ed51064925dff24",
          "someField": "someData",
          "tags": [
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    },
    {
      "_id": "5b1b1ad07325c4c541e8a972",
      "entries": [
        {
          "_id": "5b1b1b267325c4c541e8a973",
          "otherField": "otherData",
          "tags": [
            {
              "_id": "5b142608e419614016b89925",
              "name": "Outdated",
              "color": "#3498db"
            },
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    }
  ]
}
Mongoose: projects.find({}, { fields: {} })
{
  "name": "Client re-map",
  "results": [
    {
      "_id": "5ae5afc93e1d0d2965a4f2d7",
      "__v": 0,
      "entries": [
        {
          "_id": "5b159ebb0ed51064925dff24",
          "someField": "someData",
          "tags": [
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    },
    {
      "_id": "5b1b1ad07325c4c541e8a972",
      "__v": 0,
      "entries": [
        {
          "_id": "5b1b1b267325c4c541e8a973",
          "otherField": "otherData",
          "tags": [
            {
              "_id": "5b142608e419614016b89925",
              "name": "Outdated",
              "color": "#3498db"
            },
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    }
  ]
}

Обратите внимание, что сюда включены некоторые дополнительные данные для демонстрации проекции "переменных полей".

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...