99 Ngày Spring — Ngày 23: Spring Data JPA repository
Entity đã đứng vững (Ngày 22) — hôm nay ta trao nó cho Spring Data JPA repository. Lời hứa của Spring Data nghe gần như phi lý: khai báo một interface trống, nhận về trọn bộ CRUD; đặt tên method theo quy ước, nhận về truy vấn hoàn chỉnh. Bài hôm nay tháo rời lời hứa đó thành ba phần có thể kiểm chứng: bộ CRUD của JpaRepository, cơ chế proxy đứng sau, và derived query — kèm ranh giới thực dụng để biết lúc nào nên dừng.

JpaRepository — interface không cần cài đặt
// Entity Post đã có (Ngày 22) — bước tiếp: khai báo MỘT INTERFACE:
public interface PostRepository extends JpaRepository<Post, Long> { }
// kiểu entity ↗ ↖ kiểu của @Id
// Không viết một dòng cài đặt nào — Spring Data sinh implementation
// lúc khởi động. Inject và dùng ngay bộ CRUD có sẵn:
@Service
public class PostService {
private final PostRepository repo; // constructor injection (Ngày 04)
public PostService(PostRepository repo) { this.repo = repo; }
public Post luu(Post p) { return repo.save(p); } // INSERT / UPDATE
public Optional<Post> tim(Long id) { return repo.findById(id); }
public List<Post> tatCa() { return repo.findAll(); }
public boolean tonTai(Long id) { return repo.existsById(id); }
public long dem() { return repo.count(); }
public void xoa(Long id) { repo.deleteById(id); }
}
// findById trả Optional — buộc người gọi xử lý "không thấy" tường minhHai tham số generic nói đủ mọi điều Spring Data cần: kiểu entity và kiểu khóa chính — từ đó
save/findById/findAll/deleteById/count/existsByIdsẵn sàng không cần một dòng SQL.findByIdtrảOptional(series Java đào sâu ở Ngày 58) — hợp đồng tường minh về khả năng "không thấy", thay cho quy ước trảnulldễ quên kiểm tra.So sánh với tầng DAO viết tay thời JDBC: mỗi entity từng tốn một lớp cài đặt đầy boilerplate — giờ là một interface một dòng; sức lực dành cho truy vấn thật sự đặc thù.
Phả hệ repository & cơ chế proxy
// Phả hệ interface — mỗi tầng thêm một nhóm khả năng:
Repository<T, ID> // marker — đánh dấu cho Spring Data quét
└─ CrudRepository // save / findById / delete / count ...
└─ ListCrudRepository // trả List thay Iterable (Spring Data 3)
└─ PagingAndSortingRepository // findAll(Pageable) — đã gặp ở Ngày 18
└─ JpaRepository // + flush, deleteAllInBatch,
// getReferenceById (riêng JPA)
// Cơ chế: lúc khởi động, Spring Data quét các interface kế thừa
// Repository và tạo PROXY dựa trên lớp SimpleJpaRepository —
// bên trong vẫn là EntityManager của Ngày 21:
// save(entity) → em.persist(...) hoặc em.merge(...)
// findById(id) → em.find(Post.class, id)
// → repository không phải phép màu — chỉ là code bạn không phải viếtChọn interface nào để extends là chọn diện tích API:
JpaRepositorytiện nhất cho ứng dụng JPA thông thường; các dự án muốn thu hẹp bề mặt (chỉ đọc, không xóa) extends thẳngRepositoryvà tự khai báo đúng method cho phép.Proxy dựng trên
SimpleJpaRepository— mọi lời gọi cuối cùng vẫn đi quaEntityManagervà persistence context của Ngày 21: kiến thức nền không bị thay thế, chỉ được đóng gói.Hệ quả thực tế đáng nhớ:
savetrên entity đang managed là thừa — dirty checking đã tự phát hiện thay đổi khi commit (Ngày 21); hiểu tầng dưới giúp dùng tầng trên đúng cách.
Derived query — truy vấn sinh từ tên method
public interface PostRepository extends JpaRepository<Post, Long> {
// Spring Data PHÂN TÍCH TÊN METHOD và sinh truy vấn tương ứng:
Optional<Post> findBySlug(String slug);
// → SELECT p FROM Post p WHERE p.slug = ?1
List<Post> findByStatusOrderByPublishedAtDesc(PostStatus status);
// → WHERE p.status = ?1 ORDER BY p.publishedAt DESC
// (status là enum — bài Java hôm nay!)
List<Post> findByTitleContainingIgnoreCase(String tuKhoa);
// → WHERE UPPER(p.title) LIKE UPPER('%'||?1||'%')
boolean existsBySlug(String slug); // truy vấn EXISTS gọn
long countByStatus(PostStatus status); // COUNT(*)
List<Post> findTop5ByOrderByViewCountDesc(); // LIMIT 5
}
// Gõ sai tên field — findByTittle — ứng dụng FAIL NGAY KHI KHỞI ĐỘNG:
// truy vấn được kiểm chứng sớm, không đợi đến lần gọi đầu tiênQuy tắc đọc tên:
findBy+ tên field (viết hoa chữ đầu) + toán tử tùy chọn — Spring Data tách chuỗi đó thành cây điều kiện rồi sinh JPQL;existsBy/countBysinh dạng truy vấn tối ưu hơn hẳnfindByrồi đếm tay.Tham số enum (
PostStatus) khớp thẳng vào điều kiện — trạng thái bài viết là use case enum kinh điển của bài Java hôm nay; ánh xạ cột bằng@Enumerated(EnumType.STRING)chờ ở Ngày 39.Lỗi gõ sai tên field nổ ngay khi khởi động, không đợi lần gọi đầu — cùng triết lý fail-fast của
ddl-auto: validate(Ngày 21): hợp đồng với DB được kiểm chứng sớm nhất có thể.
Ranh giới của derived query
// Từ khóa derived query hay dùng:
// And / Or · Containing / StartingWith / EndingWith · IgnoreCase
// Before / After / Between · In / NotIn · True / False
// IsNull / IsNotNull · OrderBy...Asc|Desc · Top|First N · Distinct
// ✓ Hợp với derived query: 1–2 điều kiện, ý định đọc được từ tên
Optional<Post> findBySlug(String slug);
boolean existsByEmail(String email);
// ✗ Quá tải — tên method thành một câu văn:
List<Post> findByStatusAndCategorySlugAndPublishedAtBeforeAndVisibleTrue
OrderByPublishedAtDesc(PostStatus st, String cat, LocalDateTime t);
// → tín hiệu chuyển sang @Query với JPQL — chủ đề của Ngày 24
// Ranh giới thực dụng: tên method là TÀI LIỆU —
// khi tên không còn đọc nổi, nó không còn là tài liệu nữaBảng từ khóa trên phủ đại đa số truy vấn lọc-sắp xếp thường gặp —
Containing/IgnoreCasecho tìm kiếm,Between/Beforecho khoảng thời gian,Topcho bảng xếp hạng.Ranh giới không nằm ở khả năng mà ở khả năng đọc: tên method 80 ký tự vẫn chạy đúng, nhưng đã thất bại trong vai trò tài liệu — tín hiệu chuyển sang
@Query(Ngày 24) hoặc Specification (Ngày 32).Join phức tạp, gom nhóm, projection — derived query không với tới: biết giới hạn từ hôm nay giúp Ngày 24 có bối cảnh rõ ràng.
Bài tập nhỏ
Tạo
PostRepositorycho entityPostcủa Ngày 22 — viết REST endpoint CRUD tối giản qua service, kiểm tra bằng các lệnh gọi thử.Thêm
findBySlug,existsBySlugvàfindByTitleContainingIgnoreCase— bậtshow-sql(Ngày 22) đối chiếu SQL sinh ra với dự đoán của bạn.Cố tình gõ sai tên field trong một derived query — đọc kỹ thông báo lỗi lúc khởi động để quen mặt nó.
Thêm field
statuskiểu enum vào entity, viếtcountByStatus— in số bài theo từng trạng thái bằng vòng lặp quavalues()(bài Java hôm nay).
Kết luận
Repository của Spring Data gói trong ba tầng hiểu biết: JpaRepository cho bộ CRUD miễn phí trên một interface trống, proxy + SimpleJpaRepository cho thấy bên dưới vẫn là EntityManager quen thuộc, và derived query sinh truy vấn từ tên method với lưới an toàn fail-fast lúc khởi động — cùng ranh giới thực dụng: tên method là tài liệu. Ngày 24 ta vượt ranh giới đó với @Query: JPQL, native query và bind tham số — công cụ cho những truy vấn derived query không diễn đạt nổ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.


