$ поиск по ObjectId в массиве

104

Каков синтаксис для выполнения поиска $ в поле, которое является массивом 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 будет отслеживать, как массив работает должным образом (а не как буквальное значение).
Ася Камский

Ответы:

142

Обновление 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можно сказать, что «лучше всего работает» с «дизайном отношений», который противоположен тому, как что-то вроде mongoose .populate()выполняет соединения на стороне клиента. Идентифицируя вместо этого «один» в каждом «множестве», вы просто извлекаете связанные элементы без необходимости $unwindсначала обращаться к массиву.

Blakes Seven
источник
Спасибо, это работает! Является ли это индикатором того, что мои данные неправильно структурированы / нормализованы?
Джейсон Лин
1
@JasonLin Не так однозначно, как "хорошо / плохо", поэтому к ответу добавлено немного дополнительных объяснений. Все зависит от того, что вам подходит.
Blakes Seven
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, как указано в комментарии выше.

user12164
источник
4

используйте $ unwind, вы получите первый объект вместо массива объектов

запрос:

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

Я должен не согласиться, мы можем заставить $ lookup работать с массивом идентификаторов, если мы поставим перед ним стадию $ 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"
        }
    }
])

Либстер Камерад
источник