多對多的關係可以看到 products、tags 及 product_tags 這 3 張表的關係,每個產品可以有多個標籤、每個標籤可以用在多種產品上,這就是多對多的設計,像這樣的關係就會需要第三方的關聯表來記錄,透過第三方表紀錄雙方的 PK 才能夠將關聯串起。
這邊預計將 Product 資料可以關聯出 Tag 所以 Product 為父實體
建立父實體 Products Entity
@Entity
@Getter
@Setter
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
private String description;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "product_detail_id", referencedColumnName = "id")
private ProductDetails productDetails;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "product")
private List<Review> reviews;
// 多對多關連
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "product_tags",
joinColumns = @JoinColumn(name = "product_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new HashSet<>();
}
因為要關聯透過多對多關連到 Tag 來回傳一個集合,這種透過第三方表格來關聯就必須標示清楚和相關欄位之間的關係,所以要用到 @JoinTable 來關聯,內部有 4 個參數:
name = "product_tags"
就是表示第三方表的名稱joinColumns = @JoinColumn(name = "product_id")
name 表示第三方表的第一個 FKinverseJoinColumns = @JoinColumn(name = "tag_id")
name 表示第第三方表的第二個FKuniqueConstraints = @UniqueConstraint(columnNames = {"product_id", "tag_id"})
這個如果建立 table 有明確定義就可以不用寫,columnNames
是表示 "product_id", "tag_id 兩個關聯 FK 綁一組作為唯一識別第三方表的 PK,避免插入相同組合的值。多對多關聯因 Lombok @Data 註解可能發生錯誤 :
這邊我在實際操作的時候,發現 @Data 註解會導致如果要回傳成 Set 的類型沒辦法成功,導致有些資料抓不到關聯,上網查了一些資料跟詢問 AI 後推測可能錯誤是因為 @Data 內包含的 @EqualsAndHashCode 覆寫 equals() 和 hashCode() 方法所導致。
要解決有查到幾種改法:
建立子實體 Tag Entity
@Entity
@Data
@Table(name = "tags")
@JsonIgnoreProperties({"products"})
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "tags")
private Set<Product> products;
}
因為雙向關聯會導致雙向循環問題,所以這邊用到一個 @JsonIgnoreProperties({"products"}) 來避免 Tag 的資料又關聯回 Product ,如果這邊不需要 products 欄位其實可以直接都拿掉,也可以不用 ignore 註解
得到的 Product 回傳
[
{
"id": 1,
"name": "最後生還者",
"price": 59.99,
"description": "由 Naughty Dog 開發的動作冒險遊戲。",
"productDetails": {
"id": 1,
"developer": "Naughty Dog",
"publisher": "Sony Interactive Entertainment",
"releaseDate": "2013-06-14",
"languageSupport": "English, Japanese, Chinese"
},
"reviews": [
{
"id": 1,
"comment": "這是一款令人驚嘆的遊戲,擁有引人入勝的故事和令人驚嘆的視覺效果!",
"rating": 5
},
{
"id": 11,
"comment": "遊戲不錯,但希望戰鬥系統能更具挑戰性。",
"rating": 4
},
{
"id": 12,
"comment": "每一秒鐘都充滿樂趣,真的是一款精緻的遊戲!",
"rating": 5
}
],
"tags": [
{
"id": 2,
"name": "冒險"
},
{
"id": 3,
"name": "角色扮演"
}
]
},
{
"id": 2,
"name": "巫師3",
"price": 49.99,
"description": "由 CD Projekt Red 開發的開放世界角色扮演遊戲。",
"productDetails": {
"id": 2,
"developer": "CD Projekt Red",
"publisher": "CD Projekt",
"releaseDate": "2015-05-19",
"languageSupport": "English, Chinese, Polish"
},
"reviews": [
{
"id": 2,
"comment": "很棒的遊戲,但某些部分的節奏感覺有點慢。",
"rating": 4
},
{
"id": 13,
"comment": "畫質很好,但劇情有點單調。",
"rating": 3
}
],
"tags": [
{
"id": 7,
"name": "奇幻"
},
{
"id": 3,
"name": "角色扮演"
}
]
},
//......略
]
關於 Spring Data Jpa 的關聯就介紹到這邊,詳細很多實作上的問題都是自己做過才會碰到,查詢這些問題也是很重要,實際解決也更能理解底層原因跟避免下次又發生,下一篇會來介紹一下 Lombok 的註解用法,也剛好對應到這篇的一些問題上面。
Ref:
相關文章也會同步更新我的部落格,有興趣也可以在裡面找其他的技術分享跟資訊。