Hình dung Repository như ra lệnh cho một trợ lý cực kỳ máy móc: bạn không chỉ cho nó CÁCH tìm, bạn chỉ đặt TÊN cho thứ mình muốn theo một văn phạm chặt, và nó tự dịch cái tên đó ra SQL. Chặng dữ liệu bắt đầu, và đây là lớp trừu tượng bạn sẽ dùng mỗi ngày.

Một giao diện, không có mã cài đặt

interface RepoTacGia extends JpaRepository<TacGia, Long> { }

Chỉ vậy, và bạn đã có save, findById, findAll, delete, count, existsById, phân trang, sắp xếp. Spring sinh phần cài đặt lúc chạy.

Bốn giao diện xếp chồng, biết để chọn đúng:

Giao diện Cho gì
Repository rỗng, chỉ đánh dấu
CrudRepository CRUD cơ bản
PagingAndSortingRepository thêm phân trang, sắp xếp
JpaRepository thêm flush, saveAll, thao tác theo lô

JpaRepository là mặc định hợp lý. Nếu muốn giới hạn API lộ ra cho tầng nghiệp vụ thì kế thừa Repository rồi khai đúng những phương thức cần.

Truy vấn dẫn xuất từ tên phương thức

List<TacGia> findByTen(String ten);
List<TacGia> findByTenContainingIgnoreCase(String phan);
Optional<TacGia> findFirstByOrderByTenDesc();
long countByTenStartingWith(String tien);
boolean existsByTen(String ten);
List<TacGia> findByTenIn(Collection<String> tens);
List<Bai> findByTacGia_Ten(String ten);           // đi qua quan hệ

Chạy thật trên 20 tác giả, mỗi người 5 bài:

  findByTen("Tac gia 3")                  -> 1
  findByTenContainingIgnoreCase("GIA 1")  -> 11
  findFirstByOrderByTenDesc()             -> Tac gia 9
  countByTenStartingWith("Tac")           -> 20
  existsByTen("khong co")                 -> false
  findByTenIn([2 tên])                    -> 2
  findByTacGia_Ten("Tac gia 4")           -> 5 bài

Không dòng SQL nào được viết. Cái tên findByTenContainingIgnoreCase đọc lên như một câu — tìm, theo tên, có chứa, không phân biệt hoa thường — và trợ lý dịch đúng câu đó thành truy vấn.

Chú ý findFirstByOrderByTenDesc() trả về Tac gia 9 chứ không phải Tac gia 19 — sắp theo chuỗi, nên "9" lớn hơn "19". Đây không phải lỗi của Spring, nhưng nó là loại bất ngờ hay gặp.

Dấu gạch dưới trong findByTacGia_Ten là cách nói rõ ranh giới thuộc tính. Spring thường tự đoán được, nhưng khi tên có thể hiểu hai cách thì gạch dưới cứu bạn.

Sai tên thuộc tính thì ứng dụng không khởi động — Spring kiểm lúc dựng bean chứ không đợi tới lúc gọi. Trợ lý từ chối ngay ở cửa nếu bạn gọi sai tên một trường, chứ không để bạn chờ rồi mới báo. Đây là điểm mạnh: lỗi lộ ra lúc khởi động, không phải lúc 2 giờ sáng.

Khi tên phương thức quá dài

List<Don> findByTrangThaiAndNgayTaoBetweenAndKhachHang_ThanhPhoOrderByNgayTaoDesc(...)

Tới đây thì tên phương thức đã hết hữu ích — cái câu ra lệnh thành một chuỗi líu lưỡi không ai đọc nổi. Chuyển sang JPQL:

@Query("select b.tieuDe from Bai b where b.tacGia.ten = :t")
List<String> chiTieuDe(@Param("t") String t);
  JPQL chỉ lấy tiêu đề  ->  Bai 4-0

@Query cũng được kiểm lúc khởi động — sai cú pháp hay sai tên thực thể là ứng dụng không lên.

Ranh giới tôi dùng: tên phương thức cho một hoặc hai điều kiện, @Query cho phần còn lại. Điều kiện động thì dùng Specification, sẽ nói ở bài 29.

Projection: chỉ lấy cột cần

interface ChiTen { String getTen(); }
List<ChiTen> findByTenStartingWith(String tien);
  projection findByTenStartingWith  ->  11, phần tử đầu = Tac gia 1

Khai một giao diện với các phương thức get, Spring chỉ chọn những cột đó trong câu SQL.

