雙向多對多的關聯關系
雙向多對多的關聯關系(抽象成A-B)具體體現:A中有B的集合的引用,同時B中也有對A的集合的引用。A、B兩個實體對應的數據表靠一張中間表來建立連接關系。
同時我們還知道,雙向多對多的關聯關系可以拆分成三張表,兩個雙向多對一關聯關系。拆分以后還是有一張中間表,其好處就是可以在中間表中添加某些屬性用作其它。這個后面會講解。而單純的雙向多對多關聯關系的中間表有兩個外鍵列,無法增加其它屬性。
本節只講單純的雙向多對多關聯關系。從例子講解配置方法和原理:
有“商品Item”和“類別Category”兩個實體類。一中商品可以屬於多種類別,同時一種類別可以包含多種商品,這是一個典型的雙向多對多關聯關系。“雙邊多對多”的關系體現在Category中有對Item的集合的引用,反過來也是一樣的,Item中有對Category的集合的引用。從如下Item和Category屬性定義可以很清晰的理解:
List_1. Category中有對Item的集合的引用
@Table(name="t_category") @Entity public class Category { private Integer id; private String name;
//Category中有對Item的集合的引用 private Set<Item> itemsSet = new HashSet<Item>(); //省略getter、setter... }
List_2. Item中同樣有對Category的集合的引用
@Table(name="t_item") @Entity public class Item { private Integer id; private String name;
//Item中有對Category的集合的引用 private Set<Category> categoriesSet = new HashSet<Category>(); //省略getter、setter... }
假設Category實體對應的數據表為t_category,Item實體對應的數據表為t_item。中間的連接表為category_item。下面講講中間表是如何表達這種多對多的關聯關系的,下圖是一個關聯表:
Figure_1. 多對多關聯關系實例
從Figure_1中可以看出,中間表只有兩個外鍵列CATEGORY_ID和ITEM_ID。其中CATEGORY_ID參考t_category的主鍵列ID_ID,ITEM_ID則參考t_item的外鍵列ID。從中間表我們很容易看出以下的關聯關系:
先看category的對item的關聯情況:
①、category(4)中對item的集合itemsSet包含了2個item實體對象:item(1)和item(2)。 為了描述方便item(1)代表id=1的Item實體對象。
②、category(3)中的itemsSet包含了1個item實體對象:item(2)。
再看看item對category的關聯情況:
③、item(1)中的categoriesSet包含了1個category實體對象:category(4)。
④、item(2)中的categoriesSet包含了2個category實體對象:category(3)和category(4)。
雙向多對多關聯關系的映射細節
更多的關於數據庫基礎的知識可以參考數據庫書籍。下面講講JPA的實體類中如何配置這種映射關系。映射細節如下:
①、雙向多對多關聯關系的映射指的就是對實體雙方的集合屬性的映射;
eg. Category實體類中的itemsSet屬性,Item實體類中的CategoriesSet屬性
②、雙向多對多關聯關系的實體雙方是對稱的,可以選擇任意一方實體類作為映射主體來完成關鍵映射過程;
eg. 在Category和Item中我們選擇Category作為映射主體類來完成關鍵的映射過程,主要就是對Category類中的itemsSet屬性使用注解完成映射。
③、在非映射主體類中只需要簡單的使用@ManyToMany(mappedBy="xxx")來指定由對方的哪個屬性完成映射關系(xxx是屬性名字)
eg. 非映射主體類Item中使用@ManyToMany(mappedBy="itemsSet")來指定由對方(Category實體類)的itemsSet屬性完成映射關系
從上面的①~③我們知道,映射主要在映射主體類中完成,而非主體類的映射過程十分簡單。下面就詳細講解主體類中的映射步驟:
a、對映射主體的集合屬性(或其getter方法)使用@ManyToMany注解,表明是多對多關聯關系
eg. Category實體類中的getItemsSet()方法上使用@ManyToMany注解,當然可以設置該注解的fetch等屬性來修改默認策略(后面講解)
b、然后,在集合屬性的getter方法上使用@JoinTable注解來映射中間表與兩個實體類對應數據表的外鍵參考關系,下面講講該注解的屬性
- name屬性:用於指定中間表數據表的表名(eg. name="category_item"指定中間表的表名為category_item)
- joinColumns屬性:該注解用於指定映射主體類與中間表的映射關系。從javadoc中可以看到該屬性的類型是JoinColumn[],也就是說是一個@JoinColumn注解的集合。其中,@JoinColumn注解的name屬性用於指定中間表的一個外鍵列的列名,該外鍵列參考映射主體類對應數據表的的主鍵列(如果該主鍵列的列名不是ID的時候,需要用referencedColumnName屬性指定主鍵列的列名。如Category中的主鍵為“ID_ID”);
- inverseJoinColumns屬性:該注解用於指定對方(非映射主體類)實體類與中間表的映射關系。它也是一個JoinColumn[]類型。該屬性的用法與joinColumns是一致的。
下面用一個映射主體類的實例說明上面的過程,Category作為映射主體,其有一個Item實體的集合的引用itemsSet屬性。我們在其getter方法上完成映射:
List_3. 映射主體類Category的映射過程
@JoinTable(name="category_item", joinColumns={@JoinColumn(name="CATEGORY_ID", referencedColumnName="ID_ID")}, inverseJoinColumns={@JoinColumn(name="ITEM_ID", referencedColumnName="ID")}) @ManyToMany public Set<Item> getItemsSet() { return itemsSet; }
①、注解@ManyToMany指示多對多的關聯關系(因為一對多也是一個集合,所以要用注解來進行區分)
②、@JoinTable指示中間表如何映射,該注解有三個屬性:name、joinColumns、inverseJoinColumns。
- name屬性指定了中間表的表名為category_item;
- joinColumns用於映射本實體類(Category)對應數據表與中間表如何進行映射。@JoinColumn的name屬性指定了中間表的一個外鍵列,且列名為CATEGORY_ID,該外鍵類參考本實體類對應數據表的主鍵列(主鍵列的列名由@JoinColumn的referencedColumName屬性進行指定,這里指定為“ID_ID”。后面會說到本實體類的數據表的外鍵列的列名為ID_ID);
- inverseJoinColumns屬性用於映射對方實體類(Item)數據表與中間表的映射關系。其配置方法與joinColumns相同。
③、在對方實體類中的映射很簡單,使用@ManyToMany(mappedBy="itemsSet")來指定由映射主體類的itemsSet屬性(或其getter方法)完成映射過程。也就是上面@JoinTable的inverseJoinColumns屬性完成。非映射主體類Item一方的映射細節如List_4:
List_4. 非映射主體類的映射細節
@ManyToMany(mappedBy="itemsSet") public Set<Category> getCategoriesSet() { return categoriesSet; }
下面用圖解的形式將映射主體的配置項與創建好的數據表進行對應起來,如Figure_2:
Figure_2. 下圖中紫色代表映射主體相關,藍色代表非映射主體相關
雙向多對多關聯關系的默認行為
默認檢索策略和修改:
“雙向多對多”中的“多”體現在實體雙方實體類中都有一個集合屬性,用前面講解的結論得到“默認情況下,對集合屬性的檢索采用延遲加載”。所以,默認情況下,雙向多對多關聯關系中對集合的檢索也采用延遲加載。可以通過設置@ManyToMany(fetch=FetchType.EAGER)將檢索策略修改為立即加載策略(一般情況下不建議這么做)。
注意區分“解除關聯關系”和“刪除實體對象”這兩個概念和不同的處理方法:
①、解除關聯關系,其實際效果是刪除中間表中的某條記錄,而實體類對應數據表中的記錄不會被刪除。具體做法是調用集合屬性的remove方法,如下:
List_5. 解除關聯關系
Category ctg = em.find(Category.class, 3); Item item = ctg.getItemsSet().iterator().next(); /** * 解除關聯關系調用集合對象的remove方法 * 集合的remove方法會刪除的是關聯關系,也就是刪除中間表的某條記錄 * 但是,它不會刪除實體類對應數據表中的記錄 */ ctg.getItemsSet().remove(item);
②、刪除實體對象,這個和前面說的刪除操作沒有區別,同樣是調用EntityManager的remove方法。但是,要注意的是由於中間表會對實體類對象的記錄有引用關系,所以,在刪除實體類記錄之前先要解除所有和該記錄相關的關聯關系。否則,無法完成刪除操作(除非,修改刪除操作的默認行為)。
雙向多對多關聯關系相關的知識講解完畢。下面列出實驗代碼:
List_6. Category實體類的定義及映射(主鍵列的列名為ID_ID)
1 package com.magicode.jpa.doubl.many2many; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.Column; 7 import javax.persistence.Entity; 8 import javax.persistence.GeneratedValue; 9 import javax.persistence.GenerationType; 10 import javax.persistence.Id; 11 import javax.persistence.JoinColumn; 12 import javax.persistence.JoinTable; 13 import javax.persistence.ManyToMany; 14 import javax.persistence.Table; 15 16 @Table(name="t_category") 17 @Entity 18 public class Category { 19 20 private Integer id; 21 private String name; 22 23 private Set<Item> itemsSet = new HashSet<Item>(); 24 25 /** 26 * 專門將主鍵列的列名設置為 ID_ID 27 */ 28 @Column(name="ID_ID") 29 @GeneratedValue(strategy=GenerationType.AUTO) 30 @Id 31 public Integer getId() { 32 return id; 33 } 34 35 /** 36 * 1、多對多關聯關系需要建立一個中間表,所以要用@JoinTable注解來設置中間表的映射關系。 37 * 注解@JoinTable的幾點說明: 38 * ①、name屬性指定了中間表的表名; 39 * ②、joinColumns屬性映射當前實體類中的“多”(集合)在中間表的映射關系,該屬性是JoinColumn[]類型。所以, 40 * 要用@JoinColumn注解的集合為其進行賦值。同時,@JoinColumn注解中name指定中間表 41 * 的外鍵列的列名,referencedColumnName指定該外鍵列參照當前實體類對應數據表的那個列的列名。 42 * 下面注解的意思是:中間表的外鍵列CATEGORY_ID引用當前實體類所對應數據表的ID_ID列(通常是主鍵列)。 43 * ③、inverseJoinColumns屬性用於映射對方實體類中的“多”在中間表的映射關系。作用和joinColumns 44 * 一致。 45 * 注解的意思是:中間表的外鍵列ITEM_ID引用Item實體類對應數據表的ID列(ID是列名) 46 * 47 * 2、使用@ManyToMany映射多對多的關聯關系。 48 */ 49 @JoinTable(name="category_item", 50 joinColumns={@JoinColumn(name="CATEGORY_ID", referencedColumnName="ID_ID")}, 51 inverseJoinColumns={@JoinColumn(name="ITEM_ID", referencedColumnName="ID")}) 52 @ManyToMany 53 public Set<Item> getItemsSet() { 54 return itemsSet; 55 } 56 57 @Column(name="NAME") 58 public String getName() { 59 return name; 60 } 61 62 public void setId(Integer id) { 63 this.id = id; 64 } 65 66 public void setName(String name) { 67 this.name = name; 68 } 69 70 public void setItemsSet(Set<Item> itemsSet) { 71 this.itemsSet = itemsSet; 72 } 73 74 }
List_7. Item實體中關聯關系的映射
1 package com.magicode.jpa.doubl.many2many; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.Column; 7 import javax.persistence.Entity; 8 import javax.persistence.GeneratedValue; 9 import javax.persistence.GenerationType; 10 import javax.persistence.Id; 11 import javax.persistence.ManyToMany; 12 import javax.persistence.Table; 13 14 @Table(name="t_item") 15 @Entity 16 public class Item { 17 18 private Integer id; 19 private String name; 20 21 private Set<Category> categoriesSet = new HashSet<Category>(); 22 23 @Column(name="ID") 24 @GeneratedValue(strategy=GenerationType.AUTO) 25 @Id 26 public Integer getId() { 27 return id; 28 } 29 30 @Column(name="NAME", length=25) 31 public String getName() { 32 return name; 33 } 34 35 /** 36 * 使用@ManyToMany映射雙向關聯關系。作為非映射主體一方,只需要簡單的 37 * 配置該注解的mappedBy="xxx"即可。xxx是對方實體(映射主體)中集合 38 * 屬性的名稱。表示由對方主體的哪個屬性來完成映射關系。 39 */ 40 @ManyToMany(mappedBy="itemsSet") 41 public Set<Category> getCategoriesSet() { 42 return categoriesSet; 43 } 44 45 public void setId(Integer id) { 46 this.id = id; 47 } 48 49 public void setName(String name) { 50 this.name = name; 51 } 52 53 public void setCategoriesSet(Set<Category> categoriesSet) { 54 this.categoriesSet = categoriesSet; 55 } 56 57 }
List_8. 測試方法
1 package com.magicode.jpa.doubl.many2many; 2 3 import javax.persistence.EntityManager; 4 import javax.persistence.EntityManagerFactory; 5 import javax.persistence.EntityTransaction; 6 import javax.persistence.Persistence; 7 8 import org.junit.After; 9 import org.junit.Before; 10 import org.junit.Test; 11 12 public class DoubleMany2ManyTest { 13 14 private EntityManagerFactory emf = null; 15 private EntityManager em = null; 16 private EntityTransaction transaction = null; 17 18 @Before 19 public void before(){ 20 emf = Persistence.createEntityManagerFactory("jpa-1"); 21 em = emf.createEntityManager(); 22 transaction = em.getTransaction(); 23 transaction.begin(); 24 } 25 26 @After 27 public void after(){ 28 transaction.commit(); 29 em.close(); 30 emf.close(); 31 } 32 33 @Test 34 public void testPersist(){ 35 Category ctg1 = new Category(); 36 ctg1.setName("ctg-1"); 37 38 Category ctg2 = new Category(); 39 ctg2.setName("ctg-2"); 40 41 Item item1 = new Item(); 42 item1.setName("item-1"); 43 44 Item item2 = new Item(); 45 item2.setName("item-2"); 46 47 //建立關聯關系 48 ctg1.getItemsSet().add(item1); 49 ctg1.getItemsSet().add(item2); 50 ctg2.getItemsSet().add(item1); 51 ctg2.getItemsSet().add(item2); 52 53 item1.getCategoriesSet().add(ctg1); 54 item1.getCategoriesSet().add(ctg2); 55 item2.getCategoriesSet().add(ctg1); 56 item2.getCategoriesSet().add(ctg2); 57 58 //持久化操作 59 em.persist(item1); 60 em.persist(item2); 61 em.persist(ctg1); 62 em.persist(ctg2); 63 } 64 65 @Test 66 public void testFind(){ 67 Category ctg = em.find(Category.class, 3); 68 System.out.println(ctg.getItemsSet().size()); 69 70 // Item item = em.find(Item.class, 1); 71 // System.out.println(item.getCategoriesSet().size()); 72 } 73 74 @Test 75 public void testRemove(){ 76 Category ctg = em.find(Category.class, 3); 77 78 Item item = ctg.getItemsSet().iterator().next(); 79 /** 80 * 集合的remove方法會刪除的是關聯關系,也就是刪除中間表的某條記錄 81 */ 82 ctg.getItemsSet().remove(item); 83 84 /** 85 * 要刪除Category或者是Item實體對應數據表的某條記錄要用em.remove方法 86 * 當然,要刪除的記錄不能被中間表引用,否則會刪除失敗 87 */ 88 89 } 90 }