MongoDB
 sql >> Cơ Sở Dữ Liệu >  >> NoSQL >> MongoDB

$ tra cứu nhiều cấp độ mà không có $ unwind?

Tất nhiên, có một số cách tiếp cận tùy thuộc vào phiên bản MongoDB có sẵn của bạn. Những điều này thay đổi tùy theo các cách sử dụng khác nhau của $lookup cho phép thao tác đối tượng trên .populate() kết quả qua .lean() .

Tôi yêu cầu bạn đọc kỹ các phần và lưu ý rằng tất cả có thể không giống như khi xem xét giải pháp triển khai của bạn.

MongoDB 3.6, $ lookup "lồng nhau"

Với MongoDB 3.6, $lookup toán tử nhận được khả năng bổ sung để bao gồm một pipeline biểu thức trái ngược với việc chỉ cần kết hợp giá trị khóa "cục bộ" với "ngoại", điều này có nghĩa là về cơ bản, bạn có thể thực hiện mỗi $lookup là "lồng nhau" bên trong các biểu thức đường ống này

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "let": { "reviews": "$reviews" },
    "pipeline": [
       { "$match": { "$expr": { "$in": [ "$_id", "$$reviews" ] } } },
       { "$lookup": {
         "from": Comment.collection.name,
         "let": { "comments": "$comments" },
         "pipeline": [
           { "$match": { "$expr": { "$in": [ "$_id", "$$comments" ] } } },
           { "$lookup": {
             "from": Author.collection.name,
             "let": { "author": "$author" },
             "pipeline": [
               { "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
               { "$addFields": {
                 "isFollower": { 
                   "$in": [ 
                     mongoose.Types.ObjectId(req.user.id),
                     "$followers"
                   ]
                 }
               }}
             ],
             "as": "author"
           }},
           { "$addFields": { 
             "author": { "$arrayElemAt": [ "$author", 0 ] }
           }}
         ],
         "as": "comments"
       }},
       { "$sort": { "createdAt": -1 } }
     ],
     "as": "reviews"
  }},
 ])

Điều này có thể thực sự khá mạnh mẽ, như bạn thấy từ quan điểm của đường dẫn ban đầu, nó thực sự chỉ biết về việc thêm nội dung vào "reviews" mảng và sau đó mỗi biểu thức đường ống "lồng nhau" tiếp theo cũng chỉ thấy nó là các phần tử "bên trong" từ phép nối.

Nó mạnh mẽ và theo một số khía cạnh, nó có thể rõ ràng hơn một chút vì tất cả các đường dẫn trường đều liên quan đến mức lồng nhau, nhưng nó bắt đầu thụt lề trong cấu trúc BSON và bạn cần phải biết liệu bạn có khớp với các mảng hay không hoặc các giá trị số ít trong việc duyệt qua cấu trúc.

Lưu ý rằng chúng tôi cũng có thể làm những việc ở đây như "san bằng thuộc tính tác giả" như được thấy trong "comments" mục nhập mảng. Tất cả $lookup đầu ra mục tiêu có thể là một "mảng", nhưng trong "đường ống con", chúng ta có thể định hình lại mảng phần tử đơn lẻ đó thành một giá trị duy nhất.

Tra cứu MongoDB $ tiêu chuẩn

Vẫn giữ "tham gia trên máy chủ", bạn thực sự có thể làm điều đó với $lookup , nhưng nó chỉ cần xử lý trung gian. Đây là cách tiếp cận lâu đời với việc giải cấu trúc mảng với $unwind và sử dụng $group các giai đoạn để xây dựng lại mảng:

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "localField": "reviews",
    "foreignField": "_id",
    "as": "reviews"
  }},
  { "$unwind": "$reviews" },
  { "$lookup": {
    "from": Comment.collection.name,
    "localField": "reviews.comments",
    "foreignField": "_id",
    "as": "reviews.comments",
  }},
  { "$unwind": "$reviews.comments" },
  { "$lookup": {
    "from": Author.collection.name,
    "localField": "reviews.comments.author",
    "foreignField": "_id",
    "as": "reviews.comments.author"
  }},
  { "$unwind": "$reviews.comments.author" },
  { "$addFields": {
    "reviews.comments.author.isFollower": {
      "$in": [ 
        mongoose.Types.ObjectId(req.user.id), 
        "$reviews.comments.author.followers"
      ]
    }
  }},
  { "$group": {
    "_id": { 
      "_id": "$_id",
      "reviewId": "$review._id"
    },
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "review": {
      "$first": {
        "_id": "$review._id",
        "createdAt": "$review.createdAt",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content"
      }
    },
    "comments": { "$push": "$reviews.comments" }
  }},
  { "$sort": { "_id._id": 1, "review.createdAt": -1 } },
  { "$group": {
    "_id": "$_id._id",
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "reviews": {
      "$push": {
        "_id": "$review._id",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content",
        "comments": "$comments"
      }
    }
  }}
])

