Chặng dữ liệu bắt đầu. Bài này về lớp trừu tượng mà 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. Spring phân tích tên phương thức 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. Đâ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. 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.
Thử ba mươi giây
Bật log SQL:
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 đề đó.
Ngày mai: N+1 — 21 truy vấn cho thứ cần đúng 1.