Truy vấn với JPA
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
Database đã có bảng. Giờ API cần đọc, lọc, thêm, sửa, xoá sản phẩm thật. Với Spring Data JPA, bạn khai báo method trong repository, còn Spring Data dịch nó sang SQL và chạy trong database.
Khái niệm
🔍 Derived query: method trong repository có tên như findByStockGreaterThan, được Spring Data đọc tên rồi dịch thành câu SQL.
💾 @Transactional: annotation cho method chạy trong một transaction, khi method kết thúc thì mọi thay đổi trên entity đã đọc ra được ghi xuống database một lần.
| Việc | Code |
|---|---|
| Đọc hết | products.findAll() |
| Lọc | products.findByStockGreaterThan(0) |
| Tìm theo khoá | products.findById(id) |
| Thêm | products.save(p) |
| Sửa | đổi field trong method @Transactional |
| Xoá | products.deleteById(id) |
Ví dụ
Phần đọc ghi database nằm trong DbProductStore, sau interface
ProductStore như bài Dependency injection. Controller không biết có JPA:
// ProductRepository.java
package com.shop.api;
import java.util.List;
import org.springframework.data.jpa.repository.*;
interface ProductRepository
extends JpaRepository<Product, Long> {
List<Product> findByStockGreaterThan(int stock);
}// ProductStore.java
package com.shop.api;
import java.util.List;
interface ProductStore {
List<Product> inStock();
boolean setStock(long id, int stock);
}// DbProductStore.java
package com.shop.api;
import java.util.*;
import org.springframework.stereotype.*;
import org.springframework.transaction.annotation.*;
@Service
class DbProductStore implements ProductStore {
private final ProductRepository products;
public DbProductStore(ProductRepository products) {
this.products = products;
}
@Override
public List<Product> inStock() {
return products.findByStockGreaterThan(0);
}
@Override
@Transactional
public boolean setStock(long id, int stock) {
Optional<Product> found = products.findById(id);
if (found.isEmpty()) {
return false;
}
found.get().setStock(stock);
return true;
}
}// ProductsController.java
package com.shop.api;
import java.util.List;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
class ProductsController {
private final ProductStore store;
public ProductsController(ProductStore store) {
this.store = store;
}
@GetMapping("/in-stock")
public List<Product> inStock() {
return store.inStock();
}
@PutMapping("/{id}/stock")
public ResponseEntity<Void> setStock(
@PathVariable long id,
@RequestParam int stock) {
if (!store.setStock(id, stock)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
}@Serviceđăng kýDbProductStorethay choInMemoryProductStore. Xoá class cũ đi: hai bean cùng càiProductStorethì server dừng lúc khởi động vì không biết chọn bean nào.findByStockGreaterThan: Spring Data đọc tên thànhfindBy+ fieldstock+ điều kiệnGreaterThan, rồi sinhwhere stock > ?. Điều kiện chạy trong database, chỉ các dòng khớp mới được gửi về.findById(id)trả vềOptional<Product>.isEmpty()là không tìm thấy,get()lấy object ra.- Sửa: trong method
@Transactional, chỉ cần đổi field. Khi method kết thúc, Hibernate tự biết object nào đã đổi và sinh câuUPDATE, không cần gọisave. @Transactionalgói mọi câuINSERT,UPDATE,DELETEvào một transaction như bài Transaction của khoá SQL: method ném exception thì không câu nào được lưu.- Điều kiện quá dài để đặt tên method thì viết câu truy vấn bằng
@Querytrên method, ví dụ@Query("select p from Product p where p.stock > 0"). Câu này là JPQL: giống SQL nhưng dùng tên class và field.
Thử ngay
Bảng PRODUCTS vẫn còn trống. Trong VS Code, kết nối Oracle bằng user
shopboot, mật khẩu shopboot_pw, service name FREEPDB1, rồi thêm ba sản
phẩm mẫu mà các bài sau dùng lại:
INSERT INTO products (name, price, stock)
VALUES ('Bút bi', 5000, 120);
INSERT INTO products (name, price, stock)
VALUES ('Vở', 12000, 0);
INSERT INTO products (name, price, stock)
VALUES ('Thước', 7000, 50);
COMMIT;Để xem câu SQL mà Hibernate sinh ra, thêm vào application.properties:
spring.jpa.show-sql=trueGiữ các file cũ, thay và thêm các file theo phần Ví dụ, rồi chạy lại server.
Gọi curl -i http://localhost:5000/api/products/in-stock rồi xem cửa sổ
đang chạy server.
Đoán trước khi chạy: điều kiện stock > 0 được lọc trong Java sau khi
đọc hết bảng, hay nằm ngay trong câu SQL?
Xem kết quả
Hibernate: select p1_0.id,p1_0.name,p1_0.price,
p1_0.stock from products p1_0
where p1_0.stock>?Nằm trong câu SQL. Spring Data dịch tên method thành where p1_0.stock>?,
? là placeholder nhận giá trị 0, nên Oracle chỉ trả về các dòng còn hàng.
Response là 200 kèm hai sản phẩm Bút bi và Thước, không có Vở:
HTTP/1.1 200
[{"id":1,"name":"Bút bi","price":5000,"stock":120},
{"id":3,"name":"Thước","price":7000,"stock":50}]Lỗi hay gặp
Gọi findAll() rồi mới lọc. Cả bảng được đọc về bộ nhớ rồi mới lọc.
Bảng có một triệu dòng thì cả một triệu dòng bị đọc vào bộ nhớ.
// SAI — đọc hết bảng rồi mới lọc trong Java
List<Product> inStock = products.findAll()
.stream()
.filter(p -> p.getStock() > 0)
.toList();// ĐÚNG — khai báo derived query, database lọc
// ProductRepository.java
package com.shop.api;
import java.util.List;
import org.springframework.data.jpa.repository.*;
interface ProductRepository
extends JpaRepository<Product, Long> {
List<Product> findByStockGreaterThan(int stock);
}Quên @Transactional. Đổi field của entity chỉ đổi object trong bộ
nhớ. Không có @Transactional (và cũng không gọi save) thì API vẫn trả
204 nhưng log không có câu UPDATE, database giữ nguyên số cũ.
Tóm tắt
- Derived query: Spring Data đọc tên method trong repository rồi sinh SQL chạy trong database.
- Lọc bằng method của repository, không
findAll()rồi lọc bằng stream. - Sửa entity trong method
@Transactionalthì thay đổi mới được ghi. - Câu truy vấn khó đặt tên thì viết JPQL trong
@Query.
Tự kiểm tra
0/3 câuMethod KHÔNG có @Transactional: Product p = products.findById(3L).get(); p.setPrice(9000); rồi trả 204. Giá trong database có đổi không?
Cần 10 đơn hàng mới nhất. Cách nào chỉ lấy đúng 10 dòng từ database?
Method @Transactional sửa giá hai sản phẩm. Sửa xong sản phẩm thứ nhất thì ném RuntimeException. Giá sản phẩm thứ nhất trong database là gì?