iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Software Development

spring boot 3 學習筆記系列 第 25

Day25 - Spring Data JPA Repository 實戰:從 CRUD 到進階查詢

  • 分享至 

  • xImage
  •  

在先前的單元中,我們已經學會如何定義一個 Customer 實體 (Entity) 並設定好開發與生產環境的資料庫連線。現在,我們將進入 Spring Data JPA 最令人興奮的部分:資料倉儲 (Repository)

Repository 是你應用程式中業務邏輯與資料庫溝通的橋樑。Spring Data JPA 的魔力在於,它能幫我們自動產生大量的資料庫操作程式碼,讓我們能更專注於核心功能的開發。這份講義將帶你深入了解 JpaRepository,學會各種查詢技巧,並最終打造一個功能完整的客戶管理 API。

學習目標

完成今日學習後,你將能夠:

  1. 熟悉 JpaRepository:掌握核心的 CRUD (新增、讀取、更新、刪除) 方法,例如: savefindByIdfindAlldeleteById
  2. 掌握自動查詢方法:活用 衍生查詢方法 (Derived Query Methods) 的命名規則,不寫 SQL 就能完成查詢。
  3. 學會自訂查詢:使用 @Query 註解 (Annotation) 撰寫更複雜、更具彈性的 JPQL (Java Persistence Query Language) 查詢。
  4. 實現分頁與排序:透過 PageableSort 物件,輕鬆地對查詢結果進行分頁 (Pagination)排序 (Sorting)
  5. 整合應用:將 Repository 整合到服務層 (Service Layer)控制器 (Controller),並提供 REST API。

核心概念:Repository 的三大支柱

  1. CrudRepository

    • 提供了最基礎的 CRUD 功能。
    • save(S entity): 新增或更新單一實體。
    • findById(ID id): 根據主鍵 (Primary Key) 查詢,回傳一個 Optional<T>
    • findAll(): 查詢所有實體。
    • count(): 計算實體總數。
    • deleteById(ID id): 根據主鍵 (Primary Key) 刪除。
    • existsById(ID id): 判斷實體是否存在。
  2. PagingAndSortingRepository

    • 繼承自 CrudRepository
    • 在 CRUD 的基礎上,增加了分頁排序的功能。
    • findAll(Sort sort): 查詢所有實體並排序。
    • findAll(Pageable pageable): 查詢並回傳一個分頁的結果 (Page<T>)。
  3. JpaRepository

    • 繼承自 PagingAndSortingRepository
    • 這是我們最常使用的介面,它不僅包含上述所有功能,還增加了一些 JPA 專屬的方法,非常實用。
    • 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 的魔力所在。

第二步:使用衍生查詢方法 (Derived Query Methods)

這是 Spring Data JPA 的一大特色。你只需要遵循特定的命名規則來定義介面方法,Spring Data 就會自動為你產生對應的 SQL 查詢。

規則find...By<屬性名><條件關鍵字>(...參數)

  • 前綴 (Introducer):可以是 findreadgetquerycount。例如:findByNamecountByName
  • By:用來分隔前綴和查詢條件。
  • 屬性名:必須與 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 已經為我們準備好了分頁與排序的支援。

1. Repository 層的準備

首先,我們可以在 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);
}

2. Service 層的實作

接著,我們在服務層 (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()) 等完整的分頁資訊,非常方便前端使用。

第五步:建立 REST API 端點

最後,我們在控制器 (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 的核心操作:

  1. 定義 Repository:我們建立了 CustomerRepository 並繼承 JpaRepository,立即獲得了所有基礎功能。
  2. 實現查詢:我們學會了使用衍生查詢方法來快速實現標準查詢,並用 @Query 處理更複雜的更新操作。
  3. 整合分頁:我們在 Repository 中加入了支援 Pageable 的方法,並在 CustomerService 中建立了 PageRequest 來實現分頁邏輯。
  4. 暴露 API:最後,我們透過 CustomerController 將分頁功能以 REST API 的形式提供給外部使用。

現在,你已經具備了打造一個強大且高效的資料存取層所需的所有知識!

最終的 CustomerRepository 程式碼

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);
}

相關資料來源


上一篇
Day24 - Spring Data JPA 實戰:從 0 到 1 打造 Entity 與 Primary key 設計
下一篇
Day26 - Spring Data JPA 關聯對映實戰:從一對多到多層級關聯
系列文
spring boot 3 學習筆記30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言