Hibernate 多對多關(guān)聯(lián)映射
1. 前言
本節(jié)課,咱們一起繼續(xù)聊聊多對多關(guān)聯(lián)映射。通過本節(jié)課程,你將了解到:
- 多對多關(guān)聯(lián)映射的實現(xiàn);
- 雙向多對多關(guān)聯(lián)映射的實現(xiàn)。
2. 多對多關(guān)聯(lián)映射
首先了解表中的多對多關(guān)系,學(xué)生表中的數(shù)據(jù)和課程表中的數(shù)據(jù)就存在多對多關(guān)系。
一名學(xué)生可以選修多門課程,一門課程可以供多名學(xué)生選修。
數(shù)據(jù)庫通過主外鍵的機(jī)制描述表中的數(shù)據(jù)之間的關(guān)系。對于存在多對多關(guān)系的數(shù)據(jù)表,借助于中間表,分拆成兩個一對多(或者多對一)。
中間表的出現(xiàn),完美地表述了學(xué)生數(shù)據(jù)和課程數(shù)據(jù)之間的多對多關(guān)系。
數(shù)據(jù)庫的世界中有學(xué)生表、課程表,自然,Java 程序中就會有學(xué)生實體類、課程實體類。
不對,好像遺漏了什么!
別忘了,表是有 3 張的(不是還有中間表嗎)。那么 Java 程序中的實體類是不是應(yīng)該也要有 3 個:
- 學(xué)生表對應(yīng)的實體類;
- 班級表對應(yīng)的實體類;
- 中間表對應(yīng)的實體類。
至于中間表所對應(yīng)的實體類是否應(yīng)該有:答案是可以有、也可以沒有。
如果中間表僅僅只是記載了學(xué)生和課程的關(guān)系,中間表的角色定位只是一個橋梁。這種情況下,Java 程序中可以不描述中間表結(jié)構(gòu)。
Java 程序中的實體類不僅僅是用來模仿表結(jié)構(gòu),更多是看上了表中的數(shù)據(jù)。
如果中間表除了連接作用,還保存了程序中需要的數(shù)據(jù),則 Java 程序需要一個實體類填充數(shù)據(jù)。如:
針對這 2 種情況,實體類之間的映射關(guān)系會有微妙的變化。
3. 沒有中間表實體類的映射
如果中間表僅僅只是充當(dāng)橋梁作用,沒有程序需要的實質(zhì)性數(shù)據(jù)時,程序中可以沒有中間表對應(yīng)的實體類。
學(xué)生和課程的關(guān)系,直接在學(xué)生類和課程類中體現(xiàn)彼此關(guān)系就可以:
新建課程實體類:
@Entity
public class Course {
private Integer courseId;
private String courseName;
private String courseDesc;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getCourseId() {
return courseId;
}
//省略其它代碼
}
因為一名學(xué)生對應(yīng)多門課程,在學(xué)生實體類中添加集合屬性:
private Set<Course> courses;
完成程序級別上的關(guān)系描述后,還需告訴 Hibernate,實體類中的集合屬性數(shù)據(jù)來自哪一張以及如何獲???
為了把問題簡單化,在學(xué)生實體類中只體現(xiàn)和課程的關(guān)系 ,前面映射內(nèi)容注釋或刪除。
學(xué)生實體類的完整描述:
private Set<Course> courses;
@ManyToMany(targetEntity = Course.class)
@JoinTable(name = "score", joinColumns = @JoinColumn(name = "stuId", referencedColumnName = "stuId"),
inverseJoinColumns = @JoinColumn(name = "courseId", referencedColumnName = "courseId"))
public Set<Course> getCourses() {
return courses;
}
- @ManyToMany 告訴 Hibernate, course 集合中的數(shù)據(jù)來自課程表 ;
- @JoinTable 告訴 Hibernate 獲取課程表中數(shù)據(jù)時需要借助 score 中間表。分別描述中間表和學(xué)生表、課程表的連接字段。
在 Hibernate 主配置文件中修改或添加如下信息:
<property name="hbm2ddl.auto">create</property>
<mapping class="com.mk.po.Student" />
<mapping class="com.mk.po.Course" />
執(zhí)行下面的測試實例:
@Test
public void testGetStuAndCourse() {
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
}
查看 MySql 中的表:
切記把下面的信息修改回來:
<property name="hbm2ddl.auto">update</property>
手工添加測試數(shù)據(jù):
好!通過測試實例見證 Hibernate 的神奇。
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
Student stu=(Student)session.get(Student.class, new Integer(1));
System.out.println("---------------------------");
System.out.println("學(xué)生姓名:"+stu.getStuName());
System.out.println("----------------------------");
System.out.println("學(xué)生選修課程數(shù):"+stu.getCourses().size());
return stu;
}
});
查看控制臺上面的輸出結(jié)果:
Hibernate:
select
student0_.stuId as stuId1_1_0_,
student0_.stuName as stuName2_1_0_,
student0_.stuPassword as stuPassw3_1_0_,
student0_.stuPic as stuPic4_1_0_,
student0_.stuSex as stuSex5_1_0_
from
Student student0_
where
student0_.stuId=?
---------------------------
學(xué)生姓名:Hibernate
----------------------------
Hibernate:
select
courses0_.stuId as stuId1_1_1_,
courses0_.courseId as courseId2_2_1_,
course1_.courseId as courseId1_0_0_,
course1_.courseDesc as courseDe2_0_0_,
course1_.courseName as courseNa3_0_0_
from
score courses0_
inner join
Course course1_
on courses0_.courseId=course1_.courseId
where
courses0_.stuId=?
學(xué)生選修課程數(shù):2
Hibernate 構(gòu)建了兩條 SQL 語句,先是查詢到學(xué)生信息,需要課程信息時,再通過中間表連接到課程表,查詢出課程相關(guān)信息。
可得出結(jié)論:默認(rèn)情況下,Hibernate 使用了延遲加載。如此做,Hibernate 是考慮了性能的。
4. 考慮中間表的映射
命名為 score 的中間表除了維系學(xué)生表和課程表的關(guān)系,還存儲有學(xué)生成績。如果程序中需要成績數(shù)據(jù),則需要創(chuàng)建一個成績實體類。
現(xiàn)在就有了學(xué)生、課程、成績 3 個 實體類。本質(zhì)是映射成兩個多對一(或一對多)的關(guān)系:
- 學(xué)生實體類和成績實體類一對多;
- 課程實體類和成績實體類的一對多:
具體的代碼就不再貼出,大家可參考一對多的課程內(nèi)容。
本節(jié)課不關(guān)心學(xué)生的課程成績是多少,只關(guān)心,如何通過學(xué)生找到課程,或通過課程找到學(xué)生。
有一個地方需要注意:
默認(rèn)情況下,中間表使用課程 ID 和學(xué)生 ID 聯(lián)合做主鍵,也可以提供一個自定義的主鍵。
5. 雙向多對多映射
前面實現(xiàn)了學(xué)生查詢到課程,如何在查詢課程時,查詢到學(xué)生信息。很簡單,在課程 PO 中,添加學(xué)生集合屬性:
// 學(xué)生信息
private Set<Student> students;
同樣使用 @ManyToMany 注解告訴 Hibernate 數(shù)據(jù)源頭及查詢方法:
private Set<Student> students;
@ManyToMany(targetEntity = Student.class, mappedBy = "courses")
public Set<Student> getStudents() {
return students;
}
執(zhí)行下面的測試實例:
HibernateTemplate<Course> hibernateTemplate = new HibernateTemplate<Course>();
hibernateTemplate.template(new Notify<Course>() {
@Override
public Course action(Session session) {
Course course=(Course)session.get(Course.class, new Integer(1));
System.out.println("---------------------------");
System.out.println("課程名稱:"+course.getCourseName());
System.out.println("----------------------------");
System.out.println("選修此課程的學(xué)生數(shù):"+course.getStudents().size());
return course;
}
});
控制臺輸出結(jié)果:
Hibernate:
select
course0_.courseId as courseId1_0_0_,
course0_.courseDesc as courseDe2_0_0_,
course0_.courseName as courseNa3_0_0_
from
Course course0_
where
course0_.courseId=?
---------------------------
課程名稱:java
----------------------------
Hibernate:
select
students0_.courseId as courseId2_0_1_,
students0_.stuId as stuId1_2_1_,
student1_.stuId as stuId1_1_0_,
student1_.stuName as stuName2_1_0_,
student1_.stuPassword as stuPassw3_1_0_,
student1_.stuPic as stuPic4_1_0_,
student1_.stuSex as stuSex5_1_0_
from
score students0_
inner join
Student student1_
on students0_.stuId=student1_.stuId
where
students0_.courseId=?
選修此課程的學(xué)生數(shù):2
同樣,Hibernate 采用的是延遲加載模式。先查詢課程信息,當(dāng)開發(fā)者需要學(xué)生信息時,才構(gòu)建一條利用中間表進(jìn)入學(xué)生表的 SQL 查詢到學(xué)生信息。
可通過學(xué)生表查詢到課程表 ,也能從課程表查詢到學(xué)生表,這種多對多關(guān)聯(lián)映射稱為雙向映射關(guān)聯(lián)。
6. 小結(jié)
本節(jié)課和大家一起聊了聊多對多映射的實現(xiàn)。多對多是一種常見的關(guān)系。
多對多的映射實現(xiàn)可以有 2 種方案。使用中間表映射,或不使用中間表映射。
下一節(jié)課,繼續(xù)講解多對多映射中的添加、更新級聯(lián)操作,相信會給你帶來更多的驚喜。