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

$lookup на няколко нива без $unwind?

Разбира се, има няколко подхода в зависимост от наличната ви версия на MongoDB. Те варират в зависимост от различните употреби на $lookup до активиране на манипулиране на обекти в .populate() резултат чрез .lean() .

Моля ви да прочетете внимателно разделите и да имате предвид, че всичко може да не е така, както изглежда, когато обмисляте решението си за внедряване.

MongoDB 3.6, "вложено" $lookup

С MongoDB 3.6 $lookup операторът получава допълнителна способност да включва pipeline израз, за ​​разлика от простото присъединяване на стойност на "местен" към "чужд" ключ, това означава, че по същество можете да правите всеки $lookup като "вложени" в тези изрази на конвейера

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "let": { "reviews": "$reviews" },
    "pipeline": [
       { "$match": { "$expr": { "$in": [ "$_id", "$$reviews" ] } } },
       { "$lookup": {
         "from": Comment.collection.name,
         "let": { "comments": "$comments" },
         "pipeline": [
           { "$match": { "$expr": { "$in": [ "$_id", "$$comments" ] } } },
           { "$lookup": {
             "from": Author.collection.name,
             "let": { "author": "$author" },
             "pipeline": [
               { "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
               { "$addFields": {
                 "isFollower": { 
                   "$in": [ 
                     mongoose.Types.ObjectId(req.user.id),
                     "$followers"
                   ]
                 }
               }}
             ],
             "as": "author"
           }},
           { "$addFields": { 
             "author": { "$arrayElemAt": [ "$author", 0 ] }
           }}
         ],
         "as": "comments"
       }},
       { "$sort": { "createdAt": -1 } }
     ],
     "as": "reviews"
  }},
 ])

Това може да бъде наистина доста мощно, както виждате от гледна точка на оригиналния конвейер, той наистина знае само за добавяне на съдържание към "reviews" масив и след това всеки следващ „вложен“ израз на тръбопровода също вижда само „вътрешните“ елементи от съединението.

Той е мощен и в някои отношения може да е малко по-ясен, тъй като всички пътеки на полето са относителни към нивото на влагане, но започва това пълзене на вдлъбнатини в структурата на BSON и трябва да сте наясно дали съпоставяте с масиви или единични стойности при преминаване през структурата.

Забележете, че тук можем да правим и неща като "изравняване на свойството на автора", както се вижда в "comments" записи в масива. Всички $lookup целевият изход може да бъде "масив", но в рамките на "под-тръбопровод" можем да преоформим този масив от един елемент само в една стойност.

Стандартен MongoDB $lookup

Все още запазвайки „присъединяване към сървъра“, всъщност можете да го направите с $lookup , но изисква само междинна обработка. Това е дългогодишният подход с деконструиране на масив с $unwind и използващата $group етапи за възстановяване на масиви:

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "localField": "reviews",
    "foreignField": "_id",
    "as": "reviews"
  }},
  { "$unwind": "$reviews" },
  { "$lookup": {
    "from": Comment.collection.name,
    "localField": "reviews.comments",
    "foreignField": "_id",
    "as": "reviews.comments",
  }},
  { "$unwind": "$reviews.comments" },
  { "$lookup": {
    "from": Author.collection.name,
    "localField": "reviews.comments.author",
    "foreignField": "_id",
    "as": "reviews.comments.author"
  }},
  { "$unwind": "$reviews.comments.author" },
  { "$addFields": {
    "reviews.comments.author.isFollower": {
      "$in": [ 
        mongoose.Types.ObjectId(req.user.id), 
        "$reviews.comments.author.followers"
      ]
    }
  }},
  { "$group": {
    "_id": { 
      "_id": "$_id",
      "reviewId": "$review._id"
    },
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "review": {
      "$first": {
        "_id": "$review._id",
        "createdAt": "$review.createdAt",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content"
      }
    },
    "comments": { "$push": "$reviews.comments" }
  }},
  { "$sort": { "_id._id": 1, "review.createdAt": -1 } },
  { "$group": {
    "_id": "$_id._id",
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "reviews": {
      "$push": {
        "_id": "$review._id",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content",
        "comments": "$comments"
      }
    }
  }}
])

Това наистина не е толкова обезсърчително, колкото може да си помислите в началото и следва прост модел на $lookup и $unwind докато преминавате през всеки масив.

"author" Детайлът, разбира се, е единствен, така че след като се „развие“, просто искате да го оставите така, да добавите полето и да започнете процеса на „връщане назад“ в масивите.

