在先前的單元中,我們已經學會如何定義一個 Customer 實體 (Entity) 並設定好開發與生產環境的資料庫連線。現在,我們將進入 Spring Data JPA 最令人興奮的部分:資料倉儲 (Repository)。
Repository 是你應用程式中業務邏輯與資料庫溝通的橋樑。Spring Data JPA 的魔力在於,它能幫我們自動產生大量的資料庫操作程式碼,讓我們能更專注於核心功能的開發。這份講義將帶你深入了解 JpaRepository,學會各種查詢技巧,並最終打造一個功能完整的客戶管理 API。
完成今日學習後,你將能夠:
JpaRepository:掌握核心的 CRUD (新增、讀取、更新、刪除) 方法,例如: save、findById、findAll 和 deleteById。@Query 註解 (Annotation) 撰寫更複雜、更具彈性的 JPQL (Java Persistence Query Language) 查詢。Pageable 與 Sort 物件,輕鬆地對查詢結果進行分頁 (Pagination) 和排序 (Sorting)。CrudRepository
save(S entity): 新增或更新單一實體。findById(ID id): 根據主鍵 (Primary Key) 查詢,回傳一個 Optional<T>。findAll(): 查詢所有實體。count(): 計算實體總數。deleteById(ID id): 根據主鍵 (Primary Key) 刪除。existsById(ID id): 判斷實體是否存在。PagingAndSortingRepository
CrudRepository。findAll(Sort sort): 查詢所有實體並排序。findAll(Pageable pageable): 查詢並回傳一個分頁的結果 (Page<T>)。JpaRepository
PagingAndSortingRepository。findAll(): 回傳 List<T> 而不是 Iterable<T>,更方便使用。flush(): 將快取中的變更立刻同步到資料庫。saveAndFlush(S entity): 儲存並立刻同步。deleteInBatch(Iterable<T> entities): 批次刪除,效能更好。結論:在大部分情況下,直接繼承 JpaRepository 是最佳選擇,因為它提供了最完整的功能集。
CustomerRepository現在,讓我們動手為先前建立的 Customer 實體 (Entity) 打造一個 Repository。
在你的專案中,建立一個新的 Java 介面 CustomerRepository,並讓它繼承 JpaRepository。
檔案位置:src/main/java/com/example/demo/repositories/CustomerRepository.java
package com.example.demo.repositories;
import com.example.demo.entities.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
// @Repository 註解是可選的,但它可以幫助 Spring 框架更好地進行異常轉譯
@Repository
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
// 現在,這裡雖然是空的,但我們已經自動獲得了完整的 CRUD、分頁和排序功能!
}
沒錯,就是這麼簡單!我們只需要定義這個介面,完全不需要撰寫任何實作程式碼,Spring Data JPA 就會自動為我們提供一套完整的資料操作方法。這包括了所有基本的 CRUD 功能(如 save()、findById()、findAll() 和 deleteById()),以及進階的分頁與排序。這正是 Spring Data JPA 的魔力所在。
這是 Spring Data JPA 的一大特色。你只需要遵循特定的命名規則來定義介面方法,Spring Data 就會自動為你產生對應的 SQL 查詢。
規則:find...By<屬性名><條件關鍵字>(...參數)
find、read、get、query、count。例如:findByName 和 countByName。Customer Entity 中的屬性名稱完全一致(首字母大寫)。And, Or, Is, Equals, Like, Containing, Between, GreaterThan, IsNull 等。範例 1:根據 Email 查詢客戶
我們需要一個方法來根據客戶的 Email 查詢資料。這是一個精確匹配的查詢。
在 CustomerRepository.java 中加入 findByEmail 方法:
// ...
import java.util.Optional;
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
/**
* 根據 Email 查詢客戶。
* Spring Data JPA 會自動生成 JPQL: "SELECT c FROM Customer c WHERE c.email = ?1"
* 使用 Optional<Customer> 作為回傳型別是個好習慣,可以優雅地處理查無資料的情況。
*/
Optional<Customer> findByEmail(String email);
}
範例 2:根據狀態查詢客戶
現在我們需要查詢所有狀態為 ACTIVE 的客戶。
在 CustomerRepository.java 中加入 findByStatus 方法:
// ...
import com.example.demo.enums.CustomerStatus;
import java.util.List;
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
Optional<Customer> findByEmail(String email);
/**
* 根據客戶狀態查詢客戶清單。
* Spring Data JPA 會自動生成 JPQL: "SELECT c FROM Customer c WHERE c.status = ?1"
*/
List<Customer> findByStatus(CustomerStatus status);
}
@Query 進行自訂查詢與更新當衍生查詢方法的命名變得過於冗長,或需要執行更新 (UPDATE) / 刪除 (DELETE) 操作時,@Query 就是我們的最佳工具。它讓我們可以直接撰寫 JPQL 或原生 SQL。
需求:我們需要一個方法,只更新特定客戶的狀態,而不是把整個客戶物件撈出來修改後再存回去。 這樣效能更好。
在 CustomerRepository.java 中加入 updateCustomerStatus 方法:
// ...
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
// ... 其他衍生查詢方法 ...
/**
* 根據客戶 ID 更新其狀態。
* @Modifying 標示這是一個修改資料庫的查詢 (UPDATE, DELETE)。
* @Transactional 確保這個操作在一個交易 (Transaction) 中執行。
* :status 和 :id 是命名參數 (Named Parameters),會對應到 @Param 註解的參數。
*/
@Modifying
@Transactional
@Query("UPDATE Customer c SET c.status = :status WHERE c.id = :id")
void updateCustomerStatus(@Param("id") UUID id, @Param("status") CustomerStatus status);
}
注意:所有 @Modifying 查詢都必須在一個交易 (@Transactional) 中執行。通常我們會把這個註解 (Annotation) 放在服務層 (Service Layer) 的方法上,但為了範例簡潔,先加在這裡。
當客戶數量龐大時,一次性回傳所有資料是不現實的。JpaRepository 已經為我們準備好了分頁與排序的支援。
首先,我們可以在 CustomerRepository 中增加一個支援分頁的衍生查詢方法。
// ...
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
// ... 其他方法 ...
/**
* 根據狀態分頁查詢客戶 (Derived Query Method with Pagination).
* @param status 客戶狀態
* @param pageable 包含分頁與排序資訊的物件
* @return 一個包含客戶分頁結果的 Page 物件
*/
Page<Customer> findByStatus(CustomerStatus status, Pageable pageable);
}
接著,我們在服務層 (Service Layer) CustomerService 中呼叫這些 Repository 方法,並封裝業務邏輯。
在 PagingAndSortingRepository 已經為我們提供了 findAll(Pageable pageable) 方法。Pageable 是一個介面 (Interface),我們通常使用它的實作類別 PageRequest 來建立分頁請求。
CustomerService.java
package com.example.demo.services;
import com.example.demo.entities.Customer;
import com.example.demo.enums.CustomerStatus;
import com.example.demo.repositories.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.UUID;
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
/**
* 範例:分頁查詢所有客戶,查詢第二頁的資料,每頁顯示 5 筆,並依據 name 欄位升序排列。
*/
public Page<Customer> findCustomersPaginated() {
// PageRequest.of(page, size, sort)
// page: 頁碼 (從 0 開始) -> 第 2 頁就是 1
// size: 每頁筆數
// Sort: 排序條件
Pageable pageable = PageRequest.of(1, 5, Sort.by("name").ascending());
return customerRepository.findAll(pageable);
}
/**
* 提供彈性的自訂分頁查詢
*/
public Page<Customer> findCustomersPaginated(int page, int size, String sortBy, String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase("DESC")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
return customerRepository.findAll(pageable);
}
/**
* 根據狀態進行分頁查詢
*/
public Page<Customer> findCustomersByStatusPaginated(CustomerStatus status, int page, int size, String sortBy, String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase("DESC")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
// 呼叫我們在 Repository 定義的分頁方法
return customerRepository.findByStatus(status, pageable);
}
// ... 其他業務邏輯方法,例如 findById, saveCustomer 等 ...
public Optional<Customer> findById(UUID id) {
return customerRepository.findById(id);
}
public void updateCustomerStatus(UUID id, CustomerStatus status) {
customerRepository.updateCustomerStatus(id, status);
}
}
Page<Customer> 物件不僅包含了當頁的客戶列表 (getContent()),還包含了總筆數 (getTotalElements())、總頁數 (getTotalPages()) 等完整的分頁資訊,非常方便前端使用。
最後,我們在控制器 (Controller) 中建立對應的 API 端點,讓外部可以透過 HTTP 請求來存取我們的分頁查詢功能。
CustomerController.java
package com.example.demo.controllers;
import com.example.demo.entities.Customer;
import com.example.demo.enums.CustomerStatus;
import com.example.demo.services.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
@Autowired
private CustomerService customerService;
/**
* API 端點:預設分頁查詢
* GET http://localhost:8080/api/customers/paginated
*/
@GetMapping("/paginated")
public ResponseEntity<Page<Customer>> getCustomersPaginated() {
Page<Customer> customers = customerService.findCustomersPaginated();
return ResponseEntity.ok(customers);
}
/**
* API 端點:自訂分頁查詢
* GET http://localhost:8080/api/customers/paginated/custom?page=0&size=10&sortBy=name&sortDirection=ASC
* @param page 頁碼 (預設為 0)
* @param size 每頁筆數 (預設為 10)
* @param sortBy 排序欄位 (預設為 name)
* @param sortDirection 排序方向 (預設為 ASC)
*/
@GetMapping("/paginated/custom")
public ResponseEntity<Page<Customer>> getCustomersPaginatedCustom(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "ASC") String sortDirection) {
Page<Customer> customers = customerService.findCustomersPaginated(page, size, sortBy, sortDirection);
return ResponseEntity.ok(customers);
}
/**
* API 端點:根據狀態分頁查詢
* GET http://localhost:8080/api/customers/status/ACTIVE/paginated?page=0&size=5
*/
@GetMapping("/status/{status}/paginated")
public ResponseEntity<Page<Customer>> getCustomersByStatusPaginated(
@PathVariable CustomerStatus status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "5") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "ASC") String sortDirection) {
Page<Customer> customers = customerService.findCustomersByStatusPaginated(status, page, size, sortBy, sortDirection);
return ResponseEntity.ok(customers);
}
// ... 其他 CRUD 操作的 API 端點 ...
}
今天,你已經從頭到尾掌握了 Spring Data JPA Repository 的核心操作:
CustomerRepository 並繼承 JpaRepository,立即獲得了所有基礎功能。@Query 處理更複雜的更新操作。Pageable 的方法,並在 CustomerService 中建立了 PageRequest 來實現分頁邏輯。CustomerController 將分頁功能以 REST API 的形式提供給外部使用。現在,你已經具備了打造一個強大且高效的資料存取層所需的所有知識!
CustomerRepository.java
package com.example.demo.repositories;
import com.example.demo.entities.Customer;
import com.example.demo.enums.CustomerStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
/**
* 根據 Email 查詢客戶
*/
Optional<Customer> findByEmail(String email);
/**
* 根據客戶狀態查詢客戶清單。
*/
List<Customer> findByStatus(CustomerStatus status);
/**
* 根據狀態分頁查詢客戶 (Derived Query Method with Pagination).
* @param status 客戶狀態
* @param pageable 分頁與排序資訊
* @return 一個包含客戶分頁結果的 Page 物件
*/
Page<Customer> findByStatus(CustomerStatus status, Pageable pageable);
/**
* 使用 JPQL 更新客戶狀態 (@Query with @Modifying).
* @param id 要更新的客戶 ID
* @param status 新的狀態
*/
@Modifying
@Transactional
@Query("UPDATE Customer c SET c.status = :status WHERE c.id = :id")
void updateCustomerStatus(@Param("id") UUID id, @Param("status") CustomerStatus status);
}