0

[Series Thực Chiến E-commerce] Bài 18: Nghệ thuật "Bới lông tìm vết" - Lọc, Sắp xếp và Phân trang (Filter, Sort, Pagination)

Chào anh em!

Nếu ở các bài trước, chúng ta chỉ lấy data kiểu "có bao nhiêu vét máng bấy nhiêu" (Product.find()), thì hôm nay chúng ta sẽ bước vào một trong những bài toán "khoai" nhất, nhưng cũng thể hiện rõ nhất trình độ của một Backend Developer: Filter, Sort và Pagination.

Anh em cứ tưởng tượng kho hàng có 100.000 sản phẩm. Khách hàng vào web và yêu cầu: "Tìm cho tôi điện thoại (Filter), giá từ 10 - 20 triệu (Advanced Filter), sắp xếp từ rẻ đến đắt (Sort), và tôi đang ở trang số 2 (Pagination)".

Nếu bốc cả 100.000 sản phẩm lên RAM rồi dùng Javascript array filter thì sập server mất. Mọi thứ phải được giải quyết dưới tầng Database. Cùng mổ xẻ đoạn code "thần thánh" dưới đây nhé!

1. Kỹ thuật "Chaining" (Nối chuỗi Query)

Trước khi đi vào chi tiết, có một điểm cực kỳ ăn tiền trong đoạn code của anh em mà nhiều newbie không biết. Đó là dòng này: let queryCommand = Product.find(formatedQueries);

Anh em để ý KHÔNG hề có chữ await ở đây. Tại sao? Bởi vì nếu gọi await Product.find(), Mongoose sẽ chạy thẳng xuống DB và lấy data lên luôn. Còn nếu không có await, Mongoose sẽ trả về một Query Object dạng pending. Nhờ vậy, ở các bước sau chúng ta mới có thể "nhồi" thêm .sort(), .skip(), .limit() vào cái queryCommand này. Chỉ đến cuối cùng, ta mới await nó để chốt sổ! Đây là pattern cực kỳ tiêu chuẩn.

2. Tách bạch tham số & Advanced Filtering (Lọc nâng cao)

Khi Frontend gửi URL lên, nó sẽ trông như thế này: ?limit=10&page=2&sort=-price&title=iphone&price[gte]=1000

const queries = { ...req.query };

  // 1. Dọn dẹp: Tách các trường không phải là "tiêu chí lọc" ra khỏi object queries
  const excludeFields = ['limit', 'sort', 'page', 'fields'];
  excludeFields.forEach(el => delete queries[el]);

  // 2. Format lại các toán tử (Operators)
  let queryString = JSON.stringify(queries);
  queryString = queryString.replace(/\b(gte|gt|lt|lte)\b/g, matchedEl => `$${matchedEl}`);
  const formatedQueries = JSON.parse(queryString);

Giải thích nhẹ: Frontend gửi price[gte]=1000 (giá lớn hơn hoặc bằng 1000). Object req.querysẽ hiểu là { price: { gte: '1000' } }. Nhưng Mongoose lại không hiểu gte, nó chỉ hiểu cú pháp có dấu đô-la $gte. Thay vì lặp qua từng key để thêm dấu $, thủ thuật ép về chuỗi JSON -> dùng Regex chèn $ -> Parse lại thành Object là một pha xử lý cồng kềnh nhưng cực kỳ hiệu quả và "tinh tế"!

3. Tìm kiếm tương đối (Regex Search)

Khách hàng gõ "iphone" thì anh em cũng phải tìm ra "Điện thoại iPhone 15 Pro".

if (queries?.title) formatedQueries.title = { $regex: queries.title, $options: 'i' };

Đoạn này mình nhét thẳng Regex vào Mongoose. Cờ $options: 'i' (case-insensitive) giúp việc tìm kiếm không phân biệt chữ hoa chữ thường. Quá tiện!

4. Sắp xếp (Sorting)

Giả sử Frontend muốn sắp xếp theo giá giảm dần, ngày tạo tăng dần: ?sort=-price,createdAt

if (req.query.sort) {
    const sortBy = req.query.sort.split(',').join(' ');
    queryCommand = queryCommand.sort(sortBy);
  }

Mongoose yêu cầu các tiêu chí sắp xếp phải cách nhau bằng dấu cách (Space), ví dụ: "-price createdAt". Vậy nên ta chỉ cần split(',') rồi join(' ') là chuẩn form.

5. Phân trang (Pagination)

Đây là công thức toán học vỡ lòng của dân Dev:

let page = req.query.page || 1; // Mặc định trang 1
  let limit = req.query.limit || 10; // Mặc định 10 item/trang
  const skip = (page - 1) * limit;

  queryCommand = queryCommand.skip(skip).limit(limit);

Muốn lấy data ở trang 3, mỗi trang 10 sản phẩm? skip = (3 - 1) * 10 = 20. Nghĩa là Mongoose sẽ bỏ qua 20 sản phẩm đầu tiên, và .limit(10) để bốc đúng 10 sản phẩm tiếp theo.

6. Cú chốt: Thực thi & Trả về tổng số (Total Counts)

try {
    const response = await queryCommand;
    const counts = await Product.countDocuments(formatedQueries); // Đếm tổng số lượng thỏa mãn điều kiện

    return res.status(200).json({
      success: response ? true : false,
      products: response ? response : 'Cannot get products',
      counts,
    });
  } catch (err) { ... }

Tại sao phải có counts? Anh em làm Frontend (như React/Vue) khi vẽ cái thanh Pagination (Trang 1 2 3 ... 10) sẽ rất cần biết tổng số lượng sản phẩm để chia cho limit ra số trang. Không trả về counts là mấy ông dev Front-end tế anh em lên luôn đấy nhé!

Lời kết

Viết xong hàm này là anh em có thể tự tin vỗ ngực xưng tên đi apply các vị trí Backend được rồi. Logic chuẩn chỉ, code sạch đẹp và tối ưu hiệu suất tốt. Anh em test thử trên Postman bằng cách truyền đủ loại query vào URL xem kết quả trả về có "mượt" không nhé!

Sau khi khách xem được sản phẩm, chọn được món ưng ý rồi thì họ sẽ làm gì? Tất nhiên là để lại bình luận, đánh giá (Review/Comment) xem hàng có "chuẩn auth" không rồi.

Đó chính là nội dung của Lession 19: Comment Product.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí