Spring Boot bài 09: Binding biểu mẫu tạo và sửa khóa học
- 18-10-2026
- Toanngo92
- 0 Comments
Giáo trình Spring Boot MVC và REST API · Chương 5/12: Biểu mẫu, validation và điều hướng · Bài 09/28 · Biên soạn tháng 10/2026.
Mục lục
Mục tiêu bài học
Nối biểu mẫu HTML với CourseForm, phân biệt GET mở form và POST gửi dữ liệu, dùng lại form cho tạo và sửa.
Form là hợp đồng đầu vào
Biểu mẫu tạo khóa học cần tên, giá và mã danh mục. Ta dùng CourseForm chỉ chứa ba trường này, thay vì bind trực tiếp request vào entity Course. Entity còn có id và version do hệ thống quản lý; cho client tùy ý gán mọi thuộc tính có thể dẫn đến sửa dữ liệu ngoài ý định.
GET /admin/courses/new tạo form trống và cung cấp danh sách danh mục. GET /admin/courses/{id}/edit lấy DTO hiện có rồi chuyển thành CourseForm. Hai luồng dùng chung template courses/form, khác nhau ở dữ liệu ban đầu và action gửi đến. Đường dẫn quản trị yêu cầu tài khoản teacher.
<form th:action="${action}" th:object="${form}" method="post">
<label>Tên <input th:field="*{title}" maxlength="120" required></label>
<p th:errors="*{title}"></p>
<label>Giá <input th:field="*{price}" type="number" min="0" step="0.01"></label>
<p th:errors="*{price}"></p>
<label>Danh mục
<select th:field="*{categoryId}">
<option value="">Chọn danh mục</option>
<option th:each="c : ${categories}" th:value="${c.id}" th:text="${c.name}"></option>
</select>
</label>
<button>Lưu</button>
</form>
Tên thuộc tính phải khớp
th:object chọn đối tượng form; th:field tạo name, id và value phù hợp cho từng thuộc tính. Khi POST, Spring lấy giá trị request và chuyển sang String, BigDecimal, Long bằng getter/setter của CourseForm. Tên categoryId trong select phải tương ứng thuộc tính của form, không phải categoryName hoặc tên cột database.
Danh mục hiển thị tên để người dùng chọn, nhưng gửi id để Service tìm bản ghi thực sự. Một người có thể sửa HTML và gửi id không tồn tại, nên việc danh sách select chỉ chứa dữ liệu hợp lệ không đủ đảm bảo đầu vào hợp lệ. Service vẫn kiểm tra danh mục.
Chuyển DTO thành dữ liệu sửa
Phương thức edit không đưa nguyên entity vào form. Nó lấy DTO, tạo CourseForm và gán từng trường được cho sửa. id cần sửa nằm trong URL, không lấy từ một thuộc tính id không kiểm soát trong body. Cách này làm rõ tài nguyên nào được chọn và thuộc tính nào người dùng được thay đổi.
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("/admin/courses/{id}/edit") String edit(@PathVariable Long id,Model model){
CourseDto dto=service.get(id);CourseForm form=new CourseForm();form.setTitle(dto.title());form.setPrice(dto.price());form.setCategoryId(dto.categoryId());
model.addAttribute("form",form);model.addAttribute("action","/admin/courses/"+id);model.addAttribute("categories",service.categories());return "courses/form";
}
Kiểu dữ liệu và CSRF
Giá dùng BigDecimal để biểu diễn số thập phân, không dùng double cho giá tiền. Nếu nhập ký tự vào ô giá bằng cách gửi request thủ công, lỗi chuyển kiểu được đưa vào BindingResult. Bài sau xử lý tình huống đó mà không đánh mất dữ liệu người dùng vừa nhập.
Form POST dùng th:action để tích hợp với Spring và chèn CSRF token khi Security bật. Sau khi render, hãy kiểm tra có input ẩn _csrf. Không tự tạo một chuỗi token giả hoặc bỏ CSRF vì request bị 403. Lỗi binding là vấn đề dữ liệu; lỗi CSRF xảy ra ở tầng Security và thường ngăn request đi tới Controller.
Lỗi thường gặp và lưu ý
- Không bind trực tiếp toàn bộ entity nếu client chỉ được sửa một số trường.
- Nếu trả lại form sau lỗi, nhớ cung cấp lại categories và action để form vẫn hoạt động.
Ghi nhớ
GET chuẩn bị biểu mẫu, POST bind dữ liệu vào một đối tượng đầu vào giới hạn; Service kiểm tra nghiệp vụ và lưu thay đổi.

