在先前的單元中,我們已經成功建立了 Customer
實體、定義了 Repository,甚至實作了複雜的實體關聯。現在,我們要從「功能實現」邁向「效能與維護性」,探討在真實世界中至關重要的議題:如何高效地處理大量資料,以及如何安全地管理資料的生命週期。
本單元將深入 JPA 底層的 Hibernate 實作,解析 save()
、persist()
、merge()
這些看似相似卻截然不同的方法,並帶您實作強大的批次處理 (Batch Processing) 與業界推崇的軟刪除 (Soft Delete) 策略。
完成今日學習後,你將能夠:
save()
、persist()
、merge()
差異:知道在何種情境下該使用哪個方法來操作實體 (Entity),並理解實體的三種狀態 (Transient, Persistent, Detached)。在我們開始批次儲存或刪除物件之前,必須先理解一個核心觀念:JPA 中的實體 (Entity) 是有「狀態」的。Hibernate (作為 JPA 的實作) 會根據實體的狀態來決定如何管理它。
一個實體物件主要有三種狀態:
new
建立出來的物件,它還沒有與任何資料庫紀錄關聯,也沒有被 JPA 的 EntityManager
(實體管理器) 所管理。EntityManager
的管理中,並且對應到資料庫中的一筆紀錄。任何在此狀態下對物件欄位的修改,都會在交易 (Transaction) 提交時自動同步回資料庫。這就是所謂的「髒檢查 (Dirty Checking)」。EntityManager
已經關閉或被清空,不再管理它。它雖然還保有資料庫紀錄的 ID,但對它的任何修改都不會自動同步回資料庫。理解了這三種狀態後,我們就能輕易分辨 persist()
、save()
和 merge()
的不同之處。
方法 | 來源 | 主要用途 | 返回值 | 處理游離態 (Detached) 物件 | 建議 |
---|---|---|---|---|---|
persist(obj) |
JPA 標準 | 將一個新的 (Transient) 物件轉為持續態 (Persistent) | void |
會拋出例外! | 推薦用於新增資料 |
merge(obj) |
JPA 標準 | 將一個游離態 (Detached) 物件的變更,合併回一個持續態 (Persistent) 物件 | 返回一個新的、處於持續態的物件 | 尋找對應 ID 的物件並更新 | 推薦用於更新資料 |
save(obj) |
Hibernate 獨有 | 與 persist 類似,但會立即指派 ID |
Serializable (ID) |
建立一筆新的紀錄,可能導致資料重複! | 已被棄用,避免使用 |
核心結論:
entityManager.persist(newObject)
或 repository.save(newObject)
(Spring Data JPA 的 save
方法在內部會判斷是新物件還是舊物件,並呼叫對應的 persist
或 merge
)。entityManager.merge(detachedObject)
。請務必接收 merge
方法的回傳值,因為這才是真正被 EntityManager
管理的物件。想像一下,如果要新增 10,000 筆客戶資料,預設情況下,JPA 會發送 10,000 次獨立的 INSERT
SQL 敘述到資料庫。這中間包含了大量的網路延遲與資料庫負擔。批次處理能將這些 SQL 敘述打包成幾批 (例如每批 100 筆),一次性發送,效能提升非常顯著。
步驟一:啟用批次設定
在 application.properties
中加入以下設定,這是啟用批次處理的關鍵。
# --- Batch Processing Settings for PostgreSQL ---
# 設定批次大小。建議值為 10 到 50 之間。
spring.jpa.properties.hibernate.jdbc.batch_size=30
# 啟用批次插入排序,Hibernate 會先收集所有同類型的 INSERT 敘述再一起執行。
spring.jpa.properties.hibernate.order_inserts=true
# 啟用批次更新排序,同理於更新操作。
spring.jpa.properties.hibernate.order_updates=true
# 針對有版本控制 (@Version) 的資料啟用批次更新。
spring.jpa.properties.hibernate.batch_versioned_data=true
步驟二:選擇正確的主鍵生成策略
這是最常被忽略的陷阱!要讓批次插入生效,絕對不能使用 GenerationType.IDENTITY
。
因為 IDENTITY
策略需要資料庫在插入當下立即回傳產生的主鍵 ID,這會打斷批次執行的流程。對於 PostgreSQL,最佳選擇是 GenerationType.SEQUENCE
(或 AUTO
)。
請確保您的 Customer.java
和其他實體的主鍵是這樣設定的:
// 在 Customer.java 中
@Id
// 使用 UUID 或 SEQUENCE 都能很好地支援批次處理
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
// 如果您偏好數字 ID,可以使用 SEQUENCE
// @Id
// @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customer_seq")
// @SequenceGenerator(name = "customer_seq", sequenceName = "customer_id_seq", allocationSize = 1)
// private Long id;
步驟三:注意記憶體管理
當您處理極大量的資料時 (數十萬筆以上),即使啟用了批次處理,JPA 的持續性上下文 (Persistence Context) 仍會快取所有您 persist
的物件,最終可能導致記憶體溢出 (OutOfMemoryError)。
在這種情況下,您需要手動介入,定期地將快取同步 (flush) 到資料庫,並清空 (clear) 快取。
// 在 Service 層
@Transactional
public void batchCreateCustomers(List<Customer> customers) {
for (int i = 0; i < customers.size(); i++) {
entityManager.persist(customers.get(i));
// 當達到批次大小時,手動 flush 並 clear
if (i > 0 && i % 30 == 0) {
entityManager.flush(); // 將快取中的 INSERT 敘述發送到資料庫
entityManager.clear(); // 清空快取,釋放記憶體
}
}
}
直接從資料庫刪除紀錄 (DELETE FROM ...
) 稱為硬刪除 (Hard Delete)。這種方式簡單直接,但被刪除的資料就永遠消失了,對於需要稽核或保留歷史紀錄的系統來說是個災難。
軟刪除 (Soft Delete) 是一種更安全、更受推薦的做法。我們不在資料庫中實際刪除紀錄,而是增加一個欄位 (例如 deleted
或 is_active
) 來標記這筆資料的狀態。
實作軟刪除
我們將修改 Customer
實體來支援軟刪除。
步驟一:修改 Customer
實體
加入 deleted
欄位,並加上 Hibernate 獨有的 @SQLDelete
與 @SQLRestriction
註解 (Annotation)。
@SQLRestriction
的魔力在於,之後所有針對 Customer
的查詢 (包含 findById
, findAll
等),Hibernate 都會自動在 SQL 語法中加上 WHERE deleted = false
的條件!請注意,在 Hibernate 6+ (Spring Boot 3 預設使用) 中,舊的 @Where
註解 (Annotation) 已被棄用,應改用功能完全相同的 @SQLRestriction
。
Customer.java
import org.hibernate.annotations.SQLDelete;
// 匯入新的 @SQLRestriction 註解
import org.hibernate.annotations.SQLRestriction;
// ...
@Entity
@Table(name = "customers")
// 魔法發生的地方:所有查詢都會自動加上這個條件
// 使用新的 @SQLRestriction 註解來取代已棄用的 @Where
@SQLRestriction("deleted = false")
// 當執行 delete 操作時,Hibernate 會執行這條 SQL 來代替真正的 DELETE
@SQLDelete(sql = "UPDATE customers SET deleted = true WHERE id = ?")
public class Customer {
// ... 其他欄位 ...
@Column(name = "deleted", nullable = false)
private boolean deleted = false;
// --- Getters and Setters for deleted ---
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
}
@SQLRestriction("deleted = false")
: 自動過濾掉被標記為刪除的客戶。@SQLDelete(...)
: 當我們呼叫 repository.delete(customer)
時,Hibernate 不會執行 DELETE
,而是執行我們定義的 UPDATE
敘述,將 deleted
設為 true
。現在,讓我們將以上所有理論應用到我們的專案中,並提供完整的程式碼與測試指南。
這個 API 將接收一個客戶列表,並使用我們配置好的批次設定將它們一次性存入資料庫。
CustomerService.java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class CustomerService {
private final CustomerRepository customerRepository;
// 使用建構子注入 (Constructor Injection)
public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
@Transactional
public List<Customer> batchCreateCustomers(List<Customer> customers) {
// Spring Data JPA 的 saveAll 方法會自動利用我們在 properties 中設定的批次功能
return customerRepository.saveAll(customers);
}
// ... 其他方法
}
CustomerController.java
// ... imports ...
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
private final CustomerService customerService;
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@PostMapping("/batch")
public ResponseEntity<List<Customer>> createBatchCustomers() {
// 為了方便測試,我們在後端直接生成 10 筆假資料
List<Customer> newCustomers = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
Customer customer = new Customer();
customer.setName("Batch User " + i);
customer.setEmail("batch" + i + "@example.com");
customer.setStatus(CustomerStatus.ACTIVE);
newCustomers.add(customer);
}
List<Customer> savedCustomers = customerService.batchCreateCustomers(newCustomers);
return new ResponseEntity<>(savedCustomers, HttpStatus.CREATED);
}
// ... 其他 API
}
測試:啟動應用程式後,使用 POST 請求訪問 http://localhost:8080/api/customers/batch
。檢查您的主控台 (Console) 輸出與資料庫,您會發現 Hibernate 將 INSERT
敘述打包成一批發送,而不是 10 次單獨的請求。
由於我們已經在 Customer
實體上設定了 @SQLDelete
和 @SQLRestriction
,刪除操作的實現變得異常簡單。
CustomerService.java
(新增方法)
@Transactional
public void deleteCustomer(UUID customerId) {
// 檢查客戶是否存在,若不存在則拋出例外
// 注意:因為有 @SQLRestriction,這裡的 existsById 也只會尋找 deleted = false 的客戶
if (!customerRepository.existsById(customerId)) {
throw new RuntimeException("Customer not found with id: " + customerId);
}
// 這裡的 deleteById 會觸發 @SQLDelete 定義的 UPDATE,而不是真正的 DELETE
// 並且因為我們有 cascade = CascadeType.ALL 和 orphanRemoval = true,
// 相關的 AllowedDomain 也會被「硬刪除」。
// 如果 AllowedDomain 也需要軟刪除,則需要在 AllowedDomain 實體上進行類似的設定。
customerRepository.deleteById(customerId);
}
CustomerController.java
(新增 API)
// 在 CustomerController 類別中新增以下 import 和方法
import java.util.UUID;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
// ...
@DeleteMapping("/{customerId}")
public ResponseEntity<Void> deleteCustomer(@PathVariable UUID customerId) {
customerService.deleteCustomer(customerId);
return ResponseEntity.noContent().build();
}
測試:
http://localhost:8080/api/customers/{customerId}
。@Where
條款生效了。deleted
欄位已變為 true
,但紀錄本身依然存在。其關聯的 allowed_domains
紀錄則因為 orphanRemoval=true
而被硬刪除了。我們需要一個功能,將所有 INACTIVE
狀態的客戶一次性更新為 ACTIVE
。最佳做法是使用 @Query
搭配 @Modifying
直接在資料庫層級執行更新,效率最高。
CustomerRepository.java
(新增方法)
import com.example.demo.models.CustomerStatus;
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;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
// ... 其他查詢方法 ...
@Modifying
@Transactional
@Query("UPDATE Customer c SET c.status = :newStatus WHERE c.status = :oldStatus")
int updateStatusForInactiveCustomers(
@Param("oldStatus") CustomerStatus oldStatus,
@Param("newStatus") CustomerStatus newStatus
);
}
CustomerService.java
(新增方法)
// 在 CustomerService 類別中新增以下方法
@Transactional
public int activateInactiveCustomers() {
return customerRepository.updateStatusForInactiveCustomers(
CustomerStatus.INACTIVE,
CustomerStatus.ACTIVE
);
}
CustomerController.java
(新增 API)
// 在 CustomerController 類別中新增以下 import 和方法
import org.springframework.web.bind.annotation.PutMapping;
// ...
@PutMapping("/activate-all")
public ResponseEntity<String> activateAllInactiveCustomers() {
int updatedCount = customerService.activateInactiveCustomers();
return ResponseEntity.ok("Successfully activated " + updatedCount + " customers.");
}
在完成所有程式碼的撰寫後,您可以啟動 Spring Boot 應用程式,並使用 curl
或 Postman 等工具來測試我們剛剛建立的 API。
執行以下 curl
指令,或在 Postman 中對 http://localhost:8080/api/customers/batch
發送 POST
請求。
curl -X POST http://localhost:8080/api/customers/batch
預期結果:
201 CREATED
。INSERT
敘述打包成一批次執行,而不是分 10 次發送。customers
表,會多出 10 筆紀錄。首先,從資料庫中複製一筆客戶的 id
。假設 ID 為 your-customer-id
。
執行 GET
請求,確認可以查詢到該客戶。
curl http://localhost:8080/api/customers/your-customer-id
執行 DELETE
請求來進行軟刪除。
curl -X DELETE http://localhost:8080/api/customers/your-customer-id
預期結果:
DELETE
請求會回傳 204 No Content
。GET
請求,此時您應該會收到 404 Not Found
或錯誤訊息,因為 @SQLRestriction
生效,查詢時自動過濾掉了 deleted = true
的紀錄。deleted
欄位已變為 true
,但紀錄本身依然存在。手動進入資料庫,將幾筆客戶的 status
欄位從 ACTIVE
改為 INACTIVE
。
執行以下 PUT
請求。
curl -X PUT http://localhost:8080/api/customers/activate-all
預期結果:
"Successfully activated 2 customers."
。INACTIVE
的客戶都變成了 ACTIVE
。UPDATE
語句就完成了,極其高效。恭喜您!至此,我們已經成功地將本指南中的所有理論知識轉化為具體的程式碼實作。您的專案現在不僅功能更強大,而且在效能和資料安全性方面也更加穩健。
讓我們快速回顧一下我們完成的成果:
批次處理設定 (application.properties
)
application.properties
中加入了 Hibernate 的批次處理設定,將批次大小設為 30,並啟用了插入與更新的排序功能,為高效能的資料操作奠定了基礎。Customer
實體增強 (軟刪除)
Customer.java
中,我們加入了 deleted
欄位。@SQLRestriction("deleted = false")
,所有查詢將自動過濾掉被標記為「已刪除」的客戶,讓軟刪除對業務邏輯層幾乎透明。@SQLDelete(...)
,我們巧妙地將 delete
操作攔截,轉換為一個 UPDATE
操作,從而實現了資料的「假性」刪除,保留了資料的完整性。CustomerRepository
擴展
updateStatusForInactiveCustomers
方法,並使用 @Modifying
和 @Query
註解,允許我們直接在資料庫層級執行高效的批次更新。CustomerService
服務層邏輯
batchCreateCustomers
方法,利用 Spring Data JPA 的 saveAll
來觸發批次插入。deleteCustomer
方法,使其與我們的軟刪除策略無縫接軌。activateInactiveCustomers
方法,用於一次性啟用所有非活躍客戶。CustomerController
新增 API 端點
POST /api/customers/batch
:用於批次新增客戶。DELETE /api/customers/{customerId}
:現在執行的是安全的軟刪除。PUT /api/customers/activate-all
:用於批次更新客戶狀態。Customer.java
package com.example.demo.entities;
import com.example.demo.enums.CustomerStatus;
import jakarta.persistence.*;
import jakarta.validation.constraints.Size;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "customers")
// 魔法發生的地方:所有查詢都會自動加上這個條件
// 使用新的 @SQLRestriction 註解來取代已棄用的 @Where
@SQLRestriction("deleted = false")
// 當執行 delete 操作時,Hibernate 會執行這條 SQL 來代替真正的 DELETE
@SQLDelete(sql = "UPDATE customers SET deleted = true WHERE id = ?")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "name", length = 50, nullable = false)
@Size(min = 2, max = 50)
private String name;
// unique=true 會在資料庫中為此欄位建立一個 UNIQUE 約束
@Column(name = "email", unique = true, nullable = false)
private String email;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private CustomerStatus status;
@Column(name = "deleted", nullable = false)
private boolean deleted = false;
/**
* 一對多關聯 (One-to-Many).
* 這一方 (Customer) 是關聯的反向方。
* mappedBy = "customer": 告訴 JPA,這段關係的設定由 AllowedDomain 實體中的 "customer" 屬性負責。
* JPA 會去 AllowedDomain.java 裡找那個 customer 欄位上的 @JoinColumn 設定。
* cascade = CascadeType.ALL: 級聯操作。當我們對 Customer 執行操作 (儲存、刪除) 時,
* 相關的 AllowedDomain 也會一併被操作。這在新增時非常方便。
* orphanRemoval = true: "孤兒移除"。如果一個 AllowedDomain 從這個 Set 集合中被移除,
* 那麼這個 AllowedDomain 在資料庫中也應該被刪除。
*/
@OneToMany(
mappedBy = "customer",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<AllowedDomain> allowedDomains = new HashSet<>();
/**
* 一對多關聯 (One-to-Many) - Customer 和 Order
* 反向方:Order 擁有 customer_id 外鍵
*/
@OneToMany(
mappedBy = "customer",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<Order> orders = new HashSet<>();
// Helper method to sync both sides of the association
public void addDomain(AllowedDomain domain) {
allowedDomains.add(domain);
domain.setCustomer(this);
}
public void removeDomain(AllowedDomain domain) {
allowedDomains.remove(domain);
domain.setCustomer(null);
}
// Helper method to sync both sides of the association for orders
public void addOrder(Order order) {
orders.add(order);
order.setCustomer(this);
}
public void removeOrder(Order order) {
orders.remove(order);
order.setCustomer(null);
}
// ... 省略 getter 和 setter
}
CustomerRepository.java
package com.example.demo.repositories;
import com.example.demo.entities.AllowedDomain;
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.Set;
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);
/**
* 查詢特定客戶的所有允許網域
* @param customerId 客戶 ID
* @return 該客戶的所有允許網域
*/
@Query("SELECT c.allowedDomains FROM Customer c WHERE c.id = :customerId")
Set<AllowedDomain> findAllowedDomainsByCustomerId(@Param("customerId") UUID customerId);
/**
* 批次更新客戶狀態
* @param oldStatus 舊狀態
* @param newStatus 新狀態
* @return 更新的記錄數量
*/
@Modifying
@Transactional
@Query("UPDATE Customer c SET c.status = :newStatus WHERE c.status = :oldStatus")
int updateStatusForInactiveCustomers(
@Param("oldStatus") CustomerStatus oldStatus,
@Param("newStatus") CustomerStatus newStatus
);
}
CustomerService.java
package com.example.demo.services;
import com.example.demo.entities.AllowedDomain;
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 org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
/**
* Customer 服務層類別
* 提供客戶相關的業務邏輯處理
*/
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
/**
* 分頁查詢所有客戶,並按姓名排序
* 根據 README.md 範例:查詢第二頁的資料,每頁顯示 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);
}
/**
* 自訂分頁查詢所有客戶
* @param page 頁碼 (從 0 開始)
* @param size 每頁筆數
* @param sortBy 排序欄位
* @param sortDirection 排序方向 (ASC 或 DESC)
* @return 分頁結果
*/
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);
}
/**
* 根據狀態分頁查詢客戶
* @param status 客戶狀態
* @param page 頁碼 (從 0 開始)
* @param size 每頁筆數
* @param sortBy 排序欄位
* @param sortDirection 排序方向
* @return 分頁結果
*/
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);
return customerRepository.findByStatus(status, pageable);
}
/**
* 根據 Email 查詢客戶
* @param email 客戶 Email
* @return 客戶資訊
*/
public Optional<Customer> findByEmail(String email) {
return customerRepository.findByEmail(email);
}
/**
* 根據狀態查詢客戶清單
* @param status 客戶狀態
* @return 客戶清單
*/
public List<Customer> findByStatus(CustomerStatus status) {
return customerRepository.findByStatus(status);
}
/**
* 新增或更新客戶
* @param customer 客戶資訊
* @return 儲存後的客戶資訊
*/
public Customer saveCustomer(Customer customer) {
return customerRepository.save(customer);
}
/**
* 根據 ID 查詢客戶
* @param id 客戶 ID
* @return 客戶資訊
*/
public Optional<Customer> findById(UUID id) {
return customerRepository.findById(id);
}
/**
* 更新客戶狀態
* @param id 客戶 ID
* @param status 新的狀態
*/
public void updateCustomerStatus(UUID id, CustomerStatus status) {
customerRepository.updateCustomerStatus(id, status);
}
/**
* 查詢所有客戶
* @return 所有客戶清單
*/
public List<Customer> findAllCustomers() {
return customerRepository.findAll();
}
/**
* 計算客戶總數
* @return 客戶總數
*/
public long countCustomers() {
return customerRepository.count();
}
/**
* 取得特定客戶的所有允許網域
* @param customerId 客戶 ID
* @return 該客戶的所有允許網域
*/
public Set<AllowedDomain> getDomainsForCustomer(UUID customerId) {
return customerRepository.findAllowedDomainsByCustomerId(customerId);
}
/**
* 批次建立客戶
* @param customers 客戶列表
* @return 儲存後的客戶列表
*/
@Transactional
public List<Customer> batchCreateCustomers(List<Customer> customers) {
// Spring Data JPA 的 saveAll 方法會自動利用我們在 properties 中設定的批次功能
return customerRepository.saveAll(customers);
}
/**
* 軟刪除客戶
* @param customerId 客戶 ID
*/
@Transactional
public void deleteCustomer(UUID customerId) {
// 檢查客戶是否存在,若不存在則拋出例外
if (!customerRepository.existsById(customerId)) {
throw new RuntimeException("Customer not found with id: " + customerId);
}
// 這裡的 deleteById 會觸發 @SQLDelete 定義的 UPDATE,而不是真正的 DELETE
customerRepository.deleteById(customerId);
}
/**
* 批次更新所有 INACTIVE 客戶為 ACTIVE
* @return 更新的客戶數量
*/
@Transactional
public int activateInactiveCustomers() {
return customerRepository.updateStatusForInactiveCustomers(
CustomerStatus.INACTIVE,
CustomerStatus.ACTIVE
);
}
}
CustomerController.java
package com.example.demo.controllers;
import com.example.demo.entities.AllowedDomain;
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.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
/**
* Customer 控制器
* 提供客戶相關的 REST API 端點
*/
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
@Autowired
private CustomerService customerService;
/**
* 分頁查詢所有客戶
* 根據 README.md 範例:預設查詢第二頁,每頁 5 筆,按姓名升序排列
*
* GET /api/customers/paginated
*/
@GetMapping("/paginated")
public ResponseEntity<Page<Customer>> getCustomersPaginated() {
Page<Customer> customers = customerService.findCustomersPaginated();
return ResponseEntity.ok(customers);
}
/**
* 自訂分頁查詢所有客戶
*
* GET /api/customers/paginated/custom?page=0&size=10&sortBy=name&sortDirection=ASC
*
* @param page 頁碼 (從 0 開始,預設為 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);
}
/**
* 根據狀態分頁查詢客戶
*
* GET /api/customers/status/ACTIVE/paginated?page=0&size=5&sortBy=name&sortDirection=ASC
*/
@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);
}
/**
* 查詢所有客戶
* GET /api/customers
*/
@GetMapping
public ResponseEntity<List<Customer>> getAllCustomers() {
List<Customer> customers = customerService.findAllCustomers();
return ResponseEntity.ok(customers);
}
/**
* 根據 ID 查詢客戶
* GET /api/customers/{id}
*/
@GetMapping("/{id}")
public ResponseEntity<Customer> getCustomerById(@PathVariable UUID id) {
Optional<Customer> customer = customerService.findById(id);
return customer.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
/**
* 根據 Email 查詢客戶
* GET /api/customers/email/{email}
*/
@GetMapping("/email/{email}")
public ResponseEntity<Customer> getCustomerByEmail(@PathVariable String email) {
Optional<Customer> customer = customerService.findByEmail(email);
return customer.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
/**
* 根據狀態查詢客戶清單
* GET /api/customers/status/{status}
*/
@GetMapping("/status/{status}")
public ResponseEntity<List<Customer>> getCustomersByStatus(@PathVariable CustomerStatus status) {
List<Customer> customers = customerService.findByStatus(status);
return ResponseEntity.ok(customers);
}
/**
* 新增客戶
* POST /api/customers
*/
@PostMapping
public ResponseEntity<Customer> createCustomer(@RequestBody Customer customer) {
Customer savedCustomer = customerService.saveCustomer(customer);
return ResponseEntity.ok(savedCustomer);
}
/**
* 更新客戶
* PUT /api/customers/{id}
*/
@PutMapping("/{id}")
public ResponseEntity<Customer> updateCustomer(@PathVariable UUID id, @RequestBody Customer customer) {
Optional<Customer> existingCustomer = customerService.findById(id);
if (existingCustomer.isPresent()) {
customer.setId(id);
Customer updatedCustomer = customerService.saveCustomer(customer);
return ResponseEntity.ok(updatedCustomer);
}
return ResponseEntity.notFound().build();
}
/**
* 更新客戶狀態
* PATCH /api/customers/{id}/status/{status}
*/
@PatchMapping("/{id}/status/{status}")
public ResponseEntity<Void> updateCustomerStatus(@PathVariable UUID id, @PathVariable CustomerStatus status) {
Optional<Customer> customer = customerService.findById(id);
if (customer.isPresent()) {
customerService.updateCustomerStatus(id, status);
return ResponseEntity.ok().build();
}
return ResponseEntity.notFound().build();
}
/**
* 刪除客戶 (軟刪除)
* DELETE /api/customers/{id}
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCustomer(@PathVariable UUID id) {
try {
customerService.deleteCustomer(id);
return ResponseEntity.noContent().build();
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
/**
* 取得客戶總數
* GET /api/customers/count
*/
@GetMapping("/count")
public ResponseEntity<Long> getCustomerCount() {
long count = customerService.countCustomers();
return ResponseEntity.ok(count);
}
/**
* 取得特定客戶的所有允許網域
* GET /api/customers/{customerId}/domains
*
* @param customerId 客戶 ID
* @return 該客戶的所有允許網域
*/
@GetMapping("/{customerId}/domains")
public ResponseEntity<Set<AllowedDomain>> getCustomerDomains(@PathVariable UUID customerId) {
Set<AllowedDomain> domains = customerService.getDomainsForCustomer(customerId);
if (domains.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(domains);
}
/**
* 批次新增客戶
* POST /api/customers/batch
*/
@PostMapping("/batch")
public ResponseEntity<List<Customer>> createBatchCustomers() {
// 為了方便測試,我們在後端直接生成 10 筆假資料
List<Customer> newCustomers = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
Customer customer = new Customer();
customer.setName("Batch User " + i);
customer.setEmail("batch" + i + "@example.com");
customer.setStatus(CustomerStatus.ACTIVE);
newCustomers.add(customer);
}
List<Customer> savedCustomers = customerService.batchCreateCustomers(newCustomers);
return new ResponseEntity<>(savedCustomers, HttpStatus.CREATED);
}
/**
* 批次啟用所有 INACTIVE 客戶
* PUT /api/customers/activate-all
*/
@PutMapping("/activate-all")
public ResponseEntity<String> activateAllInactiveCustomers() {
int updatedCount = customerService.activateInactiveCustomers();
return ResponseEntity.ok("Successfully activated " + updatedCount + " customers.");
}
}