Има самодва нива, за да се възстанови обратно към оригиналното Venue документ, така че първото ниво на детайлност е от Review за да изградите отново "comments" масив. Всичко, което трябва, е да $push пътя на "$reviews.comments" за да ги събере, и стига "$reviews._id" полето е в "grouping _id" единствените други неща, които трябва да запазите, са всички останали полета. Можете да поставите всичко това в _id също или можете да използвате $first .

След това има само още една $group етап, за да се върнете към Venue себе си. Този път ключът за групиране е "$_id" разбира се, с всички свойства на самото място, използвайки $first и оставащият "$review" подробности се връщат обратно в масив с $push . Разбира се "$comments" изход от предишната $group става "review.comments" път.

Работата върху един документ и неговите отношения, това всъщност не е толкова лошо. $unwind операторът на тръбопровода може по принцип е проблем с производителността, но в контекста на тази употреба не би трябвало да причинява толкова голямо въздействие.

Тъй като данните все още се „присъединяват към сървъра“ все още много по-малко трафик от другата оставаща алтернатива.

Манипулация на JavaScript

Разбира се, другият случай тук е, че вместо да променяте данните на самия сървър, вие всъщност манипулирате резултата. В повечето случаи Бих подкрепял този подход, тъй като всякакви „допълнения“ към данните вероятно се обработват най-добре от клиента.

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

Така че „външният вид“ на присъединяване всъщност е резултат от множество заявки към сървъра и след това извършване на „манипулация от страна на клиента“ от данните за вграждане на детайлите в масиви.

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

Така че, за да възприемете този подход, трябва да добавите .lean() метод към заявката преди изпълнение, за да инструктира mongoose да върне "обикновени JavaScript обекти" вместо Document типове, които са отхвърлени със схематични методи, прикрепени към модела. Отбелязвайки разбира се, че получените данни вече нямат достъп до никакви „методи на екземпляри“, които иначе биха били свързани със самите свързани модели:

let venue = await Venue.findOne({ _id: id.id })
  .populate({ 
    path: 'reviews', 
    options: { sort: { createdAt: -1 } },
    populate: [
     { path: 'comments', populate: [{ path: 'author' }] }
    ]
  })
  .lean();

Сега venue е обикновен обект, можем просто да обработваме и коригираме според нуждите:

venue.reviews = venue.reviews.map( r => 
  ({
    ...r,
    comments: r.comments.map( c =>
      ({
        ...c,
        author: {
          ...c.author,
          isAuthor: c.author.followers.map( f => f.toString() ).indexOf(req.user.id) != -1
        }
      })
    )
  })
);

Така че наистина е просто въпрос на циклично преминаване през всеки от вътрешните масиви надолу до нивото, където можете да видите followers масив в author подробности. След това сравнението може да се направи с ObjectId стойности, съхранени в този масив след първо използване на .map() за да върнете стойностите на "низ" за сравнение с req.user.id който също е низ (ако не е, тогава също добавете .toString() на това ), тъй като по принцип е по-лесно да се сравнят тези стойности по този начин чрез JavaScript код.

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

Резюме

Това са основно вашите подходи, които можете да предприемете, с изключение на "развъртане на собствените", където всъщност изпълнявате "множество заявки" към базата данни сами, вместо да използвате помощника, който .populate() е.

Използвайки изхода за попълване, можете просто да манипулирате данните в резултата, както всяка друга структура от данни, стига да приложите .lean() към заявката за преобразуване или по друг начин извличане на обикновените обектни данни от върнатите документи на мангуста.

Въпреки че обобщените подходи изглеждат много по-замесени, има „много“ повече предимства за извършване на тази работа на сървъра. Могат да се сортират по-големи набори от резултати, да се правят изчисления за по-нататъшно филтриране и разбира се получавате „единичен отговор“ към "единична заявка" направени на сървъра, всичко това без допълнителни разходи.

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

В дългосрочен план разбира се $lookup е по-доброто решение, но вероятно ще трябва да положите малко повече работа в първоначалното кодиране, ако, разбира се, просто не копирате от това, което е изброено тук;)




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Върнете последния документ от справка

  2. Разбиране и управление на дисковото пространство на вашия MongoDB сървър

  3. MongoDB - Множество $или операции

  4. Как да получа идентификатора на обекта, след като запазя обект в Mongoose?

  5. Как мога да използвам MongoDB с Flask?