Spring Boot bài 06: Hiển thị chi tiết khóa học bằng Thymeleaf
- 18-10-2026
- Toanngo92
- 0 Comments
Giáo trình Spring Boot MVC và REST API · Chương 3/12: Spring MVC và dữ liệu hiển thị · Bài 06/28 · Biên soạn tháng 10/2026.
Mục lục
Mục tiêu bài học
Đọc dữ liệu từ Model, tạo liên kết chi tiết và trả trang 404 khi tài nguyên không tồn tại.
Chọn dữ liệu theo id
Trang danh sách chứa nhiều khóa học, còn trang chi tiết chỉ hiển thị một khóa học xác định. URL /courses/1 truyền id cho Controller; Controller gọi Service rồi đặt DTO vào Model với tên course. Dữ liệu hiển thị được quyết định phía server, không lấy id từ một ô ẩn rồi tin rằng đó là khóa học hợp lệ.
Phương thức Service tìm entity hoặc ném CourseNotFound. Việc trả null rồi để template truy cập thuộc tính sẽ biến một tình huống bình thường thành lỗi render khó hiểu. Tách lỗi không tồn tại giúp giao diện và API đều có thể trả 404 theo cách phù hợp.
Tệp trong mã nguồn mẫu: src/main/java/com/hocvietcode/academy/CourseWebController.java. Đoạn dưới được trích từ tệp này; xem bản tải về để có đủ import và phần còn lại.
@GetMapping("/courses/{id}") String detail(@PathVariable Long id,Model model){model.addAttribute("course",service.get(id));return "courses/detail";}
Template đọc DTO ra sao?
${course.title} đọc thuộc tính title của đối tượng trong Model. th:text thay nội dung của phần tử bằng văn bản đã escape. th:href tạo URL từ biểu thức, thay id và xử lý tham số cho đúng. Không nên nối tay URL chứa dữ liệu có khoảng trắng hoặc ký tự đặc biệt.
Trong bản mẫu, CourseDto là record có id, title, price, categoryId và categoryName. DTO không mang theo các quan hệ JPA tùy ý hoặc thông tin nhạy cảm. Đây là cách giới hạn rõ ràng những gì trang được phép hiển thị và cũng là bước chuẩn bị để dùng cùng dữ liệu trong API JSON.
<h1 th:text="${course.title}">Tên khóa học mẫu</h1>
<p>Danh mục: <span th:text="${course.categoryName}"></span></p>
<p>Giá: <span th:text="${course.price}"></span></p>
<a th:href="@{/courses/{id}(id=${course.id})}">Xem chi tiết</a>
HTTP status và giao diện phải cùng phản ánh kết quả
Một trang có dòng “Không tìm thấy” nhưng trả 200 vẫn thông báo sai cho client và công cụ tìm kiếm. WebErrors vừa đặt status 404 vừa chọn template error/custom. Trình duyệt hiển thị thông báo thân thiện; Network vẫn cho biết tài nguyên không tồn tại.
Advice này được giới hạn cho Controller giao diện và tệp. API dùng advice riêng để trả ProblemDetail, không trả HTML cho client đang mong JSON. Không gom mọi exception thành 200 kèm thông báo; cũng không gửi stack trace hoặc câu SQL ra cho người xem.
Tệp trong mã nguồn mẫu: src/main/java/com/hocvietcode/academy/WebErrors.java. Đoạn dưới được trích từ tệp này; xem bản tải về để có đủ import và phần còn lại.
package com.hocvietcode.academy;
import org.springframework.web.bind.annotation.*;
import org.springframework.ui.Model;
import jakarta.servlet.http.HttpServletResponse;
@ControllerAdvice(assignableTypes={CourseWebController.class,FileController.class})
public class WebErrors {
@ExceptionHandler(CourseNotFound.class) String missing(Model model,HttpServletResponse response){response.setStatus(404);model.addAttribute("message","Không tìm thấy khóa học");return "error/custom";}
@ExceptionHandler(IllegalArgumentException.class) String invalid(IllegalArgumentException e,Model model,HttpServletResponse response){response.setStatus(400);model.addAttribute("message",e.getMessage());return "error/custom";}
}
Kiểm tra kết quả với dữ liệu có ý nghĩa
Mở /courses/1 để thấy tên và danh mục từ dữ liệu seed. Mở /courses/999999 để kiểm tra 404. Nếu giá là 0, đó là giá trị BigDecimal hợp lệ của khóa miễn phí, không phải dữ liệu bị thiếu. Việc trình bày 0 thành “Miễn phí” có thể làm ở template, còn giá trị nghiệp vụ vẫn giữ dạng số.
Trang detail trong mã mẫu có liên kết sửa và form xóa để minh họa luồng quản trị. Server mới là nơi quyết định quyền: một người nhìn thấy liên kết không đồng nghĩa được phép thực hiện. Ở bài bảo mật, ta kiểm tra cả route quản trị và request ghi dữ liệu, thay vì chỉ ẩn nút trên giao diện.
Lỗi thường gặp và lưu ý
- Không dùng th:utext cho tiêu đề người dùng nhập vì nó render HTML không escape.
- Không sửa template để che mọi lỗi lazy loading; cần chuẩn bị DTO trong transaction ở Service.
Ghi nhớ
Trang chi tiết cần một DTO đúng phạm vi, URL rõ ràng, hiển thị an toàn và mã HTTP phản ánh đúng kết quả.

