$ lookup на ObjectId в масиві


103

Який синтаксис для пошуку $ в полі, яке є масивом ObjectIds, а не просто одним ObjectId?

Приклад документа замовлення:

{
  _id: ObjectId("..."),
  products: [
    ObjectId("..<Car ObjectId>.."),
    ObjectId("..<Bike ObjectId>..")
  ]
}

Неробочий запит:

db.orders.aggregate([
    {
       $lookup:
         {
           from: "products",
           localField: "products",
           foreignField: "_id",
           as: "productObjects"
         }
    }
])

Бажаний результат

{
  _id: ObjectId("..."),
  products: [
    ObjectId("..<Car ObjectId>.."),
    ObjectId("..<Bike ObjectId>..")
  ],
  productObjects: [
    {<Car Object>},
    {<Bike Object>}
  ],
}

Мій приклад із документом замовлення недостатньо зрозумілий? Ви хочете отримати приклади документів на продукцію?
Джейсон Лін

SERVER-22881 буде відслідковувати роботу з масивом, як очікувалося (не як буквальне значення).
Ася Камський

Відповіді:


139

Оновлення 2017 року

$ lookup тепер може безпосередньо використовувати масив як локальне поле . $unwindбільше не потрібен.

Стара відповідь

$lookupЕтап агрегації трубопроводу не буде працювати безпосередньо з масивом. Основний намір проекту полягає у поєднанні "ліворуч" як об'єднанні типу "один до багатьох" (або насправді "пошуку") щодо можливих пов'язаних даних. Але значення має бути одиничним, а не масивом.

Тому ви повинні «денормалізувати» вміст спочатку перед виконанням $lookupоперації, щоб це працювало. А це означає використання $unwind:

db.orders.aggregate([
    // Unwind the source
    { "$unwind": "$products" },
    // Do the lookup matching
    { "$lookup": {
       "from": "products",
       "localField": "products",
       "foreignField": "_id",
       "as": "productObjects"
    }},
    // Unwind the result arrays ( likely one or none )
    { "$unwind": "$productObjects" },
    // Group back to arrays
    { "$group": {
        "_id": "$_id",
        "products": { "$push": "$products" },
        "productObjects": { "$push": "$productObjects" }
    }}
])

Після $lookupзбігів з кожним членом масиву результат є самим масивом, тому ви $unwindзнову і $groupдо $pushнових масивів для кінцевого результату.

Зверніть увагу, що будь-які збіги "лівого приєднання", які не знайдені, створюватимуть порожній масив для "productObjects" на даному продукті і, таким чином, заперечуватимуть документ для елемента "product" при виклику другого $unwind.

Хоча пряме застосування до масиву було б непоганим, це просто те, як це працює в даний час, зіставляючи значення однини з можливими багатьма.

Як $lookupце в принципі дуже нове, воно наразі працює так, як це було б відомо тим, хто знайомий з мангустом як "версія бідного чоловіка" .populate()запропонованого там методу. Різниця полягає в тому, що $lookupпропонується обробка "приєднання" на стороні сервера, на відміну від клієнта, і в $lookupтому, що на даний момент бракує частини "зрілості" в .populate()пропозиціях (наприклад, інтерполяція пошуку безпосередньо в масиві).

Насправді це завдання призначене для вдосконалення SERVER-22881 , тож з деякою удачею це вдарить до наступного випуску чи одного незабаром після цього.

Як принцип дизайну, ваша поточна структура не є ні доброю, ні поганою, а просто підлягає накладним витратам при створенні будь-якого "з'єднання". Таким чином, застосовується основний постійний принцип MongoDB на початку, коли, якщо ви "можете" жити з даними, "попередньо приєднаними" в одній колекції, то найкраще це зробити.

Інша річ, про яку можна говорити $lookupяк про загальний принцип, полягає в тому, що мета "приєднатися" тут - працювати навпаки, ніж показано тут. Тож замість того, щоб зберігати «пов’язані ідентифікатори» інших документів у «батьківському» документі, загальним принципом, який найкраще підходить, є те, що «пов’язані документи» містять посилання на «батьківський».

Таким чином, $lookupможна сказати, що "найкраще працює" з "дизайном відносин", що є зворотним принципом того, як щось подібне .populate()виконує мангуст, і це приєднується до клієнта. Ідентифікуючи "один" всередині кожного "багатьох" замість цього, ви просто втягуєте пов'язані елементи, не потребуючи $unwindспочатку масиву.


Дякую, це працює! Це показник того, що мої дані не структуровані / нормалізовані належним чином?
Джейсон Лін

1
@JasonLin Не так прямо, як "добре / погано", тому до відповіді додано трохи більше пояснень. Це залежить від того, що вам підходить.
Блекс Сім

2
нинішня реалізація є дещо ненавмисною. має сенс шукати всі значення в масиві локального поля, немає сенсу використовувати масив буквально, тому SERVER-22881 буде відслідковувати це виправлення.
Ася Камський

@AsyaKamsky Це має сенс. Я взагалі лікував запити повторно$lookup перевірки та перевірки документів як на особливості їх зародження та, ймовірно, покращаться. Тож пряме розширення масиву буде вітатися, як і "запит" для фільтрації результатів. Обидва вони були б набагато більш узгодженими з .populate()процесом мангуста, до якого багато хто звик. Додавання посилання на проблему безпосередньо у вміст відповіді.
Blakes Seven,

2
Зверніть увагу, що відповідно до відповіді нижче цієї, це вже реалізовано та $lookup тепер працює безпосередньо на масив.
Адам Рейс


15

Ви також можете використовувати pipeline етап для перевірки масиву піддокументів

Ось приклад використання python(вибачте, що я змії).

