@Query, @Modifying và Projection: Tùy Biến Truy Vấn Dữ Liệu
Làm chủ @Query JPQL và Native SQL, tránh bẫy stale entity với @Modifying clearAutomatically và tối ưu hiệu năng đọc bằng DTO constructor projection.
TL;DR: Khi Derived Query không đủ đáp ứng, sử dụng @Query để viết JPQL hoặc Native SQL trực tiếp. Với thao tác UPDATE/DELETE hàng loạt (bulk DML), bắt buộc khai báo @Modifying kết hợp @Transactional; lưu ý dùng clearAutomatically = true để tránh dữ liệu trong persistence context bị lỗi thời (stale). Để tối ưu hiệu năng đọc, dùng DTO Constructor Expression thay vì Interface Projection để database chỉ SELECT đúng các cột cần thiết.
Khi nào nên sử dụng @Query thay vì Derived Query?
Derived Query tiện lợi cho 1–3 điều kiện đơn giản nhưng bộc lộ giới hạn rõ rệt trong các kịch bản sau:
| Trường hợp | Hạn chế của Derived Query | Giải pháp với @Query |
|---|---|---|
| Nhiều điều kiện | Tên method quá dài (trên 4-5 thuộc tính) | Viết JPQL nhiều dòng rõ ràng, dễ bảo trì. |
| Hàm tổng hợp | Không hỗ trợ COUNT, SUM, AVG, GROUP BY | Biểu đạt trực tiếp trong mệnh đề SELECT. |
| JOIN phức tạp | Khó điều hướng association nhiều cấp | Khai báo JOIN hoặc LEFT JOIN FETCH tường minh. |
| Tính năng DB riêng | Không hỗ trợ cú pháp đặc thù | Bật nativeQuery = true (Postgres JSONB, Full-text). |
public interface ProjectRepository extends JpaRepository<Project, Long> {
// JPQL voi Named Parameter
@Query("SELECT p FROM Project p WHERE p.status = :status AND p.createdAt > :since")
List<Project> findRecent(@Param("status") ProjectStatus status, @Param("since") Instant since);
// JPQL voi Aggregate va Group By
@Query("SELECT p.status, COUNT(p) FROM Project p GROUP BY p.status")
List<Object[]> countGroupByStatus();
// Native SQL cho PostgreSQL Full-Text Search
@Query(value = """
SELECT * FROM projects
WHERE search_vector @@ to_tsquery('english', :query)
ORDER BY ts_rank(search_vector, to_tsquery(:query)) DESC
""", nativeQuery = true)
List<Project> fullTextSearch(@Param("query") String query);
}
Tại sao thao tác Bulk Update cần @Modifying và clearAutomatically?
Mặc định, @Query được Spring Data hiểu là câu lệnh truy vấn đọc (SELECT). Khi thực hiện các thao tác DML như UPDATE hoặc DELETE, bạn bắt buộc phải đánh dấu @Modifying và bọc trong @Transactional.
Hiểm họa dữ liệu từ Bulk Update bỏ qua Persistence Context
Câu lệnh @Modifying UPDATE được thực thi trực tiếp xuống cơ sở dữ liệu qua JDBC statement. Do đó, nó bỏ qua hoàn toàn persistence context (L1 Cache):
- Bạn nạp Entity A vào bộ nhớ qua
findById(trạng thái Managed,status = ACTIVE). - Thực hiện
@ModifyingUPDATE đổi toàn bộstatusthànhARCHIVEDtrong DB. - Entity A trong bộ nhớ vẫn giữ nguyên
status = ACTIVE(bị stale). - Bạn vô tình gọi
a.setName("New Name"), kích hoạt Dirty Checking. - Khi commit transaction, Hibernate flush Entity A xuống database và vô tình ghi đè ngược lại trạng thái cũ
status = ACTIVE!

Khắc phục triệt để bằng clearAutomatically = true
@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Project p SET p.status = :newStatus WHERE p.status = :oldStatus")
int bulkUpdateStatus(@Param("oldStatus") ProjectStatus oldStatus,
@Param("newStatus") ProjectStatus newStatus);
Tham số clearAutomatically = true chỉ thị cho Hibernate xóa trắng (clear) toàn bộ persistence context ngay sau khi câu lệnh bulk update chạy xong, ép buộc các lệnh truy vấn kế tiếp phải nạp lại dữ liệu mới nhất từ database.
Tối ưu hóa hiệu năng đọc bằng Projection như thế nào?
Khi hiển thị danh sách gồm 50 dự án, UI chỉ cần 3 thông tin (id, name, status). Nếu tải toàn bộ Entity chứa 20 cột (gồm văn bản dài và metadata), hệ thống lãng phí tài nguyên mạng, RAM và CPU mapping.
So sánh Interface Projection và DTO Constructor Expression

