Backend

99 Ngày Spring — Ngày 24: Query methods & @Query

SSite Admin
20 tháng 08, 2026 7 phút đọc 0 lượt xem
99 Ngày Spring — Ngày 24: Query methods & @Query

Ngày 23 vẽ ra ranh giới của derived query: tên method dài thành câu văn là tín hiệu dừng. Hôm nay ta bước qua ranh giới đó với @Query — nơi truy vấn được viết tường minh thay vì sinh từ tên. Ba mảnh ghép của bài: JPQL truy vấn trên entity với lưới an toàn fail-fast, bind tham số đúng cách (và vì sao nó là hàng rào chống SQL injection), rồi native query cùng @Modifying cho những việc JPQL không với tới.

Sketchnote Ngày 24: Query methods và @Query — JPQL truy vấn trên entity, bind tham số theo tên với @Param, native query kèm đánh đổi, @Modifying cho bulk update

@Query với JPQL — truy vấn trên entity

public interface PostRepository extends JpaRepository<Post, Long> {

    // Derived query hôm qua chạm trần — @Query nhận truy vấn TƯỜNG MINH:
    @Query("""
        SELECT p FROM Post p
        WHERE p.status = :status
          AND p.publishedAt <= :now
        ORDER BY p.publishedAt DESC
        """)
    List<Post> timDaXuatBan(@Param("status") PostStatus status,
                            @Param("now") LocalDateTime now);
}

// JPQL truy vấn trên ENTITY, không phải bảng:
//   Post           = tên class            (không phải bảng posts)
//   p.publishedAt  = tên field trong Java (không phải cột published_at)
// Đổi tên field mà quên sửa @Query? FAIL NGAY KHI KHỞI ĐỘNG —
// cùng lưới an toàn fail-fast của derived query (Ngày 23)
  • JPQL nhìn thế giới qua entity và field Java — không phải bảng và cột: Hibernate dịch sang SQL của dialect đang dùng, nên cùng một câu JPQL chạy trên PostgreSQL lẫn MySQL không đổi một ký tự.

  • Truy vấn được phân tích ngay lúc khởi động — sai tên field, sai cú pháp là ứng dụng từ chối chạy: cùng triết lý fail-fast của derived query (Ngày 23) và ddl-auto: validate (Ngày 21), lỗi lộ sớm nhất có thể.

  • Text block (series Java, Ngày 21) là bạn đồng hành tự nhiên của @Query: truy vấn nhiều dòng đọc như SQL thật, không còn chuỗi + nối lê thê.

Bind tham số — vị trí, tên, và hàng rào injection

// Hai cách bind tham số vào truy vấn:

// 1. Theo VỊ TRÍ — ?1, ?2: ngắn, nhưng đổi thứ tự tham số là vỡ ngầm
@Query("SELECT p FROM Post p WHERE p.slug = ?1")
Optional<Post> timTheoSlug(String slug);

// 2. Theo TÊN — :slug + @Param: tự tài liệu, an toàn khi refactor (khuyến nghị)
@Query("SELECT p FROM Post p WHERE p.slug = :slug")
Optional<Post> timTheoSlug(@Param("slug") String slug);

// LIKE: Spring Data cho phép bọc wildcard quanh tham số ngay trong JPQL
@Query("SELECT p FROM Post p WHERE p.title LIKE %:tuKhoa%")
List<Post> timTheoTieuDe(@Param("tuKhoa") String tuKhoa);

// TUYỆT ĐỐI không cộng chuỗi thành truy vấn ("... WHERE slug = '" + s + "'")
// Bind tham số là hàng rào chống SQL INJECTION:
// giá trị đi đường riêng xuống driver, không bao giờ trộn vào câu lệnh
  • Bind theo tên thắng bind theo vị trí ở khả năng bảo trì: thêm một tham số vào giữa danh sách không làm xô lệch các :ten đã có, còn ?1/?2 thì âm thầm trỏ nhầm chỗ.

  • Từ Java 21 trở đi tên tham số method có thể được giữ trong bytecode (cờ -parameters — Spring Boot bật sẵn qua plugin), khi đó @Param có thể lược bớt; viết tường minh vẫn là thói quen an toàn khi build script thay đổi.

  • Điểm không thương lượng: giá trị người dùng chỉ đi qua bind — cộng chuỗi thành truy vấn là mở cửa cho SQL injection, lỗ hổng bảo mật kinh điển nhất của tầng dữ liệu; bind đưa giá trị xuống driver theo kênh riêng, không bao giờ trộn vào câu lệnh.

Kính lúp đặt trên bàn phím laptop — @Query là chiếc kính lúp tự mài: mô tả chính xác thứ cần tìm bằng JPQL thay vì để tên method nói hộ

Native query — khi cần SQL thật

// Native query — SQL thật của PostgreSQL, khi JPQL không với tới:
@Query(value = """
    SELECT * FROM posts
    WHERE to_tsvector('simple', title) @@ plainto_tsquery(:tuKhoa)
    """, nativeQuery = true)
List<Post> timFullText(@Param("tuKhoa") String tuKhoa);

// Phân trang với native query cần countQuery riêng:
@Query(value = "SELECT * FROM posts WHERE status = :st",
       countQuery = "SELECT count(*) FROM posts WHERE status = :st",
       nativeQuery = true)
Page<Post> timTheoTrangThai(@Param("st") String st, Pageable pageable);

