MongoDB
 sql >> база данни >  >> NoSQL >> MongoDB

Попълване на обект в масив

Това, което основно сте пропуснали тук, е „пътят“ към полето, което искате да populate() всъщност е 'portfolio.formatType' а не само 'portfolio' както си написал. Поради тази грешка и структурата може да имате няколко общи погрешни схващания.

Попълване на корекция

Основната корекция се нуждае само от правилния път и не се нуждаете от model аргумент, тъй като това вече се подразбира в схемата:

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

Като цяло обаче не е добра идея да "смесвате" както "вградени" данни, така и "реферирани" данни в рамките на масиви и наистина трябва или да вграждате всичко, или просто да препращате към всичко. Също така е малко "анти-модел" като цяло да поддържате масив от препратки в документа, ако намерението ви е препратка, тъй като причината ви трябва да бъде да не накарате документа да надхвърли ограничението от 16MB BSON. И когато това ограничение никога няма да бъде достигнато от вашите данни, обикновено е по-добре да „вградите напълно“. Това наистина е по-широка дискусия, но трябва да сте наясно.

Следващата обща точка тук е populate() сам по себе си е донякъде "стара шапка" и наистина не е "магическото" нещо, което повечето нови потребители го възприемат като. За да е ясно populate() е НЕ ПРИСЪЕДИНЯВАНЕ и всичко, което прави, е да изпълнява друга заявка към сървъра, за да върне „свързаните“ елементи, след което да обедини това съдържание в документите, върнати от предишната заявка.

$lookup алтернатива

Ако търсите "присъединявания", тогава наистина вероятно сте искали "вграждане", както беше споменато по-рано. Това наистина е „начинът на MongoDB“ за справяне с „връзки“, но запазване на всички „свързани“ данни заедно в един документ. Другото средство за „присъединяване“, когато данните са в отделни колекции, е чрез $lookup оператор в съвременни версии.

Това става малко по-сложно поради вашата "смесена" форма на масив от съдържание, но като цяло може да бъде представено като:

// 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" ] }
            ]
          }
        }
      }
    }
  }}
]);

Или с по-изразителната форма на $lookup от 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",
      }}
    ]
  }}
]);

Двата подхода работят малко по-различно, но и двата по същество работят с концепцията за връщане на съвпадащите „свързани“ записи и след това „пренасочване“ към съдържанието на съществуващия масив, за да се слее с "name" свойства, "вградени" в масива. Това всъщност е основното усложнение, което иначе е доста лесен метод за извличане.

Това е почти същият процес като populate() всъщност прави на "клиента", но се изпълнява на "сървъра". Така че сравненията използват $indexOfArray оператор, за да намерите къде е съвпадащият ObjectId стойностите са и след това връщат свойство от масива при този съответстващ „индекс“ чрез $arrayElemAt операция.

Единствената разлика е, че в съвместимата с MongoDB 3.6 версия правим това „заместване“ в „чуждото“ съдържание „преди“ обединените резултати се връщат на родителя. В предишни издания връщаме целия съвпадащ чужд масив и след това ги „обединяваме“, за да образуваме единичен „обединен“ масив, използвайки $map .

Въпреки че първоначално може да изглеждат „по-сложни“, голямото предимство тук е, че представляват „една заявка“ към сървъра с "единичен отговор" и не издаване и получаване на "множество" заявки като populate() прави. Това всъщност спестява много режийни разходи в мрежовия трафик и значително увеличава времето за реакция.

В допълнение, това са "реални присъединявания", така че можете да направите много повече, което не може да бъде постигнато с "множество заявки". Например можете да "сортирате" резултатите на "join" и да върнете само най-добрите резултати, като използвате populate() трябва да изтегли "всички родители", преди дори да може да потърси кои "деца" да върне в резултат. Същото важи и за условията за "филтриране" на дъщерното "присъединяване".

Има малко повече подробности за това в Извършване на заявки след попълване в Mongoose за общите ограничения и какво всъщност можете да направите на практика, за да „автоматизирате“ генерирането на такива „сложни“ отчети за тръбопровод за агрегиране, където е необходимо.

Демонстрация

Друг често срещан проблем при извършването на тези "съединявания" и разбирането на референтната схема като цяло е, че хората често грешат концепциите за това къде и кога да съхраняват референциите и как всичко работи. Следователно следните списъци служат като демонстрация както на съхранението, така и на извличането на такива данни.

В собствена реализация на Promises за по-стари версии на 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());

})()

И с async/await синтаксис за по-нови версии на NodeJS, включително текущата серия LTS v.8.x:

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()
  }

})()

Последният списък е коментиран на всеки етап, за да се обяснят частите и можете поне да видите чрез сравнение как двете форми на синтаксис са свързани една с друга.

Имайте предвид, че „експресивният“ $lookup примерът се изпълнява само там, където MongoDB сървърът, към който е свързан, действително поддържа синтаксиса.

И „изходът“ за тези, които не могат да си направят труда да изпълнят кода сами:

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
    }
  ]
}



  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Филтриране на YearMonth от Mongo документ

  2. Как да променя паролата на потребител на MongoDB?

  3. Как да инициализирате набор за репликация на mongodb, без да извиквате rs.initiate()?

  4. брой на $lookup резултат mongodb

  5. Сортирайте поддокументи в MongoDB