$ lookup trên ObjectId's trong một mảng


103

Cú pháp để thực hiện tra cứu $ trên một trường là một mảng ObjectId thay vì chỉ một ObjectId đơn lẻ là gì?

Tài liệu Đơn hàng Ví dụ:

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

Truy vấn không hoạt động:

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

Kết quả như ý

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

Ví dụ của tôi với tài liệu đặt hàng không đủ rõ ràng? bạn có muốn các tài liệu ví dụ cho các sản phẩm?
Jason Lin

SERVER-22881 sẽ theo dõi việc làm cho mảng hoạt động như mong đợi (không phải dưới dạng giá trị chữ).
Asya Kamsky

Câu trả lời:


139

Cập nhật năm 2017

$ lookup hiện có thể sử dụng trực tiếp một mảng làm trường cục bộ . $unwindkhông còn cần thiết.

Câu trả lời cũ

Các $lookupgiai đoạn đường ống dẫn tổng hợp sẽ không làm việc trực tiếp với một mảng. Mục đích chính của thiết kế là cho "kết nối bên trái" như một kiểu kết hợp "một đến nhiều" (hoặc thực sự là "tra cứu") trên dữ liệu có thể liên quan. Nhưng giá trị được dự định là số ít và không phải là một mảng.

Vì vậy, bạn phải "de-normalize" nội dung trước khi thực hiện $lookupthao tác để điều này hoạt động. Và điều đó có nghĩa là sử dụng $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" }
    }}
])

Sau $lookuptrận đấu mỗi thành viên mảng kết quả là một mảng riêng của mình, vì vậy bạn $unwindmột lần nữa và $groupđể $pushmảng mới cho kết quả cuối cùng.

Lưu ý rằng bất kỳ kết quả "kết hợp bên trái" nào không được tìm thấy sẽ tạo ra một mảng trống cho "productObjects" trên sản phẩm đã cho và do đó phủ định tài liệu cho phần tử "product" khi thứ hai $unwindđược gọi.

Mặc dù ứng dụng trực tiếp vào một mảng sẽ rất hay, nhưng đó chỉ là cách điều này hiện hoạt động bằng cách khớp một giá trị đơn lẻ với một giá trị có thể có.

Về $lookupcơ bản là rất mới, nó hiện đang hoạt động như quen thuộc với những người đã quen thuộc với mongoose như một "phiên bản dành cho người nghèo" của .populate()phương pháp được cung cấp ở đó. Sự khác biệt là $lookupcung cấp xử lý "phía máy chủ" của "tham gia" trái ngược với trên máy khách và một số "độ chín" trong $lookuphiện đang thiếu so với những gì .populate()cung cấp (chẳng hạn như nội suy tra cứu trực tiếp trên một mảng).

Đây thực sự là một vấn đề được chỉ định để cải tiến SERVER-22881 , vì vậy nếu may mắn, điều này sẽ xảy ra với bản phát hành tiếp theo hoặc ngay sau đó.

Theo nguyên tắc thiết kế, cấu trúc hiện tại của bạn không tốt hay xấu mà chỉ phụ thuộc vào chi phí khi tạo bất kỳ "nối" nào. Do đó, nguyên tắc cơ bản của MongoDB khi khởi đầu được áp dụng, trong đó nếu bạn "có thể" sống với dữ liệu "được tham gia trước" trong một bộ sưu tập, thì tốt nhất là nên làm như vậy.

Một điều khác có thể nói về $lookupnguyên tắc chung, đó là mục đích của "tham gia" ở đây là hoạt động theo cách khác với được hiển thị ở đây. Vì vậy, thay vì giữ "id liên quan" của các tài liệu khác trong tài liệu "gốc", nguyên tắc chung hoạt động tốt nhất là nơi "tài liệu liên quan" chứa tham chiếu đến "tài liệu gốc".

Vì vậy, $lookupcó thể nói là "hoạt động tốt nhất" với một "thiết kế quan hệ" là mặt trái của cách mà một thứ như mongoose .populate()thực hiện mà phía khách hàng tham gia. Thay vào đó, bằng cách lý tưởng hóa "một" trong mỗi "nhiều", sau đó bạn chỉ cần lấy các mục liên quan mà không cần đến $unwindmảng trước.


Cảm ơn bạn nó hoạt động! Đây có phải là dấu hiệu cho thấy dữ liệu của tôi không được cấu trúc / chuẩn hóa đúng cách không?
Jason Lin

1
@JasonLin Không đơn giản như "tốt / xấu", do đó, có một chút giải thích thêm vào câu trả lời. Nó phụ thuộc vào những gì phù hợp với bạn.
Blakes Seven,

2
việc thực hiện hiện tại là hơi không chủ ý. hợp lý khi tra cứu tất cả các giá trị trong một mảng trường cục bộ, không hợp lý khi sử dụng mảng theo nghĩa đen để SERVER-22881 sẽ theo dõi việc sửa lỗi đó.
Asya Kamsky

@AsyaKamsky Điều đó có lý. Tôi thường coi các yêu cầu lại $lookupvà Xác thực tài liệu là các tính năng trong giai đoạn sơ khai và có khả năng cải thiện. Vì vậy, việc mở rộng trực tiếp trên một mảng sẽ được hoan nghênh, cũng như một "truy vấn" để lọc kết quả. Cả hai điều đó sẽ phù hợp hơn với .populate()quy trình mongoose mà nhiều người đã quen. Thêm liên kết vấn đề trực tiếp vào nội dung câu trả lời.
Blakes Seven,

2
Lưu ý rằng theo câu trả lời bên dưới câu trả lời này, điều này hiện đã được triển khai và $lookuphiện hoạt động trực tiếp trên một mảng.
Adam Reis,


15

Bạn cũng có thể sử dụng pipelinegiai đoạn này để thực hiện kiểm tra trên một mảng tài liệu con

Đây là ví dụ sử dụng python(xin lỗi tôi là người rắn).

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

Điều bắt buộc ở đây là so khớp tất cả các đối tượng trong ObjectId array(nước ngoài _idnằm trong localfield / prop products).

Bạn cũng có thể xóa hoặc chiếu các bản ghi nước ngoài với các bản ghi bổ sung stage, như được chỉ ra trong nhận xét ở trên.


4

sử dụng $ unwind bạn sẽ nhận được đối tượng đầu tiên thay vì mảng đối tượng

truy vấn:

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

kết quả:

{
    "_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

Việc tổng hợp với $lookupvà sau đó $groupkhá cồng kềnh, vì vậy nếu (và đó là phương tiện nếu) bạn đang sử dụng node & Mongoose hoặc một thư viện hỗ trợ với một số gợi ý trong lược đồ, bạn có thể sử dụng a .populate()để tìm nạp các tài liệu đó:

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

Tôi không đồng ý, chúng ta có thể làm cho $ lookup hoạt động với mảng ID nếu chúng ta mở đầu nó với giai đoạn $ match.

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

Nó trở nên phức tạp hơn nếu chúng ta muốn chuyển kết quả tra cứu đến một đường dẫn. Nhưng sau đó, có một cách để làm như vậy (đã được đề xuất bởi @ 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"
        }
    }
])

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.