Spring REST API & Data JPA
31/46
Bài 31 / 46~12 phútRepository & QueriesLộ trình · chặng 16/21Miễn phí… lượt xem

@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ợpHạn chế của Derived QueryGiải pháp với @Query
Nhiều điều kiệnTê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ợpKhông hỗ trợ COUNT, SUM, AVG, GROUP BYBiểu đạt trực tiếp trong mệnh đề SELECT.
JOIN phức tạpKhó điều hướng association nhiều cấpKhai báo JOIN hoặc LEFT JOIN FETCH tường minh.
Tính năng DB riêngKhông hỗ trợ cú pháp đặc thùBật nativeQuery = true (Postgres JSONB, Full-text).
Java
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):

  1. Bạn nạp Entity A vào bộ nhớ qua findById (trạng thái Managed, status = ACTIVE).
  2. Thực hiện @Modifying UPDATE đổi toàn bộ status thành ARCHIVED trong DB.
  3. Entity A trong bộ nhớ vẫn giữ nguyên status = ACTIVE (bị stale).
  4. Bạn vô tình gọi a.setName("New Name"), kích hoạt Dirty Checking.
  5. 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!

Trục thời gian một transaction: findById nạp p với status ACTIVE, bulk update chạy thẳng qua JDBC đưa DB row 42 sang ARCHIVED, p vẫn ACTIVE vì persistence context không được báo, setName đánh dấu p dirty, và lúc commit Hibernate flush cả status cũ khiến DB row 42 quay lại ACTIVE

Khắc phục triệt để bằng clearAutomatically = true

Java
@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

Ba hàng đối chiếu cách khai báo với câu SQL thật: trả entity thì SELECT cả 15 cột, interface projection VẪN SELECT cả 15 cột rồi map xong mới bỏ 12 cột thừa, DTO constructor expression thì SELECT đúng 3 cột

  1. 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.
  2. 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:
Java
// 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:

SQL
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 JPQLNative SQLDTO Projection
Cú phápTên method JavaChuỗi JPQL hướng đối tượngCú pháp SQL thuần của DBJPQL SELECT new
Tính khả chuyểnHoàn toàn độc lập DBĐộc lập DB qua DialectBị khóa chặt vào vendor DBĐộc lập DB
Kiểm tra lúc StartupCó (Fail-Fast)Có (Validate cú pháp)Không (Chỉ lỗi ở runtime)Có
Tối ưu số cột SELECTKhô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ì?

  1. Quên @Modifying khi viết UPDATE/DELETE: Ứng dụng sẽ ném lỗi runtime Not supported for DML operations.
  2. 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.
  3. Nhầm lẫn giữa cú pháp JPQL và SQL: Viết SELECT * FROM table_name thay vì SELECT e FROM Entity e trong @Query không có nativeQuery = true.
  4. 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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao phương thức repository thực hiện bulk UPDATE qua @Query bắt buộc phải có annotation @Modifying?
  2. Q2
    Cơ 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ũ?
  3. Q3
    Giả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?
  4. 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ì?
  5. Q5
    Tạ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.

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

Bài tiếp theo

Specification và JPA Auditing: Truy Vấn Động và Nhật Ký