VieTopik
Tiếng HànTiếng AnhIT
  • Góc học tập
Tải app
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
Spring Boot Web APIDữ liệu với JPA
Bài 15/19
5 phút

Truy vấn với JPA

Nội dung bài · 5 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 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ý DbProductStore thay cho InMemoryProductStore. Xoá class cũ đi: hai bean cùng cài ProductStore thì 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ành findBy + field stock + điều kiện GreaterThan, rồi sinh where 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âu UPDATE, không cần gọi save.
  • @Transactional gói mọi câu INSERT, UPDATE, DELETE và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 @Query trê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.
findByStockGreaterThan(0) Spring Data dịch sang SQL where stock > ? Database Chỉ trả về các dòng còn hàng
findByStockGreaterThan được dịch sang SQL, database chỉ trả dòng khớp

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=true

Giữ 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 @Transactional thì 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âu
Câu 1

Method 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âu 2

Cần 10 đơn hàng mới nhất. Cách nào chỉ lấy đúng 10 dòng từ database?

Câu 3

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ì?

MigrationQuan hệ một-nhiều

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt