MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

Wypełnij obiekt w tablicy

Zasadniczo przegapiłeś tutaj „ścieżkę” do pola, które chcesz populate() to w rzeczywistości 'portfolio.formatType' a nie tylko 'portfolio' jak wpisałeś. Z powodu tego błędu i struktury możesz mieć jednak kilka ogólnych nieporozumień.

Wypełnij korektę

Podstawowa korekta wymaga jedynie prawidłowej ścieżki, a ty nie potrzebujesz model argument, ponieważ jest to już zawarte w schemacie:

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

Jednak generalnie nie jest dobrym pomysłem "mieszanie" zarówno "osadzone" danych, jak i "odnośnych" danych w tablicach, i tak naprawdę powinieneś albo osadzać wszystko, albo po prostu odwoływać się do wszystkiego. Jest to również trochę "anty-wzorcem" w ogóle, aby zachować tablicę odniesień w dokumencie, jeśli masz zamiar się odwoływać, ponieważ Twoim powodem nie powinno być powodowanie, że dokument przekroczy limit 16 MB BSON. A tam, gdzie Twoje dane nigdy nie osiągną tego limitu, generalnie lepiej jest „w pełni osadzić”. To naprawdę szersza dyskusja, ale coś, o czym powinieneś wiedzieć.

Następnym ogólnym punktem tutaj jest populate() samo w sobie jest trochę „starym kapeluszem” i tak naprawdę nie jest „magiczną” rzeczą, za którą większość nowych użytkowników ją postrzega. Aby wyczyścić populate() to NIE DOŁĄCZENIE , a wszystko, co robi, to wykonanie kolejnego zapytania do serwera w celu zwrócenia „powiązanych” elementów, a następnie scalenie tej treści z dokumentami zwróconymi z poprzedniego zapytania.

$lookup alternatywa

Jeśli szukasz „połączeń”, to prawdopodobnie chciałeś „osadzenia”, jak wspomniano wcześniej. To jest naprawdę "Sposób MongoDB" radzenia sobie z "relacjami", ale utrzymywanie wszystkich "powiązanych" danych w jednym dokumencie. Innym sposobem „dołączenia”, w którym dane znajdują się w oddzielnych zbiorach, jest użycie $lookup operator w nowoczesnych wydaniach.

Staje się to nieco bardziej skomplikowane ze względu na twoją „mieszaną” formę tablicy treści, ale generalnie może być reprezentowane jako:

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

Lub w bardziej wyrazistej formie $lookup od 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",
      }}
    ]
  }}
]);

Te dwa podejścia działają nieco inaczej, ale oba zasadniczo działają z koncepcją zwrócenia pasujących „powiązanych” wpisów, a następnie „ponownego mapowania” na istniejącą zawartość tablicy w celu scalenia z "name" właściwości „osadzone” wewnątrz tablicy. To jest właściwie główna komplikacja, która w przeciwnym razie jest dość prostą metodą odzyskiwania.

To prawie ten sam proces, co populate() faktycznie działa na „kliencie”, ale jest wykonywany na „serwerze”. Zatem porównania używają $indexOfArray operator, aby znaleźć pasujący ObjectId wartości są, a następnie zwracają właściwość z tablicy o dopasowanym „indeksie” za pośrednictwem $arrayElemAt operacja.

Jedyna różnica polega na tym, że w wersji zgodnej z MongoDB 3.6 wykonujemy to „podstawienie” w „obcej” treści „przed” połączone wyniki są zwracane do rodzica. W poprzednich wersjach zwracamy całą pasującą obcą tablicę, a następnie „połączamy” obie, aby utworzyć pojedynczą „połączoną” tablicę za pomocą $map .

Chociaż początkowo mogą one wyglądać na „bardziej złożone”, dużą zaletą jest to, że stanowią one „pojedyncze żądanie” do serwera z „pojedynczą odpowiedzią” i nie wystawiać i nie otrzymywać „wielu” żądań jako populate() czy. W rzeczywistości oszczędza to dużo ruchu sieciowego i znacznie wydłuża czas odpowiedzi.

Ponadto są to „prawdziwe sprzężenia”, więc można zrobić o wiele więcej, czego nie można osiągnąć za pomocą „wielu zapytań”. Na przykład możesz "sortować" wyniki po "dołączeniu" i zwracać tylko najlepsze wyniki, gdzie jak przy użyciu populate() musi przyciągnąć „wszystkich rodziców”, zanim będzie mógł nawet szukać, które „dzieci” w rezultacie wrócą. To samo dotyczy warunków „filtrowania” w przypadku „dołączenia” dziecka.

Więcej szczegółów na ten temat znajdziesz w Pytaniu po wypełnieniu w Mongoose o ogólnych ograniczeniach i o tym, co faktycznie możesz zrobić, aby „zautomatyzować” generowanie takich „złożonych” instrukcji potoku agregacji tam, gdzie jest to potrzebne.

Demonstracja

Innym częstym problemem związanym z wykonywaniem tych „połączeń” i ogólnym zrozumieniem schematu odniesienia jest to, że ludzie często mylą się z pojęciami, gdzie i kiedy przechowywać odniesienia i jak to wszystko działa. Dlatego poniższe wykazy służą jako demonstracja zarówno przechowywania, jak i wyszukiwania takich danych.

W natywnej implementacji Promises dla starszych wersji 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());

})()

Oraz z async/await składnia nowszych wydań NodeJS, w tym bieżącej serii 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()
  }

})()

Ta ostatnia lista jest skomentowana na każdym etapie w celu wyjaśnienia części, a przez porównanie możesz przynajmniej zobaczyć, jak obie formy składni odnoszą się do siebie.

Zwróć uwagę, że „ekspresyjny” $lookup przykład działa tylko wtedy, gdy podłączony serwer MongoDB faktycznie obsługuje składnię.

I „wyjście” dla tych, którym nie przeszkadza samo uruchomienie kodu:

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. Mongodb:nie udało się połączyć z serwerem przy pierwszym połączeniu

  2. Jak zliczyć wiele kluczy w tym samym zapytaniu agregującym $group MongoDB?

  3. Ignoruj ​​połączenie z gniazdem MongoDB podczas testu wiosennego

  4. Java MongoDB:Jaka jest różnica między com.mongodb.DB a com.mongodb.client

  5. Jak uruchomić mongodb, zakładając konto