// Đánh đổi khi rời JPQL:
//   ✗ mất tính di động — SQL trên là của riêng PostgreSQL
//   ✗ tên BẢNG/CỘT thật — refactor entity không còn được kiểm chứng
// → JPQL trước; native chỉ khi cần tính năng riêng của DB
  • Use case chính đáng: tính năng riêng của DB — full-text search, window function, CTE đệ quy — những thứ JPQL cố tình không trừu tượng hóa; lúc đó nativeQuery = true là cánh cửa chính thống.

  • Cái giá phải trả: câu SQL nói chuyện với bảng và cột thật, nên đổi tên entity/field không còn được kiểm chứng lúc khởi động — hợp đồng với DB chuyển từ compiler sang kỷ luật của đội.

  • Phân trang là điểm khác biệt dễ quên: JPQL để Spring Data tự sinh câu đếm, native cần countQuery riêng — thiếu nó Page không biết tổng số phần tử.

@Modifying — UPDATE và DELETE hàng loạt

// @Query mặc định chỉ SELECT — UPDATE/DELETE cần thêm @Modifying:
@Modifying
@Transactional
@Query("UPDATE Post p SET p.viewCount = p.viewCount + 1 WHERE p.id = :id")
int tangLuotXem(@Param("id") Long id);     // trả về SỐ DÒNG bị ảnh hưởng

// Bulk update đi THẲNG xuống DB — bỏ qua persistence context (Ngày 21):
// entity đang managed trong context KHÔNG biết giá trị vừa đổi.
// @Modifying(clearAutomatically = true) xóa context sau khi chạy,
// để lần đọc tiếp theo buộc phải lấy dữ liệu mới từ DB.

// So với vòng lặp findAll → sửa từng entity → save:
// một câu UPDATE cho N dòng thay vì N+1 câu lệnh — nhanh hơn hàng chục lần.
// (@Transactional ở đây là mức tối thiểu cho demo — Ngày 28 dành trọn cho transaction)
  • @Query mặc định là truy vấn đọc — thiếu @Modifying thì câu UPDATE/DELETE ném lỗi ngay: một ràng buộc tường minh giúp method đọc và method ghi không lẫn vào nhau.

  • Bulk update đi vòng qua persistence context: nhanh hơn vòng lặp save từng entity hàng chục lần, nhưng entity đang managed trở nên clearAutomatically = true là công tắc đồng bộ lại, đánh đổi bằng việc context phải nạp lại từ đầu.

  • Giá trị trả về int — số dòng bị ảnh hưởng — là công cụ kiểm chứng miễn phí: cập nhật "một bản ghi" mà nhận về 0 hay 2 là tín hiệu điều kiện WHERE có vấn đề.

Bài tập nhỏ

  • Viết lại method 80 ký tự của Ngày 23 (findByStatusAndCategorySlug...) thành @Query JPQL với bind theo tên — so hai phiên bản về khả năng đọc.

  • Cố tình gõ sai tên field trong JPQL (p.titl) — đọc thông báo lỗi lúc khởi động và so với lỗi derived query hôm qua.

  • Viết tangLuotXem với @Modifying — gọi hai lần liên tiếp, kiểm chứng viewCount tăng 2 và giá trị trả về là 1 mỗi lần.

  • Bật show-sql (Ngày 22), chạy một native query phân trang — đối chiếu câu SQL và countQuery thực tế được gửi xuống DB.

Kết luận

Bức tranh truy vấn của Spring Data giờ đã đủ ba tầng: derived query cho phần lớn nhu cầu lọc-sắp xếp, @Query JPQL khi tên method hết khả năng diễn đạt — vẫn giữ lưới fail-fast và tính di động, native query + @Modifying cho tính năng riêng của DB và bulk update — kèm những đánh đổi phải trả bằng kỷ luật. Nguyên tắc xuyên suốt: giá trị người dùng chỉ đi qua bind tham số. Ngày 25 ta rời truy vấn đơn bảng để nối các entity lại với nhau: quan hệ 1-1 và 1-N với @ManyToOne/@OneToMany — và khái niệm owning side quyết định ai giữ khóa ngoại. Hẹn gặp lại!

S

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.

Bài viết liên quan

99 Ngày Java — Ngày 24: equals, hashCode & toString

Ngày 24 của 99 Ngày Java: bộ ba contract của Object — equals chuyển "bằng nhau" từ tham chiếu sang định danh nghiệp vụ với contract 5 điều, quy tắc vàng equals đi cùng hashCode, hai bước tra cứu hash-rồi-equals của HashMap cùng bẫy key bị sửa, và Objects utility + record giúp cài chuẩn ít dòng nhất.

20 thg 8, 20267 phút0
Garbage Collection trong Java: reachability, heap generations và vì sao vẫn memory leak

GC tự động nhưng không "miễn nghĩ": tiêu chí duy nhất của nó là reachability, heap chia thế hệ vì đa số object chết trẻ, stop-the-world pause là cái giá của việc dọn dẹp, và memory leak trong Java nghĩa là object vẫn reachable ngoài ý muốn — kèm 4 thủ phạm leak phổ biến và checklist vận hành GC trên production.

20 thg 8, 20267 phút2
99 Ngày Spring — Ngày 23: Spring Data JPA repository

Ngày 23 của 99 Ngày Spring: Spring Data JPA repository — JpaRepository với bộ CRUD có sẵn trên một interface trống, phả hệ Repository → CrudRepository → JpaRepository và cơ chế proxy trên SimpleJpaRepository, derived query sinh truy vấn từ tên method với lưới fail-fast lúc khởi động.

19 thg 8, 20266 phút12