$ ค้นหาบน 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>}
  ],
}

ตัวอย่างเอกสารคำสั่งซื้อของฉันไม่ชัดเจนเพียงพอหรือไม่ คุณต้องการเอกสารตัวอย่างสำหรับผลิตภัณฑ์หรือไม่?
Jason Lin

SERVER-22881 จะติดตามการทำให้อาร์เรย์ทำงานตามที่คาดไว้ (ไม่ใช่ค่าตามตัวอักษร)
Asya Kamsky

คำตอบ:


145

อัปเดต 2017

$ lookup สามารถใช้อาร์เรย์เป็นฟิลด์โลคัลได้โดยตรง$unwindไม่จำเป็นอีกต่อไป

คำตอบเก่า

$lookupเวทีท่อรวมจะไม่ทำงานโดยตรงกับอาร์เรย์ จุดประสงค์หลักของการออกแบบคือเพื่อให้ "การรวมด้านซ้าย" เป็นการรวมประเภท "หนึ่งต่อหลาย" (หรือ "การค้นหา") ในข้อมูลที่เกี่ยวข้องที่เป็นไปได้ แต่ค่านี้ตั้งใจให้เป็นเอกพจน์ไม่ใช่อาร์เรย์

ดังนั้นคุณต้อง "de-normalize" เนื้อหาก่อนที่จะดำเนิน$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อาร์เรย์ใหม่สำหรับผลลัพธ์สุดท้าย

โปรดทราบว่าการจับคู่ "left join" ที่ไม่พบจะสร้างอาร์เรย์ว่างสำหรับ "productObjects" บนผลิตภัณฑ์ที่กำหนดและจะลบล้างเอกสารสำหรับองค์ประกอบ "product" เมื่อ$unwindมีการเรียกครั้งที่สอง

แม้ว่าแอปพลิเคชันโดยตรงกับอาร์เรย์จะดี แต่ก็เป็นเพียงวิธีการทำงานในปัจจุบันโดยการจับคู่ค่าเอกพจน์กับจำนวนที่เป็นไปได้

ในฐานะที่$lookupเป็นพื้นใหม่มากมันปัจจุบันทำงานเป็นจะคุ้นเคยกับผู้ที่มีความคุ้นเคยกับพังพอนเป็น "มนุษย์ที่ยากจนรุ่น" ของ.populate()วิธีการที่นำเสนอมี ความแตกต่างที่$lookupนำเสนอการประมวลผล "เข้าร่วม" "ฝั่งเซิร์ฟเวอร์" ตรงข้ามกับไคลเอนต์และ$lookupขณะนี้"วุฒิภาวะ" บางส่วนขาดหายไปจาก.populate()ข้อเสนอ (เช่นการแก้ไขการค้นหาโดยตรงบนอาร์เรย์)

นี่เป็นปัญหาที่ได้รับมอบหมายสำหรับการปรับปรุงSERVER-22881ดังนั้นด้วยความโชคดีสิ่งนี้จะไปถึงรุ่นถัดไปหรือหลังจากนั้นไม่นาน

ตามหลักการออกแบบโครงสร้างปัจจุบันของคุณไม่ดีหรือไม่ดี แต่ขึ้นอยู่กับค่าใช้จ่ายเมื่อสร้าง "เข้าร่วม" ด้วยเหตุนี้หลักการพื้นฐานของ MongoDB ในการเริ่มต้นจึงมีผลบังคับใช้โดยที่หากคุณ "สามารถ" อยู่กับข้อมูล "รวมล่วงหน้า" ในคอลเล็กชันเดียวได้ก็ควรทำเช่นนั้น

อีกสิ่งหนึ่งที่อาจกล่าวได้ว่า$lookupเป็นหลักการทั่วไปก็คือเจตนาของการ "เข้าร่วม" ที่นี่คือการทำงานในลักษณะอื่นนอกเหนือจากที่แสดงไว้ที่นี่ ดังนั้นแทนที่จะเก็บ "รหัสที่เกี่ยวข้อง" ของเอกสารอื่นไว้ในเอกสาร "พาเรนต์" หลักการทั่วไปที่ได้ผลดีที่สุดคือที่ "เอกสารที่เกี่ยวข้อง" มีการอ้างอิงถึง "พาเรนต์"

ดังนั้น$lookupอาจกล่าวได้ว่า "ทำงานได้ดีที่สุด" ด้วย "การออกแบบความสัมพันธ์" ซึ่งเป็นสิ่งที่ตรงกันข้ามกับวิธี.populate()การทำงานของพังพอนในการรวมฝั่งไคลเอ็นต์ ด้วยการกำหนดค่า "หนึ่ง" ภายใน "หลายรายการ" แทนจากนั้นคุณก็ดึงรายการที่เกี่ยวข้องโดยไม่จำเป็นต้อง$unwindใช้อาร์เรย์ก่อน


ขอบคุณมันใช้งานได้! นี่เป็นตัวบ่งชี้ว่าข้อมูลของฉันไม่มีโครงสร้าง / ทำให้เป็นมาตรฐานอย่างถูกต้องหรือไม่
Jason Lin

1
@JasonLin ไม่ตรงไปตรงมาเหมือน "ดี / ไม่ดี" ดังนั้นจึงมีคำอธิบายเพิ่มเติมเล็กน้อยสำหรับคำตอบ ขึ้นอยู่กับสิ่งที่เหมาะกับคุณ
Blakes Seven

2
การใช้งานในปัจจุบันค่อนข้างไม่ได้ตั้งใจ มันสมเหตุสมผลที่จะค้นหาค่าทั้งหมดในอาร์เรย์ของฟิลด์โลคัลมันไม่สมเหตุสมผลที่จะใช้อาร์เรย์อย่างแท้จริงดังนั้น SERVER-22881 จะติดตามการแก้ไขนั้น
Asya Kamsky

@AsyaKamsky ที่สมเหตุสมผล. โดยทั่วไปฉันได้รับการปฏิบัติต่อการสอบถาม re $lookupและการตรวจสอบเอกสารว่าเป็นคุณสมบัติในวัยเด็กและมีแนวโน้มที่จะปรับปรุง ดังนั้นการขยายโดยตรงบนอาร์เรย์จะได้รับการต้อนรับเช่นเดียวกับ "แบบสอบถาม" เพื่อกรองผลลัพธ์ ทั้งสองอย่างนี้จะสอดคล้องกับ.populate()กระบวนการพังพอนที่หลายคนคุ้นเคย การเพิ่มลิงก์ปัญหาลงในเนื้อหาคำตอบโดยตรง
Blakes Seven

2
โปรดทราบว่าตามคำตอบด้านล่างนี้ตอนนี้ได้นำไปใช้$lookupแล้วและตอนนี้ทำงานโดยตรงบนอาร์เรย์
Adam Reis

49

เริ่มด้วย MongoDB v3.4 (ปล่อยตัวในปี 2016) ที่เวทีท่อรวมยังสามารถทำงานโดยตรงกับอาร์เรย์$lookup ไม่มีความจำเป็น$unwindใด ๆ อีกแล้ว

นี้ได้รับการติดตามในเซิร์ฟเวอร์ 22881


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

ใช้$ คลายคุณจะได้รับวัตถุแรกแทนอาร์เรย์ของวัตถุ

คำถาม:

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

ฉันไม่เห็นด้วยเราสามารถทำให้ $ lookup ทำงานกับอาร์เรย์ ID ได้ถ้าเรานำหน้าด้วย $ match stage

// 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.