Phân trang con trỏ MongoDB

Mixmax có một danh sách API rất phong phú cho phép người dùng của chúng tôi truy cập trực tiếp vào tất cả dữ liệu của họ. Một số API, chẳng hạn như Danh bạ có thể trả về hàng triệu kết quả. Rõ ràng là chúng tôi không thể trả lại tất cả chúng cùng một lúc, vì vậy chúng tôi cần trả lại một tập hợp con - hoặc một trang - tại một thời điểm. Kỹ thuật này được gọi là phân trang và phổ biến đối với hầu hết các API. Phân trang có thể được thực hiện theo nhiều cách khác nhau, một số cách tốt hơn những cách khác. Trong bài đăng này, chúng tôi thảo luận về các cách tiếp cận triển khai khác nhau và cách cuối cùng chúng tôi triển khai cách tiếp cận của riêng mình để phục vụ dữ liệu được lưu trữ trong MongoDB. Chúng tôi cũng giới thiệu một mô-đun npm mới để dễ dàng cho phép bạn làm điều tương tự

Phương pháp triển khai phân trang chung #1. Phân trang dựa trên offset (“trang được đánh số”)

Đây là một trong những cách tiếp cận phân trang phổ biến hơn. Người gọi API chuyển một tham số

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
3 (còn gọi là
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
4 hoặc
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
5) và
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
6 (còn gọi là
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
7 hoặc
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
8). Giới hạn là số lượng kết quả mà nó muốn và phần bù là số lượng mục cần “bỏ qua” trước khi bắt đầu trả về kết quả. Ví dụ: Github cung cấp API dựa trên offset để tìm nạp Github Repos giống như thế này

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
0

Mô hình này có một lỗ hổng lớn. nếu danh sách kết quả đã thay đổi giữa các lần gọi API, thì các chỉ mục sẽ thay đổi và khiến một mục được trả lại hai lần hoặc bị bỏ qua và không bao giờ được trả lại. Vì vậy, trong ví dụ trên, nếu repo Github bị xóa sau khi bạn truy vấn trang kết quả đầu tiên, thì mục nhập thứ 101 trước đó (không có trong trang 100 đầu tiên ban đầu của bạn) giờ sẽ là mục nhập thứ 100, vì vậy nó sẽ không xảy ra'

Phương pháp triển khai phân trang chung #2. Phân trang dựa trên thời gian

Cách tiếp cận thứ hai là phân trang dựa trên thời gian. Vì vậy, nếu API của bạn (trong trường hợp này là /items) trả về kết quả được sắp xếp theo thứ tự mới nhất trước tiên, bạn có thể chuyển ngày

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
1 của mục cuối cùng trong danh sách để nhận trang tiếp theo của các mục được tạo trước nó

API mẫu

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
2

Điều này giải quyết vấn đề trong Phương pháp chung số 1 ở trên vì kết quả không còn bị bỏ qua. Nếu bạn truy vấn trang đầu tiên và sau đó một mục mới bị xóa, nó sẽ không thay đổi kết quả trong trang thứ hai của bạn và tất cả đều ổn. Tuy nhiên, phương pháp này có một lỗ hổng lớn. nếu có nhiều mục được tạo cùng một lúc thì sao? . Bạn sẽ bỏ lỡ kết quả

cách tiếp cận đúng. Phân trang dựa trên con trỏ

Cách tốt nhất để tạo phân trang API theo cách an toàn là API trả về một “con trỏ” hoặc chuỗi mờ kèm theo danh sách kết quả. Mã thông báo này thường được gọi là

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
5 hoặc
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
6 hoặc
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
7 Mã thông báo này có thể được chuyển với yêu cầu API tiếp theo để yêu cầu trang tiếp theo

Đây là một ví dụ từ Twitter API

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
8

Bằng cách trả về một “con trỏ”, API đảm bảo rằng nó sẽ trả về chính xác mục nhập tiếp theo trong danh sách, bất kể những thay đổi nào xảy ra với bộ sưu tập giữa các lần gọi API. Hãy coi con trỏ là điểm đánh dấu vĩnh viễn trong danh sách có nội dung “chúng tôi đã rời khỏi đây”