db.products.aggregate([
  { '$lookup': {
      'from': 'products',
      'let': { 'pid': '$products' },
      'pipeline': [
        { '$match': { '$expr': { '$in': ['$_id', '$$pid'] } } }
        // Add additional stages here 
      ],
      'as':'productObjects'
  }
])

Суть тут полягає у тому, щоб відповідати всім об’єктам у ObjectId array(чужому, _idщо знаходиться в localполі / пропіproducts ).

Ви також можете очистити або спроектувати закордонні записи з додатковими stages, як зазначено у коментарі вище.


4

Використовуючи $ Unind, ви отримаєте перший об'єкт замість масиву об'єктів

запит:

db.getCollection('vehicles').aggregate([
  {
    $match: {
      status: "AVAILABLE",
      vehicleTypeId: {
        $in: Array.from(newSet(d.vehicleTypeIds))
      }
    }
  },
  {
    $lookup: {
      from: "servicelocations",
      localField: "locationId",
      foreignField: "serviceLocationId",
      as: "locations"
    }
  },
  {
    $unwind: "$locations"
  }
]);

результат:

{
    "_id" : ObjectId("59c3983a647101ec58ddcf90"),
    "vehicleId" : "45680",
    "regionId" : 1.0,
    "vehicleTypeId" : "10TONBOX",
    "locationId" : "100",
    "description" : "Isuzu/2003-10 Ton/Box",
    "deviceId" : "",
    "earliestStart" : 36000.0,
    "latestArrival" : 54000.0,
    "status" : "AVAILABLE",
    "accountId" : 1.0,
    "locations" : {
        "_id" : ObjectId("59c3afeab7799c90ebb3291f"),
        "serviceLocationId" : "100",
        "regionId" : 1.0,
        "zoneId" : "DXBZONE1",
        "description" : "Masafi Park Al Quoz",
        "locationPriority" : 1.0,
        "accountTypeId" : 0.0,
        "locationType" : "DEPOT",
        "location" : {
            "makani" : "",
            "lat" : 25.123091,
            "lng" : 55.21082
        },
        "deliveryDays" : "MTWRFSU",
        "timeWindow" : {
            "timeWindowTypeId" : "1"
        },
        "address1" : "",
        "address2" : "",
        "phone" : "",
        "city" : "",
        "county" : "",
        "state" : "",
        "country" : "",
        "zipcode" : "",
        "imageUrl" : "",
        "contact" : {
            "name" : "",
            "email" : ""
        },
        "status" : "",
        "createdBy" : "",
        "updatedBy" : "",
        "updateDate" : "",
        "accountId" : 1.0,
        "serviceTimeTypeId" : "1"
    }
}


{
    "_id" : ObjectId("59c3983a647101ec58ddcf91"),
    "vehicleId" : "81765",
    "regionId" : 1.0,
    "vehicleTypeId" : "10TONBOX",
    "locationId" : "100",
    "description" : "Hino/2004-10 Ton/Box",
    "deviceId" : "",
    "earliestStart" : 36000.0,
    "latestArrival" : 54000.0,
    "status" : "AVAILABLE",
    "accountId" : 1.0,
    "locations" : {
        "_id" : ObjectId("59c3afeab7799c90ebb3291f"),
        "serviceLocationId" : "100",
        "regionId" : 1.0,
        "zoneId" : "DXBZONE1",
        "description" : "Masafi Park Al Quoz",
        "locationPriority" : 1.0,
        "accountTypeId" : 0.0,
        "locationType" : "DEPOT",
        "location" : {
            "makani" : "",
            "lat" : 25.123091,
            "lng" : 55.21082
        },
        "deliveryDays" : "MTWRFSU",
        "timeWindow" : {
            "timeWindowTypeId" : "1"
        },
        "address1" : "",
        "address2" : "",
        "phone" : "",
        "city" : "",
        "county" : "",
        "state" : "",
        "country" : "",
        "zipcode" : "",
        "imageUrl" : "",
        "contact" : {
            "name" : "",
            "email" : ""
        },
        "status" : "",
        "createdBy" : "",
        "updatedBy" : "",
        "updateDate" : "",
        "accountId" : 1.0,
        "serviceTimeTypeId" : "1"
    }
}

0

Агрегація з $lookupта подальшими $groupє досить громіздкою, тому якщо (і це засіб, якщо) ви використовуєте node & Mongoose або допоміжну бібліотеку з деякими підказками у схемі, ви можете використовувати a .populate()для отримання цих документів:

var mongoose = require("mongoose"),
    Schema = mongoose.Schema;

var productSchema = Schema({ ... });

var orderSchema = Schema({
  _id     : Number,
  products: [ { type: Schema.Types.ObjectId, ref: "Product" } ]
});

var Product = mongoose.model("Product", productSchema);
var Order   = mongoose.model("Order", orderSchema);

...

Order
    .find(...)
    .populate("products")
    ...

0

Я маю не погодитися, ми можемо змусити пошук $ $ працювати з масивом ідентифікаторів, якщо ми введемо його до етапу $ match.

// replace IDs array with lookup results
db.products.aggregate([
    { $match: { products : { $exists: true } } },
    {
        $lookup: {
            from: "products",
            localField: "products",
            foreignField: "_id",
            as: "productObjects"
        }
    }
])

Це ускладнюється, якщо ми хочемо передати результат пошуку в конвеєр. Але знову ж таки є спосіб зробити це (вже запропонований @ user12164):

// replace IDs array with lookup results passed to pipeline
db.products.aggregate([
    { $match: { products : { $exists: true } } },
    {
        $lookup: {
            from: "products",
             let: { products: "$products"},
             pipeline: [
                 { $match: { $expr: {$in: ["$_id", "$$products"] } } },
                 { $project: {_id: 0} } // suppress _id
             ],
            as: "productObjects"
        }
    }
])

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.