Điều này thực sự không quá khó khăn như bạn nghĩ lúc đầu và tuân theo một mô hình đơn giản của $lookup$unwind khi bạn tiến bộ qua từng mảng.

"author" tất nhiên là chi tiết là số ít, vì vậy một khi "chưa được ràng buộc", bạn chỉ muốn để nó theo cách đó, hãy thực hiện thêm trường và bắt đầu quá trình "quay trở lại" vào các mảng.

Chỉ có hai các cấp độ để xây dựng lại Venue ban đầu tài liệu, vì vậy mức chi tiết đầu tiên là bằng Review để xây dựng lại "comments" mảng. Tất cả những gì bạn cần là $push đường dẫn của "$reviews.comments" để thu thập những thứ này và miễn là "$reviews._id" trong trường "grouping _id", những thứ khác bạn cần giữ lại là tất cả các trường khác. Bạn có thể đặt tất cả những thứ này vào _id cũng như bạn có thể sử dụng $first .

Sau đó, chỉ còn một $group nữa để quay lại Venue chinh no. Lần này, khóa nhóm là "$_id" tất nhiên, với tất cả các tài sản của chính địa điểm bằng cách sử dụng $first"$review" còn lại chi tiết quay trở lại một mảng với $push . Tất nhiên là "$comments" đầu ra từ $group trước đó trở thành "review.comments" đường dẫn.

Làm việc trên một tài liệu duy nhất và các mối quan hệ của nó, điều này thực sự không quá tệ. $unwind nhà điều hành đường ống có thể nói chung là một vấn đề về hiệu suất, nhưng trong bối cảnh của việc sử dụng này, nó không thực sự gây ra nhiều tác động như vậy.

Vì dữ liệu vẫn đang được "kết hợp trên máy chủ" nên vẫn còn lưu lượng truy cập thấp hơn nhiều so với các giải pháp thay thế còn lại khác.

Thao tác JavaScript

Tất nhiên trường hợp khác ở đây là thay vì thay đổi dữ liệu trên chính máy chủ, bạn thực sự thao tác với kết quả. Trong hầu hết trường hợp tôi sẽ ủng hộ cách tiếp cận này vì mọi "bổ sung" vào dữ liệu có thể được xử lý tốt nhất trên máy khách.

Tất nhiên là vấn đề với việc sử dụng populate() đó là trong khi nó có thể 'trông giống như' một quy trình đơn giản hơn nhiều, trên thực tế nó KHÔNG PHẢI LÀ THAM GIA theo bất kỳ cách nào. Tất cả populate() thực sự là "ẩn" quy trình cơ bản của việc gửi nhiều truy vấn đến cơ sở dữ liệu, rồi chờ kết quả thông qua xử lý không đồng bộ.

Vì vậy, "sự xuất hiện" của một phép nối thực sự là kết quả của nhiều yêu cầu tới máy chủ và sau đó thực hiện "thao tác phía máy khách" của dữ liệu để nhúng chi tiết vào các mảng.

Vì vậy, ngoài cảnh báo rõ ràng đó rằng các đặc tính hiệu suất không ngang bằng với máy chủ $lookup , một lưu ý khác tất nhiên là "mongoose Documents" trong kết quả không thực sự là các đối tượng JavaScript thuần túy có thể bị thao túng thêm.

Vì vậy, để thực hiện phương pháp này, bạn cần thêm .lean() phương thức cho truy vấn trước khi thực thi, để hướng dẫn mongoose trả về "các đối tượng JavaScript thuần túy" thay vì Document các kiểu được ép kiểu với các phương thức lược đồ được đính kèm với mô hình. Tất nhiên, lưu ý rằng dữ liệu kết quả không còn có quyền truy cập vào bất kỳ "phương thức phiên bản" nào mà nếu không sẽ được liên kết với chính các mô hình liên quan:

