JPA 提供兩種標準方式映射複合主鍵:
@Id 欄位對應,適合既有資料庫或需獨立重用 PK 類。本文以「小時統計表」為例,PK = (stat_date, path_id, type),完整示範兩種寫法與取捨。
@Embeddable
public class HourDataPK implements Serializable {
@Column(name = "stat_date")
@Temporal(TemporalType.DATE)
private LocalDate statDate; // 建議用 Java 8 Time API
@Column(name = "path_id")
private Long pathId;
@Column(name = "type")
private Integer type;
// 無參建構子、getter/setter、equals/hashCode 略
// equals/hashCode 必須包含所有 PK 欄位
}
@Entity
@Table(name = "TB_HOUR_DATA")
public class HourData implements Serializable {
@EmbeddedId
private HourDataPK pk; // 單一屬性持有複合 PK
@Column(name = "value", length = 512)
private String value;
// 業務便利方法
public int[] getHours() { /* 解析 value 為 int[24] */ }
public void setHours(int[] hours) { /* 組裝 value */ }
// getter/setter 略
}
// 查詢
HourDataPK pk = new HourDataPK();
pk.setStatDate(LocalDate.of(2024, 1, 15));
pk.setPathId(1001L);
pk.setType(1);
HourData data = em.find(HourData.class, pk);
// 新增
HourData newData = new HourData();
HourDataPK newPk = new HourDataPK();
newPk.setStatDate(LocalDate.now());
newPk.setPathId(2002L);
newPk.setType(2);
newData.setPk(newPk);
newData.setValue("0,1,2,3,...");
em.persist(newData);
// PK 類不變,但不加 @Embeddable
public class HourDataPK implements Serializable {
private LocalDate statDate;
private Long pathId;
private Integer type;
// equals/hashCode 同前
}
@Entity
@Table(name = "TB_HOUR_DATA")
@IdClass(HourDataPK.class) // 指定 PK 類
public class HourData implements Serializable {
@Id
@Column(name = "stat_date")
@Temporal(TemporalType.DATE)
private LocalDate statDate;
@Id
@Column(name = "path_id")
private Long pathId;
@Id
@Column(name = "type")
private Integer type;
@Column(name = "value", length = 512)
private String value;
// getter/setter 略
}
差異:查詢時 em.find(HourData.class, pk) 依然可用,但 Entity 內部沒有單一 pk 屬性,需自行組裝 PK 物體。
@Embeddable
public class OrderItemPK implements Serializable {
@ManyToOne
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
// equals/hashCode 需比較 order.id + product.id
}
⚠️ 陷阱:PK 類若含關聯,equals/hashCode 必須比較關聯的 主鍵值 而非物件參考,否則 em.find() 會失效。
public interface HourDataRepository extends JpaRepository {
// 自動支援 findById(HourDataPK)、existsById、deleteById
List findByPkStatDateBetween(LocalDate start, LocalDate end);
@Query("select h from HourData h where h.pk.pathId = :pid and h.pk.type = :type")
List findByPathAndType(@Param("pid") Long pid, @Param("type") Integer type);
}
@Embeddable PK 類,複製原 @IdClass 欄位。@EmbeddedId 屬性,移除多個 @Id。find/insert/update/delete 全流程。| 場景 | 推薦 | 理由 |
|---|---|---|
| 新專案、綠地開發 | @EmbeddedId | 物件導向、單一 PK 物件、程式碼乾淨 |
| 既有資料庫、大量原生 SQL、PK 類需共用 | @IdClass | Entity 結構接近資料表、PK 類可獨立引用 |
| PK 含關聯 (@ManyToOne) | @EmbeddedId | 關聯導航更自然 |
| Spring Data JPA | 皆可 | Repository 介面自動適配兩種模式 |
複合主鍵映射的核心在於 PK 類正確實作 equals/hashCode/Serializable 以及 欄位對應精確。@EmbeddedId 更符合現代 JPA 風格,建議新專案優先採用;既有專案若無痛點,@IdClass 也能穩定運行。選定後貫徹專案規範,避免混用造成認知負擔。