配列内のObjectIdの$ lookup


103

単一のObjectIdではなくObjectIdの配列であるフィールドで$ lookupを実行するための構文は何ですか?

注文ドキュメントの例:

{
  _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は、配列を期待どおりに(リテラル値としてではなく)機能させることを追跡します。
Asya Kamsky 2016年

回答:


139

2017年更新

$ lookupは、配列を直接ローカルフィールドとして使用できるようになりました$unwindもう必要ありません。

古い答え

$lookup集合パイプラインステージは、アレイを直接操作しないであろう。設計の主な目的は、可能性のある関連データの「1対多」タイプの結合(または実際には「ルックアップ」)としての「左結合」です。ただし、値は配列ではなく、単数であることを意図しています。

したがって$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」の空の配列を作成するため、2番目の$unwind呼び出し時に「product」要素のドキュメントを無効にすることに注意してください。

配列に直接適用するのは良いことですが、特異値を可能な限り多くに一致させることによってこれが現在どのように機能するかということです。

$lookupそれが現在に精通している人にはおなじみだろうとして動作しますが、基本的には非常に新しいですマングースの「貧しいマンのバージョン」など.populate()が提供方法。違いは$lookup、クライアントではなく「結合」の「サーバー側」処理を提供すること、および$lookup現在の「成熟度」の一部には.populate()提供されているもの(配列で直接ルックアップを補間するなど)がないことです。

これは実際にはSERVER-22881の改善に割り当てられた問題であるため、運が良ければ、次のリリースまたはその直後にリリースされます。

設計原則として、現在の構造は良いものでも悪いものでもありませんが、「結合」を作成するときにオーバーヘッドが発生するだけです。そのため、MongoDBの基本的な基本原則が適用されます。1つのコレクションで「事前結合」されたデータを使用して「できる」場合は、そうすることをお勧めします。

$lookup一般原則として言えるもう1つのことは、ここでの「結合」の目的は、ここで示した方法とは逆の方法で作業することです。したがって、他のドキュメントの「関連ID」を「親」ドキュメント内に保持するのではなく、「関連ドキュメント」に「親」への参照が含まれている場合に、最も効果的な一般原則が得られます。

つまり$lookup、mongooseのようなものが.populate()クライアント側の結合を実行する方法の逆である「リレーションデザイン」で「最適に機能する」と言えます。代わりに、各「多数」内の「1」を特定することにより、$unwind最初に配列を必要とせずに、関連する項目をプルするだけです。


よろしくお願いします!これは、データが適切に構造化/正規化されていないことを示していますか?
Jason Lin

1
@JasonLin「良い/悪い」ほどstraigtforwardではないので、答えに説明が少し追加されています。それはあなたに合ったものに依存します。
Blakes Seven、2016

2
現在の実装は意図的ではありません。ローカルフィールドの配列のすべての値を検索することは意味があります。配列を文字どおりに使用することは意味がないため、SERVER-22881はその修正を追跡します。
Asya Kamsky 2016年

@AsyaKamskyそれは理にかなっています。私は一般的に、照会$lookupとドキュメントの検証を初期の機能であり、改善される可能性が高いものとして扱ってきました。そのため、結果をフィルタリングするための「クエリ」と同様に、配列の直接拡張が歓迎されます。それらの両方は.populate()、多くの人が慣れているマングースのプロセスとはるかに一致しています。問題リンクを回答コンテンツに直接追加します。
ブレイクスセブン2016

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(field / propにある外部の)内のすべてのオブジェクトを照合する_idことです。localproducts

stage上記のコメントで示されているように、追加のを使用して外部レコードをクリーンアップまたは投影することもできます。


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またはスキーマにいくつかのヒントを持つサポートライブラリを使用している場合(そしてそれが中程度の場合)、.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ステージを前に付ければ、ID配列で$ lookupを機能させることができるので、私は同意しません。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.