在先前的單元中,我們已經學會如何定義一個 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);
}