Triển khai phân trang dựa trên con trỏ (trong MongoDB)

Vì vậy, chúng tôi đã mô tả con trỏ là cách tiếp cận phù hợp để phân trang API, nhưng chúng tôi thực sự triển khai chúng như thế nào? . Nó được gọi là phân trang con trỏ mongo. Tôi sẽ mô tả ngắn gọn cách nó được triển khai và suy nghĩ của chúng tôi đằng sau nó

Vì con trỏ chỉ là một điểm đánh dấu để nói “chúng ta đã rời khỏi đây”, nên chúng ta chỉ cần biến nó thành giá trị của một trường trong bộ sưu tập. Tuy nhiên, trường đó phải thỏa mãn các tính chất sau

  • Duy nhất. Giá trị này phải là duy nhất cho tất cả các tài liệu trong bộ sưu tập. Nếu không, chúng ta sẽ gặp sự cố trong Phương pháp tiếp cận chung #2. nếu nhiều tài liệu có cùng giá trị, thì chúng ta không thể “tiếp tục chính xác nơi chúng ta đã dừng lại” vì không phải tất cả các tài liệu đều có thể phân biệt được với nhau. Điều này cũng có nghĩa là tất cả các tài liệu phải có trường này được đặt thành một thứ gì đó, nếu không MongoDB sẽ coi nó là null

  • Có thể đặt hàng. Mọi giá trị của trường này phải có thể được so sánh với mọi giá trị khác. Điều này là do chúng tôi cần sắp xếp theo trường này để có được danh sách kết quả để sau đó trả lại trong các trang. Nếu trường là một số

    const items = db.items.find({
      _id: { $lt: req.query.next }
    }).sort({
       _id: -1
    }).limit(2);
    
    const next = items[items.length - 1]._id
    res.json({ items, next })
    9,
    const items = db.items.find({
      _id: { $lt: req.query.next }
    }).sort({
       _id: -1
    }).limit(2);
    
    const next = items[items.length - 1]._id
    res.json({ items, next })
    00 hoặc bất kỳ kiểu dữ liệu có thể sắp xếp nào khác trong MongoDB, thì nó thỏa mãn điều này

  • bất biến. Giá trị trong trường này không thể thay đổi, nếu không thì các trang sẽ không trả về đúng thông tin. Ví dụ: nếu bạn chọn trường “tên” làm trường con trỏ và tên bị thay đổi trong khi ai đó đang tìm nạp kết quả thì mục nhập đó có thể bị bỏ qua

Vì vậy, giả sử rằng chúng tôi đang chọn trường MongoDB tiêu chuẩn

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
01 để phân trang con trỏ. Đây là một sự lựa chọn hợp lý vì nó thỏa mãn các tiêu chí trên và luôn tồn tại trên mọi tài liệu trong MongoDB. Trong ví dụ này, chúng tôi sẽ triển khai API trả về 2 mục. Nó sẽ trả về một “con trỏ” (thực sự chỉ là giá trị chuỗi của
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
01 cuối cùng) mà người gọi có thể chuyển để đến trang tiếp theo

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
03

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
4

Sau đó, khi người dùng muốn lấy trang thứ hai, họ chuyển con trỏ (dưới dạng

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
5) trên URL

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
05

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })

Giải pháp này hoạt động rất tốt vì chúng tôi đang trả về các kết quả được sắp xếp theo

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
01, điều này trong MongoDB xảy ra với thời gian tạo được làm tròn đến giây gần nhất cộng với một số entropy khác. Nhưng giả sử chúng ta muốn trả về kết quả theo thứ tự khác, chẳng hạn như ngày mặt hàng được đưa vào cửa hàng trực tuyến,
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
07. Để làm cho nó rõ ràng trong ví dụ, chúng tôi sẽ thêm
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
08 vào chuỗi truy vấn

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
09

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
0

Sau đó, để tìm nạp trang thứ hai, họ sẽ chuyển con trỏ

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
5 (có thể là ngày ra mắt được mã hóa dưới dạng chuỗi)

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
41

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
4

