iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0

https://ithelp.ithome.com.tw/upload/images/20240904/20150977s7zH4hK4Ja.png

多對多 N:N

多對多的關係可以看到 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 個參數:

  1. name = "product_tags" 就是表示第三方表的名稱
  2. joinColumns = @JoinColumn(name = "product_id") name 表示第三方表的第一個 FK
  3. inverseJoinColumns = @JoinColumn(name = "tag_id") name 表示第第三方表的第二個FK
  4. uniqueConstraints = @UniqueConstraint(columnNames = {"product_id", "tag_id"}) 這個如果建立 table 有明確定義就可以不用寫,columnNames是表示 "product_id", "tag_id 兩個關聯 FK 綁一組作為唯一識別第三方表的 PK,避免插入相同組合的值。

多對多關聯因 Lombok @Data 註解可能發生錯誤 :

這邊我在實際操作的時候,發現 @Data 註解會導致如果要回傳成 Set 的類型沒辦法成功,導致有些資料抓不到關聯,上網查了一些資料跟詢問 AI 後推測可能錯誤是因為 @Data 內包含的 @EqualsAndHashCode 覆寫 equals() 和 hashCode() 方法所導致。

要解決有查到幾種改法:

  • 把 Set 改成 List 可以直接解決,但通常多對多關聯會建議使用 Set 可以避免重複資料並且有更好效能
  • 不使用 @Data 改成比較精準像上面改成 @Getter @Setter ,針對這些住寫後面會有篇文章詳細說明 Lombok 的註解
  • 多加上 @EqualsAndHashCode(of = "id")

建立子實體 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:

相關文章也會同步更新我的部落格,有興趣也可以在裡面找其他的技術分享跟資訊。


上一篇
Day 12 - Spring Data JPA (4)資料庫關聯 1 : N
下一篇
Day 14 - Lombok
系列文
關於我和 Spring Boot 變成家人的那件事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言