99 Ngày Spring — Ngày 33: Projection — chỉ lấy cột cần
Ngày 31 tối ưu số dòng (phân trang), Ngày 32 tối ưu điều kiện lọc (Specification) — hôm nay tối ưu chiều còn lại: số cột. Màn hình danh sách chỉ hiện 3–4 trường nhưng findAll quen tay kéo cả entity 15 cột kèm mô tả 2000 ký tự về rồi… vứt. Projection của Spring Data đẩy ranh giới DTO (Ngày 30) xuống tận câu SQL: interface projection cho gọn, record projection cho chuẩn, dynamic projection cho linh hoạt — và một bẫy open projection khiến mọi công sức thành "gọn giả".

Vấn đề: SELECT cả entity cho màn hình 3 cột
// Màn hình DANH SÁCH task: bảng chỉ hiện tiêu đề + trạng thái + hạn chót
// Nhưng repository quen tay:
Page<Task> page = repo.findAll(pageable);
// SQL thật (soi log — Ngày 27):
// SELECT id, tieu_de, mo_ta, status, han_chot,
// created_at, updated_at, created_by, updated_by, ...
// → kéo CẢ mo_ta 2000 ký tự × 20 dòng — màn list không bao giờ hiển thị!
// Cái giá của "SELECT cả entity":
// 1. Băng thông + RAM: cột to (mô tả, JSON, blob) đi theo mỗi dòng
// 2. Hibernate phải dựng entity ĐẦY ĐỦ + theo dõi dirty checking (Ngày 25)
// — cho dữ liệu CHỈ ĐỌC rồi vứt
// 3. Entity kéo theo quan hệ LAZY — chạm nhầm một getter trong vòng lặp
// là N+1 nổ ngay giữa màn list (Ngày 27)
// Ngày 30 đã tách DTO khỏi entity Ở TẦNG API.
// Hôm nay đẩy ranh giới xuống TẬN SQL: chỉ SELECT đúng cột màn hình cần.Ba khoản phí của entity đầy đủ cho dữ liệu chỉ-đọc: cột to đi theo mỗi dòng (mô tả, JSON), chi phí dựng entity + dirty checking (Ngày 25) cho thứ sẽ vứt ngay, và quan hệ lazy chờ nổ N+1 (Ngày 27) khi ai đó chạm nhầm getter trong vòng lặp.
Ngày 30 đã có nguyên tắc "entity không rời service" — nhưng đó là ranh giới ở tầng API: DB vẫn trả đủ cột. Projection kéo ranh giới xuống tầng SQL — đúng nghĩa chỉ lấy thứ cần.
Quy tắc chẩn đoán quen thuộc: soi SQL log (Ngày 27) — thấy
SELECTdài dằng dặc cho màn list là thấy chỗ tối ưu; đo trước, tin sau.
Interface projection — đổi mỗi kiểu trả về
// Interface projection — khai báo "tôi chỉ cần các cột này"
public interface TaskSummary {
Long getId();
String getTieuDe();
TaskStatus getStatus();
LocalDate getHanChot();
}
// Repository: đổi KIỂU TRẢ VỀ — Spring Data lo phần còn lại
public interface TaskRepository extends JpaRepository<Task, Long> {
Page<TaskSummary> findByStatus(TaskStatus status, Pageable pageable);
List<TaskSummary> findByHanChotBefore(LocalDate moc);
}
// SQL sinh ra (soi log): SELECT id, tieu_de, status, han_chot FROM tasks ...
// → đúng 4 cột — mo_ta 2000 ký tự Ở YÊN dưới DB ✅
// Quy tắc khớp tên: getTieuDe() ↔ thuộc tính tieuDe của entity
// Sai tên là lỗi NGAY LÚC KHỞI ĐỘNG — dễ bắt hơn lỗi runtime lẻ tẻ
// Cùng interface dùng được nhiều query — "hợp đồng cột" tái sử dụng:
Page<TaskSummary> timKiem(Specification<Task> spec, Pageable pageable); // ❌ KHÔNG chạy
// Lưu ý: findAll(Specification) KHÔNG hỗ trợ projection trực tiếp (bẫy phổ biến!)
// → với Specification: dùng findBy(spec, q -> q.as(TaskSummary.class).page(pageable))Cơ chế gọn kỳ lạ: khai báo interface toàn getter, đổi kiểu trả về của method repository — Spring Data tự sinh
SELECTđúng các cột khớp tên và tạo proxy đằng sau; sai tên thuộc tính vỡ ngay lúc khởi động, không âm thầm.Một interface là một "hợp đồng cột" tái sử dụng cho nhiều query —
TaskSummarydùng cho list, cho search, cho export; ba màn cùng một định nghĩa cột.Bẫy phổ biến nhất của chương:
findAll(Specification)không nhận projection trực tiếp — kết hợp Specification (Ngày 32) phải đi qua fluent APIfindBy(spec, q -> q.as(TaskSummary.class)); biết trước đỡ mất một buổi chiều.
Record projection — DTO thật sự từ tầng SQL
// Class/record projection — DTO thật sự, bất biến (Ngày 29 vẫy tay)
public record TaskRow(Long id, String tieuDe, TaskStatus status, LocalDate hanChot) { }
// Cách 1 — constructor expression trong JPQL:
@Query("""
select new vn.motdev.blog.task.dto.TaskRow(
t.id, t.tieuDe, t.status, t.hanChot)
from Task t
where t.status = :status
""")
List<TaskRow> danhSachTheoTrangThai(TaskStatus status);
// Cách 2 — derived query cũng nhận record (Spring Data khớp theo TÊN component):
List<TaskRow> findByStatusOrderByHanChotAsc(TaskStatus status);
// Interface vs record — chọn thế nào?
// Interface Record
// Khai báo gọn, chỉ getter 1 dòng record
// Bản chất proxy Spring tạo object thật, bất biến
// JPQL tường minh không cần new ...() dài dòng
// Dùng ngoài tầng data ổn TỐT — DTO chuẩn chỉnh
// Nested (task.nguoiTao) hỗ trợ (cẩn thận N+1) tự map thủ công
// Thực dụng: interface cho nhanh gọn nội bộ — record khi DTO đi xa (API, cache)Record (Ngày 20, 29) làm projection là cú bắt tay đẹp: bất biến,
equals/hashCodechuẩn, và là object thật — không phải proxy — nên tự tin đem đi xa: trả API, bỏ cache, ghi log.Hai đường sinh: constructor expression
select new ...()trong JPQL (tường minh, dài) hoặc derived query khớp theo tên component (gọn, đủ dùng cho đa số).Chọn thực dụng: interface projection cho đọc nhanh nội bộ tầng data; record khi DTO vượt ranh giới — cùng triết lý DTO của checkpoint Ngày 30, giờ sinh thẳng từ SQL.
Dynamic projection và bẫy open projection
// Dynamic projection — MỘT method, nhiều hình dạng kết quả
public interface TaskRepository extends JpaRepository<Task, Long> {
<T> List<T> findByStatus(TaskStatus status, Class<T> type);
}
List<TaskSummary> gon = repo.findByStatus(TODO, TaskSummary.class); // 4 cột
List<TaskRow> row = repo.findByStatus(TODO, TaskRow.class); // record
List<Task> day = repo.findByStatus(TODO, Task.class); // cả entity (khi cần sửa)
// Ba lớp dùng cùng một query — màn list lấy gọn, màn edit lấy đủ.
// Kết hợp đồ nghề các ngày trước:
// - Pageable (Ngày 18, 31): Page<TaskSummary> — phân trang trên cột gọn ✅
// - Specification (Ngày 32): qua fluent API findBy(spec, q -> q.as(...))
// - @EntityGraph: KHÔNG cần nữa cho màn list — không entity, không lazy, không N+1
// Bẫy cuối — OPEN projection với @Value/SpEL:
public interface TaskBad {
@Value("#{target.tieuDe + ' (' + target.status + ')'}")
String getLabel(); // ❌ SpEL cần "target" = CẢ entity
}
// → Hibernate buộc phải SELECT đủ cột — projection "giả": nhìn gọn, chạy phí như cũ
// Format chuỗi để tầng service/DTO lo — đừng nhét vào projectionDynamic projection — thêm tham số
Class<T>— cho một method phục vụ cả màn list (gọn) lẫn màn edit (đủ entity): bớt trùng lặp method mà vẫn giữ từng màn đúng khẩu phần.Phần thưởng kín đáo cho màn list: không entity thì không lazy — nguy cơ N+1 (Ngày 27) biến mất theo định nghĩa, khỏi cần
@EntityGraphcho đường đọc này.Bẫy open projection: getter gắn
@Value/SpEL cầntargetlà cả entity → Hibernate buộc SELECT đủ cột — nhìn như projection, chạy phí nhưfindAll; format chuỗi là việc của service/DTO, đừng nhét vào hợp đồng cột.
Checklist dùng projection
Màn danh sách / export / báo cáo (đọc rồi vứt) → projection; form edit (đọc để sửa) → entity đầy đủ + dirty checking.
Soi SQL log xác nhận
SELECTchỉ còn cột khai báo — projection mà log vẫn SELECT đủ cột là dính open projection hoặc nested kéo quan hệ.Nested projection (
getNguoiTao().getUsername()) tiện nhưng dễ kéo JOIN/entity con — màn list lớn thì flatten thành cột phẳng (nguoiTaoUsername) cho chắc.Projection là đọc: không dirty checking, không save ngược — cần sửa thì tải entity; đúng phân vai với
@Transactional(readOnly = true)(Ngày 28, 30).Kết hợp:
Page<TaskSummary>+Pageablechạy thẳng; với Specification dùngfindBy(spec, q -> q.as(...)).
Bài tập nhỏ
Thêm
TaskSummary(interface) cho checkpoint Ngày 30, đổi endpoint list sangPage<TaskSummary>— soi SQL log trước/sau và đếm cột.Tạo
record TaskRow+ constructor expression; so sánh JSON trả về với bản interface — có khác gì không, vì sao?Viết dynamic projection
findByStatus(status, Class<T>)và dùng ở 2 chỗ: list (Summary) và edit (Task).Cố tình tạo open projection với
@Value— soi log chứng minh SELECT đủ cột quay lại, rồi chuyển format sang DTO.Kết hợp Ngày 32: endpoint search dùng
findBy(spec, q -> q.as(TaskSummary.class).page(pageable))— xác nhận WHERE động + SELECT gọn cùng lúc.
Kết luận
Bốn ý gói bài: entity đầy đủ cho dữ liệu chỉ-đọc là lãng phí ba tầng — cột thừa, dựng object + dirty checking, và lazy chờ N+1; interface projection chỉ cần đổi kiểu trả về — hợp đồng cột khớp tên, vỡ sớm lúc khởi động; record projection cho DTO bất biến đi xa được; dynamic projection một method nhiều khẩu phần — và tránh open projection kẻo gọn giả phí thật. Ngày 34 ta rời tầng query đến tầng schema: Flyway migration — vì sao ddl-auto là thảm họa production và cách version hóa schema như version hóa code. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