Tuy nhiên, nếu chúng tôi tung ra một loạt các mặt hàng vào cùng một ngày và thời gian thì sao? . Chúng tôi không thể sử dụng nó làm trường con trỏ. Nhưng chờ đã, có một cách. chúng ta có thể sử dụng hai trường để tạo con trỏ. Vì chúng tôi biết rằng trường

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
01 trong MongoDB luôn đáp ứng ba tiêu chí trên, chúng tôi biết rằng nếu chúng tôi sử dụng nó cùng với trường
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
07 của mình, sự kết hợp của hai trường sẽ đáp ứng các yêu cầu và có thể được sử dụng cùng nhau làm trường con trỏ. Điều này cũng sẽ cho phép người dùng tiếp tục nhận được phản hồi được sắp xếp theo
const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
07

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
09

Sau đó, để tìm nạp trang tiếp theo, chúng tôi sẽ gọi

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
47

Bây giờ chúng tôi có chính xác những gì chúng tôi muốn. một API cho phép người dùng nhận các kết quả được phân trang được sắp xếp theo ngày ra mắt. Khi các mục mới được khởi chạy và các mục cũ bị xóa, kết quả được phân trang sẽ không bị gián đoạn và một mục sẽ không bao giờ bị trùng lặp hoặc bỏ sót trong phản hồi

Các ví dụ mã trên giải thích suy nghĩ của chúng tôi đằng sau việc xây dựng thư viện mongo-cursor-pagination. Nếu bạn sử dụng thư viện, bạn sẽ không cần viết đoạn mã trên - mô-đun sẽ làm điều đó cho bạn. Chúng tôi đã sử dụng mongo-cursor-pagination tại Mixmax để phục vụ hàng triệu tài liệu thông qua API nhà phát triển của chúng tôi

Một lưu ý về các chỉ mục MongoDB

Khi sử dụng mô-đun chạy các truy vấn tương tự như mã ví dụ ở trên, điều quan trọng là bạn phải tạo các chỉ mục MongoDB phù hợp. Điều này đặc biệt quan trọng đối với các bộ sưu tập lớn (>1 nghìn bản ghi) vì bạn có thể vô tình làm chậm cơ sở dữ liệu của mình. Vì vậy, đối với truy vấn trên, hãy luôn đảm bảo tạo một chỉ mục (như được giải thích tại đây) bao gồm tất cả các thuộc tính được sử dụng trong truy vấn cùng với trường con trỏ (được gọi là Trường phân trang trong mô-đun) và trường

const items = db.items.find({
  _id: { $lt: req.query.next }
}).sort({
   _id: -1
}).limit(2);

const next = items[items.length - 1]._id
res.json({ items, next })
01

Phần kết luận

Sử dụng kỹ thuật phân trang dựa trên con trỏ và MongoDB, chúng tôi có thể phục vụ hàng triệu yêu cầu API mỗi ngày. Các API của chúng tôi phản hồi trong cùng một khoảng thời gian bất kể chúng đang phân phát bao nhiêu tài nguyên hoặc truy vấn trang nào

Phân trang con trỏ là gì?

Phân trang dựa trên con trỏ hoạt động bằng cách trả về một con trỏ tới một mục cụ thể trong tập dữ liệu . Trong các yêu cầu tiếp theo, máy chủ trả về kết quả sau con trỏ đã cho.

MongoDB có hỗ trợ phân trang không?

Phân trang MongoDB cung cấp một cách hiệu quả để lập mô hình dữ liệu giúp phân trang nhanh và hiệu quả, trong MongoDB, chúng ta có thể khám phá một số lượng lớn dữ liệu một cách nhanh chóng và dễ dàng.

Phương pháp nào được coi là hiệu quả nhất để phân trang?

Hầu hết các trang web sử dụng phân trang bù đắp vì tính đơn giản của nó và mức độ trực quan của phân trang đối với người dùng. Để thực hiện phân trang bù đắp, chúng tôi thường sẽ cần hai mẩu thông tin. giới hạn - Số hàng để tìm nạp từ cơ sở dữ liệu. offset - Số hàng cần bỏ qua.