- Interface Projection: Khai báo interface với các getter tương ứng. Tuy tiện lợi nhưng Hibernate vẫn SELECT toàn bộ các cột rồi mới dùng Proxy để lọc ở tầng ứng dụng Java. Không tối ưu được lưu lượng SQL.
- DTO Constructor Expression (Khuyến cáo chuẩn mực):
Sử dụng Java Record kết hợp cú pháp
SELECT new com.pkg.Dto(...)trong JPQL:
// DTO Record bat bien
public record ProjectSummary(Long id, String name, ProjectStatus status) {}
// Repository Query
public interface ProjectRepository extends JpaRepository<Project, Long> {
@Query("""
SELECT new com.olhub.dto.ProjectSummary(p.id, p.name, p.status)
FROM Project p
WHERE p.status = :status
""")
List<ProjectSummary> findSummaryByStatus(@Param("status") ProjectStatus status);
}
Câu lệnh SQL thực tế sinh ra:
SELECT p.id, p.name, p.status FROM projects p WHERE p.status = ?
Database chỉ quét đúng 3 cột, loại bỏ 100% dữ liệu dư thừa ngay từ tầng lưu trữ.
Bảng so sánh các kỹ thuật truy vấn dữ liệu
| Tiêu chí | Derived Query | @Query JPQL | Native SQL | DTO Projection |
|---|---|---|---|---|
| Cú pháp | Tên method Java | Chuỗi JPQL hướng đối tượng | Cú pháp SQL thuần của DB | JPQL SELECT new |
| Tính khả chuyển | Hoàn toàn độc lập DB | Độc lập DB qua Dialect | Bị khóa chặt vào vendor DB | Độc lập DB |
| Kiểm tra lúc Startup | Có (Fail-Fast) | Có (Validate cú pháp) | Không (Chỉ lỗi ở runtime) | Có |
| Tối ưu số cột SELECT | Không (Load full Entity) | Không (Trừ khi dùng DTO) | Tùy câu lệnh SQL viết | ✅ Tối ưu tối đa |
Những cạm bẫy phổ biến cần tránh là gì?
- Quên @Modifying khi viết UPDATE/DELETE: Ứng dụng sẽ ném lỗi runtime
Not supported for DML operations. - Quên clearAutomatically trên Bulk Update: Dẫn tới hiện tượng stale entity ghi đè ngược dữ liệu đã cập nhật.
- Nhầm lẫn giữa cú pháp JPQL và SQL: Viết
SELECT * FROM table_namethay vìSELECT e FROM Entity etrong@Querykhông cónativeQuery = true. - Lạm dụng Native Query cho các truy vấn đơn giản: Làm mất tính độc lập cơ sở dữ liệu và mất cơ chế kiểm tra cú pháp lúc startup của Hibernate.
Tự kiểm tra kiến thức
- Q1Tại sao phương thức repository thực hiện bulk UPDATE qua @Query bắt buộc phải có annotation @Modifying?
- Q2Cơ chế nào giải thích tại sao câu lệnh bulk UPDATE trong cùng một transaction lại có thể bị ghi đè ngược bởi Entity cũ?
- Q3Giải pháp tối ưu nào giúp giải quyết triệt để vấn đề stale data sau khi chạy @Modifying bulk update trong cùng transaction?
- Q4Điểm vượt trội của DTO Constructor Expression so với Interface Projection ở tầng cơ sở dữ liệu là gì?
- Q5Tại sao nên ưu tiên sử dụng Named Parameter (:paramName) thay vì Positional Parameter (?1) trong các câu lệnh @Query?
Bài tiếp theo: Specification & Auditing: Truy Vấn Động và Nhật Ký Thực Thể
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
JpaRepository và Derived Queries: Truy Vấn Không Cần Viết Code
Bài tiếpSpecification và JPA Auditing: Truy Vấn Động và Nhật Ký
Lộ trình Junior Java Backend Developer
Chặng 16/21 · Spring Data JPA — bài 10/20 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này