Một lập trình viên Magento đã thay thế 300 lệnh gọi ProductRepositoryInterface::getById() riêng biệt bằng một truy vấn collection duy nhất. Số lượng truy vấn cơ sở dữ liệu đã giảm từ 300 xuống còn 1, và trang danh mục tải nhanh hơn đáng kể. Cái giá phải trả là gì? Một số sản phẩm đã biến mất khỏi cửa hàng trực tuyến, khiến cả người mua lẫn chủ cửa hàng đều bối rối.

Vấn đề N+1 trên trang danh mục Magento

Trang web liệt kê 300 sản phẩm. Với mỗi sản phẩm, mã nguồn thực hiện lấy hai thuộc tính tùy chỉnh thông qua phương thức getById() của repository, tạo ra một truy vấn cho mỗi sản phẩm—đây chính là mô hình N+1 kinh điển. Việc thay thế các lần tải từng sản phẩm đó bằng một collection giúp lấy tất cả các hàng cần thiết trong một truy vấn duy nhất đã cắt giảm đáng kể số lượng truy vấn. Độ trễ ở front-end được cải thiện, nhưng danh sách sản phẩm giờ đây lại thiếu mất những mặt hàng vốn đã xuất hiện trước đó.

Tại sao collection lại ẩn các mặt hàng hết hàng

Module CatalogInventory của Magento tự động thêm một bộ lọc "còn hàng" (in-stock) vào bất kỳ product collection nào được xây dựng cho frontend. Bộ lọc này sẽ kích hoạt khi cài đặt cửa hàng Display Out of Stock Products bị tắt, một lựa chọn phổ biến của các chủ cửa hàng không muốn hiển thị các mặt hàng không có sẵn.

Ngược lại, repository không bao giờ áp dụng bộ lọc kho hàng. Nó chỉ đơn giản kiểm tra xem sản phẩm có tồn tại hay không và trả về kết quả, bất kể mức tồn kho là bao nhiêu. Khi mã nguồn chuyển từ các lệnh gọi repository sang collection, bộ lọc kho hàng đã âm thầm loại bỏ mọi sản phẩm có số lượng bằng không. Không có lỗi nào được ghi lại; collection chỉ đơn giản là trả về ít hàng hơn so với mong đợi của lập trình viên.

Điều này có ý nghĩa gì đối với các chủ cửa hàng

Một cửa hàng trực tuyến âm thầm loại bỏ các SKU hết hàng sẽ gây ra một số vấn đề:

  • Các thành phần front-end nhận được các mục bị thiếu, dẫn đến việc hiển thị nhãn trống hoặc sai giá.
  • Việc gỡ lỗi (debugging) trở nên khó khăn hơn vì triệu chứng—các sản phẩm bị thiếu—không tạo ra bất kỳ cảnh báo nào.

Vì hầu hết các bài kiểm tra tự động đều sử dụng các dữ liệu mẫu (fixtures) có sẵn hàng, lỗi này thường ẩn mình cho đến khi trang web chạy với kho hàng thực tế.

Cách duy trì tốc độ mà không ẩn kho hàng

  1. Bỏ qua bộ lọc kho hàng mặc định – Loại bỏ bộ lọc khỏi collection một cách rõ ràng nếu bạn cần mọi sản phẩm bất kể tình trạng sẵn có. Magento cung cấp các phương thức để vô hiệu hóa hoặc thay thế stock plugin cho từng truy vấn.
  2. Sử dụng repository khi độ chính xác là quan trọng – Repository đảm bảo rằng mọi ID sản phẩm được yêu cầu đều được trả về, ngay cả khi nó đã hết hàng. Hãy lưu kết quả vào cache hoặc chỉ tải các ID bạn cần để kiểm soát các truy vấn dư thừa.
  3. Sử dụng resource model hoặc SQL thuần – Truy vấn trực tiếp bảng sản phẩm để lấy các thuộc tính cần thiết. Cách này sẽ bỏ qua tất cả các collection plugin và cho phép bạn toàn quyền kiểm soát những hàng nào được bao gồm.

Tốc độ đối lập với tính chính xác

Việc thay thế một vòng lặp N+1 gây tốn tài nguyên bằng một truy vấn collection duy nhất nghe có vẻ hợp lý; sự sụt giảm độ trễ là rất rõ rệt. Tuy nhiên, một truy vấn nhanh hơn nhưng lại trả về sai tập hợp sản phẩm thì đó là một lỗi (defect), chứ không phải là một sự tối ưu hóa. Các lập trình viên phải cân nhắc giữa tốc độ thuần túy và rủi ro hiển thị một danh mục sản phẩm không đầy đủ.

Những điều cần lưu ý tiếp theo

  • Sự sai lệch cấu hình (Configuration drift) – Xác minh rằng cờ Display Out of Stock Products khớp với hành vi mong muốn của bất kỳ mã collection tùy chỉnh nào.
  • Tác dụng phụ của plugin – Các module khác có thể thêm các bộ lọc riêng của chúng vào collection. Hãy xem xét lại plugin stack bất cứ khi nào bạn thay đổi chiến lược truy vấn.

Bài học rất rõ ràng: việc khắc phục vấn đề N+1 bằng collection chỉ thực sự hiệu quả nếu bạn đồng thời kiểm tra các bộ lọc mặc định của collection đó. Việc bỏ qua bộ lọc kho hàng tích hợp sẵn của Magento có thể âm thầm cắt tỉa danh mục sản phẩm của bạn, biến một sự cải thiện về hiệu suất thành một sự tổn thất về doanh thu.