[Series Thực Chiến E-commerce] Bài 36: Quay xe "Chê" - Tối ưu thuật toán Dislike Blog
Chào bố đời!
Đã có "Thả tim" (Like) thì chắc chắn phải có nút "Chê" (Dislike) để cân bằng cảm xúc cho độc giả rồi. Về cơ bản, logic của Dislike chính là một tấm gương phản chiếu của nút Like ở Bài 35.
Nhìn lướt qua đoạn code của bố đời, mình thấy tư duy logic luồng chạy (flow) của anh em đã rất sắc bén: Chia rõ ràng các case (Đã dislike thì gỡ, Đang like thì gỡ like rồi mới thêm dislike).
Tuy nhiên, dưới góc độ thực chiến và tối ưu hiệu suất (Performance), đoạn code này đang dính 2 "hạt sạn" khá lớn. Lấy sổ tay ra ghi chép lại ngay nhé bố đời!
1. Phẫu thuật Controller: Bẫy .includes() và Nỗi đau "Query DB nhiều lần"
Hạt sạn thứ nhất: Cái bẫy của .includes()
Anh em dùng blog.dislikes.includes(_id) nhìn thì rất ngắn gọn và hợp lý. NHƯNG, trong Mongoose, các phần tử trong mảng dislikes đang mang kiểu dữ liệu là ObjectId (Object), trong khi _id lấy từ token ra thường là String. So sánh hai kiểu dữ liệu khác nhau bằng .includes() rất dễ dẫn đến việc code lúc chạy được lúc không (hoặc trả về false vĩnh viễn).
-> Cách fix: Vẫn nên dùng.find() và ép kiểu .toString() như Bài 35 cho an toàn tuyệt đối.
Hạt sạn thứ hai: Đấm Database 2 lần liên tiếp (Hiệu suất giảm sút)
Anh em nhìn vào khối lệnh else của mình:
if (isLiked) {
await Blog.findByIdAndUpdate(id, { $pull: { likes: _id } }, { new: true }); // Đấm DB lần 1
}
updatedBlog = await Blog.findByIdAndUpdate(id, { $push: { dislikes: _id } }, { new: true }); // Đấm DB lần 2
Giả sử có 1000 ông đang Like, tự nhiên đổi ý bấm Dislike cùng một lúc. Server của anh em sẽ phải gọi xuống Database 2000 lần! Vừa tốn kết nối, vừa làm chậm thời gian phản hồi API.
Trong khi đó, Mongoose hoàn toàn cho phép anh em nhét cả lệnh gỡ ($pull) và lệnh thêm ($push) vào CÙNG MỘT LÚC.
Cùng mình "độ" lại hàm dislikesBlog cho chuẩn dân chơi Backend nhé:
const mongoose = require('mongoose');
// 💡 Góp ý nhỏ: Sửa tên hàm thành dislikeBlog (bỏ chữ 's') nhé bố đời!
const dislikeBlog = asyncHandler(async (req, res) => {
const { _id } = req.user;
// Vẫn nhắc lại bài cũ: Đổi thành 'bid' cho đồng bộ với file Router nhen!
const { bid } = req.params;
if (!bid) return res.status(400).json({ message: 'Blog ID is required' });
if (!mongoose.Types.ObjectId.isValid(bid)) return res.status(400).json({ message: 'Invalid Blog ID' });
const blog = await Blog.findById(bid);
if (!blog) return res.status(404).json({ message: 'Blog not found' });
// 💡 FIX SẠN 1: Ép kiểu toString() để so sánh chính xác tuyệt đối
const isDisliked = blog?.dislikes?.find(el => el.toString() === _id.toString());
const isLiked = blog?.likes?.find(el => el.toString() === _id.toString());
let updatedBlog;
if (isDisliked) {
// Nếu đã chê rồi -> Bấm phát nữa để gỡ chê (Trở về trạng thái bình thường)
updatedBlog = await Blog.findByIdAndUpdate(bid, { $pull: { dislikes: _id } }, { new: true });
} else {
// 💡 FIX SẠN 2: Gộp 2 thao tác (Xóa Like và Thêm Dislike) vào duy nhất 1 lần gọi DB!
// Mongoose đủ thông minh để hiểu: Nếu trong mảng 'likes' không có _id thì nó bỏ qua lệnh $pull, chỉ chạy lệnh $push.
updatedBlog = await Blog.findByIdAndUpdate(
bid,
{
$push: { dislikes: _id },
$pull: { likes: _id }
},
{ new: true }
);
}
return res.json({
success: !!updatedBlog,
rs: updatedBlog
});
});
module.exports = {
// ... các hàm trước
dislikeBlog
}
2. Trạm gác Router
Về phần Router thì bố đời đã làm quá tốt rồi, chốt chặn verifyAccessToken hoàn toàn hợp lý để định danh người dùng. Mình chỉ cần sửa lại tên biến :id thành :bid cho khớp với Controller là xong.
const router = require('express').Router();
const ctrls = require('../controllers/blog');
const { verifyAccessToken, isAdmin } = require('../middlewares/verifyToken');
// ... (các route blog khác)
// 💡 PUT: Route Dislike bài viết
router.put('/blogs/dislike/:bid', [verifyAccessToken], ctrls.dislikeBlog);
module.exports = router;
Lời kết
Bật Postman lên chốt sổ cặp bài trùng Like/Dislike này thôi! Anh em thử luân phiên bấm gọi API Like và Dislike để xem hai mảng likes và dislikes tự động thêm bớt phần tử vô cùng nhịp nhàng mà chỉ tốn đúng 1 lượt query DB. Khá là mượt mà đúng không?
Đến đây, chúng ta tạm gác lại phần Content Marketing (Blog) đầy sôi động. Trang web bán hàng của chúng ta đã có Sản phẩm, có Danh mục, có Tin tức. Nhưng khoan đã, một yếu tố sống còn để khách hàng quyết định mua hay không mua một món đồ điện tử, mỹ phẩm... chính là Thương hiệu (Brand)!
(Ví dụ: Cùng là Điện thoại, nhưng khách thích lọc ra điện thoại của Apple, Samsung chứ không thích Oppo).
Vậy nên, chúng ta cần xây dựng một Module mới để quản lý các Thương hiệu này: Lession 37: Add brands.
All rights reserved