双向多对多的关联关系
双向多对多的关联关系(抽象成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 }