---
name: jpa-patterns
description: 用於 Spring Boot 的 JPA/Hibernate 模式，涵蓋實體設計、關聯、查詢優化、事務、審計 (Auditing)、索引、分頁及連接池。
---

# JPA/Hibernate 模式 (JPA/Hibernate Patterns)

用於 Spring Boot 中的資料建模、倉儲實作與效能調校。

## 何時啟用

- 設計 JPA 實體 (Entities) 與資料表映射 (Table Mappings)。
- 定義關聯 (@OneToMany, @ManyToOne, @ManyToMany)。
- 優化查詢（防止 N+1 問題、抓取策略、投影 Projections）。
- 配置事務 (Transactions)、審計或軟刪除 (Soft Deletes)。
- 建立分頁、排序或自定義倉儲方法。
- 調校連接池 (HikariCP) 或二級快取。

## 實體設計 (Entity Design)

```java
@Entity
@Table(name = "markets", indexes = {
  @Index(name = "idx_markets_slug", columnList = "slug", unique = true)
})
@EntityListeners(AuditingEntityListener.class)
public class MarketEntity {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable = false, length = 200)
  private String name;

  @Column(nullable = false, unique = true, length = 120)
  private String slug;

  @Enumerated(EnumType.STRING)
  private MarketStatus status = MarketStatus.ACTIVE;

  @CreatedDate private Instant createdAt;
  @LastModifiedDate private Instant updatedAt;
}
```

啟用審計功能：
```java
@Configuration
@EnableJpaAuditing
class JpaConfig {}
```

## 關聯與防止 N+1 問題

```java
@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PositionEntity> positions = new ArrayList<>();
```

- 預設使用延遲加載 (Lazy Loading)；必要時在查詢中加入 `JOIN FETCH`。
- 避免在集合 (Collections) 上使用 `EAGER`（預期加載）；在讀取路徑上優先使用 DTO 投影。

```java
@Query("select m from MarketEntity m left join fetch m.positions where m.id = :id")
Optional<MarketEntity> findWithPositions(@Param("id") Long id);
```

## 倉儲模式 (Repository Patterns)

```java
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
  Optional<MarketEntity> findBySlug(String slug);

  @Query("select m from MarketEntity m where m.status = :status")
  Page<MarketEntity> findByStatus(@Param("status") MarketStatus status, Pageable pageable);
}
```

- 針對輕量化查詢使用投影 (Projections)：
```java
public interface MarketSummary {
  Long getId();
  String getName();
  MarketStatus getStatus();
}
Page<MarketSummary> findAllBy(Pageable pageable);
```

## 事務 (Transactions)

- 在服務層方法加上 `@Transactional` 註釋。
- 在讀取路徑上使用 `@Transactional(readOnly = true)` 以優化效能。
- 謹慎選擇傳播 (Propagation) 行為；避免過長的事務。

```java
@Transactional
public Market updateStatus(Long id, MarketStatus status) {
  MarketEntity entity = repo.findById(id)
      .orElseThrow(() -> new EntityNotFoundException("找不到指定的市場資料"));
  entity.setStatus(status);
  return Market.from(entity);
}
```

## 分頁 (Pagination)

```java
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
Page<MarketEntity> markets = repo.findByStatus(MarketStatus.ACTIVE, page);
```

對於游標式 (Cursor-like) 分頁，應在具備排序的 JPQL 中增加 `id > :lastId` 條件。

## 索引與效能

- 為常用的過濾欄位建索引（如 `status`, `slug`, 外鍵）。
- 使用符合查詢模式的複合索引（例如：`status, created_at`）。
- 避免使用 `select *`；僅投影必要的欄位。
- 批次寫入：配合 `saveAll` 並設置 `hibernate.jdbc.batch_size`。

## 連接池 (HikariCP)

推薦配置屬性：
```
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.validation-timeout=5000
```

針對 PostgreSQL LOB 處理：
```
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
```

## 快取 (Caching)

- 一級快取 (1st-level cache) 是基於 EntityManager 的；避免橫跨事務保存實體。
- 對於讀取密集的實體，可考慮啟用二級快取，但須謹慎驗證其清理 (Eviction) 策略。

## 資料遷移 (Migrations)

- 務必使用 Flyway 或 Liquibase；在生產環境中絕不要依賴 Hibernate 的 auto DDL。
- 保持遷移腳本具備冪等性 (Idempotent) 且採用增量式修改；未經計畫絕不隨意刪除欄位。

## 測試資料存取層

- 優先使用 `@DataJpaTest` 配合 Testcontainers 來模擬生產環境。
- 使用日誌驗證 SQL 效率：將 `logging.level.org.hibernate.SQL` 設為 `DEBUG`，並將 `logging.level.org.hibernate.orm.jdbc.bind` 設為 `TRACE` 以觀察參數值。

**請記住**：保持實體簡鍊、查詢意圖明確並縮短事務時長。透過抓取策略與投影防止 N+1 問題，並針對讀取/寫入路徑建立合適的索引。
