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.

@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ệnhBind 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/?2thì â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 đó@Paramcó 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.
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 DBUse 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 = truelà 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
countQueryriêng — thiếu nóPagekhô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)@Querymặc định là truy vấn đọc — thiếu@Modifyingthì 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 cũ —
clearAutomatically = truelà 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@QueryJPQL 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
tangLuotXemvới@Modifying— gọi hai lần liên tiếp, kiểm chứngviewCounttă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àcountQuerythự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!
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.


