Backend

99 Ngày Spring — Ngày 23: Spring Data JPA repository

SSite Admin
19 tháng 08, 2026 6 phút đọc 1 lượt xem
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.

Sketchnote Ngày 23: Spring Data JPA repository — JpaRepository với CRUD có sẵn, phả hệ interface, derived query từ tên method, ranh giới chuyển sang @Query

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 minh
  • Hai tham số generic nói đủ mọi điều Spring Data cần: kiểu entitykiểu khóa chính — từ đó save/findById/findAll/deleteById/count/existsById sẵn sàng không cần một dòng SQL.

  • findById trả 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ả null dễ 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ết
  • Chọn interface nào để extends là chọn diện tích API: JpaRepository tiệ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ẳng Repository và 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 qua EntityManager và 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ớ: save trê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ên
  • Quy 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/countBy sinh dạng truy vấn tối ưu hơn hẳn findBy rồ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ể.

Thư viện với những kệ sách cao — repository là thủ thư của tầng dữ liệu: mô tả thứ cần tìm bằng tên gọi chuẩn, việc tra cứu để thủ thư lo

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ữa
  • Bảng từ khóa trên phủ đại đa số truy vấn lọc-sắp xếp thường gặp — Containing/IgnoreCase cho tìm kiếm, Between/Before cho khoảng thời gian, Top cho 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 PostRepository cho entity Post củ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, existsBySlugfindByTitleContainingIgnoreCase — bật show-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 status kiểu enum vào entity, viết countByStatus — in số bài theo từng trạng thái bằng vòng lặp qua values() (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!

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 23: Enum

Ngày 23 của 99 Ngày Java: enum — tập giá trị cố định an toàn kiểu với == đáng tin (khác wrapper Ngày 22), lớp đầy đủ với field, constructor và method, switch expression exhaustive biến compiler thành lưới an toàn khi thay đổi, cùng EnumMap và EnumSet chuyên dụng — và quy tắc lưu trữ theo name() thay vì ordinal.

19 thg 8, 20266 phút0
99 Ngày Java — Ngày 22: Wrapper class & autoboxing

Ngày 22 của 99 Ngày Java: wrapper class và autoboxing — cầu nối tự động giữa primitive và đối tượng, Integer cache với hiện tượng == đúng ở 127 sai ở 128, bẫy unboxing null gây NPE, boxing thầm lặng trong vòng lặp, và cách chọn giữa parseInt với valueOf.

18 thg 8, 20266 phút26
99 Ngày Spring — Ngày 22: Entity mapping cơ bản

Ngày 22 của 99 Ngày Spring: entity đầu tiên với @Entity/@Id và lý do bắt buộc no-arg constructor (entity là class, không phải record), chiến lược sinh khóa IDENTITY vs SEQUENCE trên PostgreSQL, @Column với ràng buộc và ánh xạ kiểu — wrapper cho cột nullable, cùng show-sql và quy ước snake_case.

18 thg 8, 20266 phút20