Spring Boot bài 14: Quan hệ JPA, DTO và tránh truy vấn N+1
- 18-10-2026
- Toanngo92
- 0 Comments
Giáo trình Spring Boot MVC và REST API · Chương 6/12: JPA, MySQL và transaction · Bài 14/28 · Biên soạn tháng 10/2026.
Mục lục
Mục tiêu bài học
Ánh xạ nhiều khóa học thuộc một danh mục, chuyển entity sang DTO trong transaction và hiểu vấn đề N+1.
Quan hệ có hướng phục vụ nhu cầu hiện tại
Mỗi Course thuộc một Category nên Course có @ManyToOne và category_id. Bản mẫu không thêm List<Course> ngược vào Category vì chưa có nhu cầu truy cập qua chiều đó. Quan hệ một chiều giúp người mới tập trung vào khóa ngoại và cách tải dữ liệu; không phải quan hệ nào cũng cần hai collection tham chiếu lẫn nhau.
FetchType.LAZY cho biết dữ liệu danh mục có thể được tải trễ. Với quan hệ to-one, hãy chú ý khả năng thực tế của provider và kiểm tra SQL. Cascade không được bật tùy tiện: xóa một khóa học không nên kéo theo xóa danh mục đang được nhiều khóa học dùng.
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "category_id")
private Category category;
@Version
private Long version;
DTO giữ response có phạm vi rõ ràng
Nếu trả thẳng entity qua JSON, serializer có thể đi vào quan hệ lazy, tạo truy vấn ngoài dự kiến hoặc lặp vô hạn với quan hệ hai chiều. Entity cũng có thể chứa trường không muốn công khai. CourseDto chỉ gồm dữ liệu cần hiển thị và định nghĩa hợp đồng ổn định hơn cho cả HTML và API.
Chuyển đổi trong CourseService khi transaction còn mở giúp categoryName được lấy đúng lúc. spring.jpa.open-in-view=false buộc ta chuẩn bị dữ liệu ở tầng nghiệp vụ, tránh để template hoặc serializer vô tình tiếp tục truy vấn sau đó. Nếu gặp LazyInitializationException, xem lại thời điểm mapping và cách fetch, không bật lại mọi cơ chế lazy theo thói quen.
Tệp trong mã nguồn mẫu: src/main/java/com/hocvietcode/academy/CourseDto.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 java.math.BigDecimal;
public record CourseDto(Long id,String title,BigDecimal price,Long categoryId,String categoryName) {
public static CourseDto from(Course c){return new CourseDto(c.getId(),c.getTitle(),c.getPrice(),c.getCategory().getId(),c.getCategory().getName());}
}
N+1 xuất hiện như thế nào?
Một truy vấn lấy N khóa học có thể kéo theo N truy vấn nữa khi đọc tên danh mục từng dòng. Với danh sách lớn, chi phí tăng dù code vòng lặp trông đơn giản. CourseRepository dùng @EntityGraph(attributePaths="category") trên phương thức tìm kiếm để tải quan hệ cần thiết cho trang dữ liệu.
Đây là lựa chọn có mục tiêu cho quan hệ to-one. Không nên chuyển mọi quan hệ thành EAGER hoặc fetch join bừa mọi collection, nhất là khi phân trang. Cần kiểm tra SQL và số truy vấn trong bối cảnh dữ liệu thực tế, vì một annotation không tự tối ưu mọi trường hợp.
Tệp trong mã nguồn mẫu: src/main/java/com/hocvietcode/academy/CourseRepository.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.
public interface CourseRepository extends JpaRepository<Course,Long> {
@EntityGraph(attributePaths="category")
Page<Course> findByTitleContainingIgnoreCase(String title,Pageable pageable);
}
Khóa ngoại và version bảo vệ điều gì?
Khóa ngoại giữ cho category_id tham chiếu một danh mục tồn tại. Validation và Service giúp trả thông báo sớm, nhưng DB vẫn cần ràng buộc để bảo vệ tính toàn vẹn trước các luồng ghi khác. @Version hỗ trợ phát hiện thay đổi cạnh tranh ở mức entity/transaction.
Tuy nhiên mẫu update luôn đọc lại bản ghi hiện tại; nó chưa có cơ chế so sánh phiên bản người dùng đã mở trong form hoặc ETag của API. Vì vậy không nên tuyên bố đã giải quyết toàn bộ trường hợp hai người mở form lâu rồi ghi đè nhau. Khi mở rộng, cần mang phiên bản kỳ vọng từ client và xử lý xung đột rõ ràng.
Lỗi thường gặp và lưu ý
- Không đặt CascadeType.ALL cho quan hệ nhiều-một chỉ để lỗi lưu biến mất.
- Không trả entity ra JSON rồi dùng annotation che từng lỗi phát sinh mà chưa xác định hợp đồng dữ liệu.
Ghi nhớ
Chọn quan hệ theo nhu cầu, fetch dữ liệu có chủ đích và mapping DTO trong transaction giúp cả giao diện lẫn API dễ kiểm soát.