Đây là công cụ bị dùng ít hơn nó xứng đáng. Bảng có 40 cột mà màn hình chỉ hiện 3 cột — projection cắt lượng dữ liệu đi rất nhiều, và quan trọng hơn: đối tượng trả về không được quản lý, nên không có LazyInitializationException, không có dirty checking, không có rủi ro ghi ngoài ý muốn.

Với dữ liệu chỉ để đọc, projection gần như luôn là lựa chọn đúng. Bài 30 sẽ nói kỹ.

Page luôn tốn hai truy vấn

  findAll(Pageable)  ->  2 truy vấn, tongSo=20, tongTrang=4

Một câu lấy dữ liệu, một câu COUNT(*). Trên bảng lớn có điều kiện lọc, câu đếm này có khi tốn hơn câu lấy dữ liệu.

Không cần tổng số thì dùng Slice — nó lấy thêm một dòng để biết còn trang sau không, và bỏ hẳn câu đếm:

Slice<TacGia> findByTenStartingWith(String tien, Pageable p);

Giao diện cuộn vô tận không cần tổng số. Đó là một truy vấn tiết kiệm được ở mọi lần lật trang.

Ba điều dễ vấp

save() không phải lúc nào cũng INSERT. Nó gọi merge nếu entity có id khác null, và merge sẽ chạy một câu SELECT trước để xem bản ghi đã có chưa. Với chèn hàng loạt có id tự đặt, đó là gấp đôi số truy vấn. Bài 32 sẽ đo.

getReferenceById khác findById. Cái đầu trả về proxy không truy vấn CSDL — hữu ích khi bạn chỉ cần gán quan hệ. Nhưng chạm vào bất kỳ thuộc tính nào là nó truy vấn, và nếu id không tồn tại thì ném EntityNotFoundException ở một chỗ rất xa nơi bạn gọi.

Đặt tên hai kiểu là hai hành vi. deleteByTen(String) tải các entity lên rồi xoá từng cái — đúng để cascade và event chạy, nhưng chậm. @Modifying @Query("delete from ...") xoá một câu — nhanh, nhưng bỏ qua cascade và không cập nhật persistence context. Biết mình đang dùng cái nào.

Nếu chỉ làm một thứ sau bài này, hỏi thẳng xem cái tên bạn vừa đặt thật ra chạy bao nhiêu câu SQL, trong ba mươi giây — bật log:

logging.level.org.hibernate.SQL: DEBUG

Gọi một endpoint danh sách bất kỳ và đếm số dòng SQL hiện ra. Nếu số đó lớn hơn 2–3, bạn có vấn đề — và ngày mai là bài về vấn đề đó.

Mẫu số chung

Khai cái bạn muốn từ một kho dữ liệu rồi để framework tự tổng hợp cách làm — tức mẫu Repository — có ở khắp nơi: Spring Data, EF Core của .NET, và sâu xa là Repository của Domain-Driven Design tách miền nghiệp vụ khỏi cơ chế truy vấn. Còn truy-vấn-dẫn-xuất-từ-tên là một DSL nhỏ, quy ước đứng thay cho mã. Cái đánh đổi luôn giống nhau: quy ước thì ngắn gọn và an toàn cho tới khi nhu cầu vượt quá sức diễn đạt của nó — lúc đó phải hạ xuống dạng tường minh (@Query, SQL thuần). Kỹ năng là biết lằn ranh ấy nằm đâu với công cụ của mình; một tên phương thức bảy mệnh đề là đã vượt quá. Và mặt trái của mọi trừu tượng tiện lợi là chi phí giấu mặt: save() lặng lẽ làm merge cộng SELECT, Page chạy thêm một COUNT, xoá dẫn xuất thì tải-lên-rồi-xoá — nên luôn phải biết cái khai báo của mình thật ra phát ra SQL gì (bật log, đếm câu).

Điều thứ hai: kiểm hợp đồng càng sớm càng tốt. Spring soát mọi tên thuộc tính và chuỗi JPQL ngay lúc dựng bean, nên một lỗi gõ nhầm hỏng lúc khởi động, không phải lúc 2 giờ sáng dưới tải — cùng cái lợi với kiểu tĩnh, với soát schema, và với tinh thần hỏng-sớm nói chung. Một truy vấn mà tính đúng chỉ được chứng minh khi có người dùng đi đúng vào đường đó là một con bọ ngủ đông; đẩy phép kiểm về sớm nhất có thể (khởi động, biên dịch, CI) biến một cú gọi điện lúc 2 giờ sáng thành một lỗi khởi động bạn thấy trước khi kịp phát hành.

Ngày mai: N+1 — 21 truy vấn cho thứ cần đúng 1.