let venue = await Venue.findOne({ _id: id.id })
  .populate({ 
    path: 'reviews', 
    options: { sort: { createdAt: -1 } },
    populate: [
     { path: 'comments', populate: [{ path: 'author' }] }
    ]
  })
  .lean();

Hiện tại venue là một đối tượng đơn giản, chúng tôi có thể đơn giản xử lý và điều chỉnh khi cần:

venue.reviews = venue.reviews.map( r => 
  ({
    ...r,
    comments: r.comments.map( c =>
      ({
        ...c,
        author: {
          ...c.author,
          isAuthor: c.author.followers.map( f => f.toString() ).indexOf(req.user.id) != -1
        }
      })
    )
  })
);

Vì vậy, nó thực sự chỉ là một vấn đề xoay quanh từng mảng bên trong cho đến mức bạn có thể thấy followers mảng trong author thông tin chi tiết. Sau đó, so sánh có thể được thực hiện với ObjectId các giá trị được lưu trữ trong mảng đó sau lần đầu tiên sử dụng .map() để trả về các giá trị "chuỗi" để so sánh với req.user.id cũng là một chuỗi (nếu không, thì cũng thêm .toString() về điều đó), vì nhìn chung dễ dàng so sánh các giá trị này theo cách này thông qua mã JavaScript.

Một lần nữa, mặc dù tôi cần nhấn mạnh rằng nó "trông có vẻ đơn giản" nhưng trên thực tế, đó là điều bạn thực sự muốn tránh đối với hiệu suất hệ thống, vì những truy vấn bổ sung đó và việc truyền giữa máy chủ và máy khách tốn rất nhiều thời gian xử lý và thậm chí do chi phí yêu cầu, điều này làm tăng thêm chi phí thực trong quá trình vận chuyển giữa các nhà cung cấp dịch vụ lưu trữ.

Tóm tắt

Về cơ bản, đó là những phương pháp tiếp cận mà bạn có thể thực hiện, ngắn gọn là "tự làm" nơi bạn thực sự thực hiện "nhiều truy vấn" vào cơ sở dữ liệu thay vì sử dụng trình trợ giúp .populate() là.

Sử dụng đầu ra điền, sau đó bạn có thể chỉ cần thao tác dữ liệu với kết quả giống như bất kỳ cấu trúc dữ liệu nào khác, miễn là bạn áp dụng .lean() vào truy vấn để chuyển đổi hoặc trích xuất dữ liệu đối tượng thuần túy từ các tài liệu mongoose được trả về.

Trong khi các phương pháp tiếp cận tổng hợp có vẻ liên quan hơn nhiều, có "rất nhiều" nhiều lợi thế hơn khi thực hiện công việc này trên máy chủ. Các tập hợp kết quả lớn hơn có thể được sắp xếp, các phép tính có thể được thực hiện để lọc thêm và tất nhiên bạn sẽ nhận được một "phản hồi duy nhất" đối với một "yêu cầu duy nhất" được thực hiện cho máy chủ, tất cả đều không có chi phí bổ sung.

Hoàn toàn có thể tranh luận rằng bản thân các đường ống có thể đơn giản được xây dựng dựa trên các thuộc tính đã được lưu trữ trên lược đồ. Vì vậy, việc viết phương pháp của riêng bạn để thực hiện "xây dựng" này dựa trên lược đồ đính kèm sẽ không quá khó khăn.

Tất nhiên về lâu dài hơn $lookup là giải pháp tốt hơn, nhưng có lẽ bạn sẽ cần phải thực hiện thêm một chút công việc mã hóa ban đầu, nếu tất nhiên, bạn không chỉ đơn giản sao chép từ những gì được liệt kê ở đây;)




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Bảo mật cơ sở dữ liệu 101:Hiểu đặc quyền truy cập cơ sở dữ liệu

  2. Tìm tài liệu có trường mảng chứa ít nhất n phần tử của một mảng nhất định

  3. Mã hóa cơ sở dữ liệu MongoDB

  4. MongoDB $ isoDayOfWeek

  5. Mongoose Giá trị duy nhất trong mảng đối tượng lồng nhau