一、簡述
一對多關聯映射(one-to-many)
1、在對象模型中,一對多的關聯關係,使用集合表示
比如Classes(班級)和Student(學生)之間是一對多的關係
public class Classes{
private String id;
private String name;
private Set students;
}
public class Student{
publicString id;
publicString name;
}
2、我們以前學過學生對班級是多對一,返過來,班級對學生就是一對多。
3、我們多對一的關係是用戶和組。返過來看,從組這邊來看,就是一對多了。
下面學生的示例是班級和學生。和用戶和組是一樣的。
一個班級有多個學生,這是一對多的關係;返過來,多個學生屬於一個班級,這就是多對一了。
4、建立對象模型
5、這兩個對象模型之間是有關係的。我們現在講的是一對多。一的一端是班級。多的一端是學生。那麼怎麼樣能體現出這種關係呢?
我們在學習多對一時,是在多的一端加上一個字段。這個字段做爲外鍵關聯一的一端。多對一,就是我們在看到學生的時候,能夠知道這個學生是哪個班級的。或者是當我們看到用戶的時候,知道這個用戶是哪個組的。所以在用戶裏面持有組的引用。
6、那麼一對多,就是一個組裏面有多少個用戶。所以要維護這種關係,必須在組裏面持有用戶的集合。
班級和學生也是一樣的。一個班級有多少學生,所以在班級裏面要持有相應的學生的集合。
如下圖
我們用Set,通常用戶Set做映射。
箭頭表示兩者之間是有關係的。
7、上面的是對象模型,那麼這種模型要映射成什麼樣呢?
當我們定義多對一的關係時,在加載多的一端時,能夠把1的一端加載上來。因爲兩者之間是有關係的。
同理,一對多也是一樣的,它要維護這種關係。這種關係就是一對多。一的一端要指向多。
在維護這種關係時,在加載一的時候,就會把一的一端加載上來。
也就是說,在我在加載班級時,這個班級有多少個學生,它會把所有的學生自動查詢上來,放到Set集合裏面。這就是維護這個關係的目的。
8、我們知道,實體類要映射成表。所在下面畫兩個表。
依我們來看,是先有班級。再分配學生。
學生有了,班級有了。要保證知道一個班級有多少學生。
因爲students這個集合是在Classes上,要映射它的時候,那麼我們是要在t_classes表上加一個字段,然後將所有的學生用,表達式表達出來嗎?可是這樣做不符合數據庫的設計範式。
9、所以說,這種關係的維護應該是在t_student這一端。也就是說,在t_student表中加一個字段,這個字段就是classesid。
也就是說,一對多關聯映射,要把兩個表的關聯字段加到多的一端。
10、所以說,一對多與多對一的映射是一樣的。沒有區別。都在多的一端加一個外鍵,指向一的一端。
但是兩者也是有區別的。區別就是關係。如果維護的是多對一,則加載多的時候,能把1加上來。如果維護的是一對多,則加載班級時,能把WHSD1011對應的兩個學生加載上來。
我的理解:對於要相關聯的表來說,如果一個表想要看到對方的表內容,則就要在自己的實體類中持有對方的引用。
如果只有一方看到另一方,就是單向的。
如果要雙方都看到,就要在實體模型中彼此都持有對方的引用。
二、新建項目hibernate_one2many_1(拷貝hibernate_session)這個項目就O了。:我們這個實例還是單向的。只能在加載班級時,把所有的學生加載上來。但是當把學生拿上來的時候,看不到這個學生所在的班級。
三、建立對象模型
1、注意:一對多關聯映射,通常用Set這個集合,那麼爲什麼用Set呢?我們可以這樣理解:Set裏面的對象是不能重複的。當然也可以用其他的。不過一般情況下用Set。
一定要用Set這個接口,而不用HashSet。因爲Hibernate中有延遲加載。實體對象就實現了延遲加載。也就是採用代理的方式。集合也有延遲加載。hibernate中對JDK中的Set集合進行了擴展,也就是實現了這個接口,所以不能用HashSet。
所以要用Set接口。因爲hibernate對Set有相應的實現,對Set進行了擴展。
2、我們的Set裏面就是Student對象的集合。這樣就構成了一對多的關係。
2.1 Classes.java
package com.bjsxt.hibernate;
import java.util.Set;
public class Classes {
private int id;
private String name;
private Set students;
public Set getStudents(){
returnstudents;
}
public voidsetStudents(Set students) {
this.students = students;
}
public int getId() {
returnid;
}
public void setId(intid) {
this.id = id;
}
public String getName(){
returnname;
}
public voidsetName(String name) {
this.name = name;
}
}
2.2Student.java
package com.bjsxt.hibernate;
public class Student {
private int id;
private String name;
public int getId() {
returnid;
}
public void setId(intid) {
this.id = id;
}
public String getName(){
return name;
}
public voidsetName(String name) {
this.name = name;
}
}
四、對象模型建立好之後,就開始寫映射文件,這是hibernate開發的正確思路。
1、在寫映射文件時,先從簡單的寫起。
寫Students.hmb.xml文件。
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<classname="com.bjsxt.hibernate.Student" table="t_student">
<idname="id">
<!-- 主鍵的生成方式不能是uuid。因爲這種生成方式是生成32位16進制的字符串
而我們的實體類中id的類型爲int.所以要修改主鍵的生成方式爲native.就是以數字形式自增 -->
<generator class="native"></generator>
</id>
<property name="name"/>
</class>
</hibernate-mapping>
2、再映射難一點的,Classes.hbm.xml文件如下 :
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mappingpackage="com.bjsxt.hibernate">
<classname="Classes" table="t_classes">
<idname="id">
<generatorclass="native"></generator>
</id>
<property name="name"/>
<!-- 上面爲簡單屬性
下面要看一下集合要如何映射
答:集合要用set標籤來映射
set標籤的名字屬性的值就是Classes類中的集合類型數據的變量名 -->
<setname="students">
<!-- 那麼要如何映射set裏面的東西呢?
我們知道ont2many的映射關係,與many2one的映射是一樣的,要在多的一端加字段的。
但是到目前爲止,在t_student這張表裏面是沒有classid這個字段標識的。
所以要把這個字段加過來,加過來之後,還要做爲外鍵指向t_classes這張表的主鍵
我們用key標籤來實現 -->
<!-- 在key標籤裏面要使用的屬性是列,就是給定在t_student表中的加上的列的名字。
加了key這個標籤後,就會把classid這個字段加入到t_student這張表裏面了,
它做爲外鍵指向t_class表的id字段。 -->
<keycolumn="classid"></key>
<!-- 接下來,採用one-to-many這個標籤,採用這個標籤,一方面是一對多,
另一方面,要用class這個屬性來指定Classes.java類中的students這個集合裏面
到底是什麼元素。我們這個實例裏面是Student對象集合。
一定要指定集合中的元素是什麼類型的對象。-->
<one-to-many class="Student"/>
<!--ont-to-many標籤就代表了students集合裏面是Student對象。
這樣指明之後,classes就能找到student這張表。
因爲student在這裏都寫了。
而且在Student.hbm.xml文件中,也已經映射Student對象映射到哪張表了。
這樣就可以把classid加到表t_student裏面,而且做爲外鍵指向t_classes表的主鍵id. -->
</set>
</class>
</hibernate-mapping>
五、到hibernate.cfg.xml文件中,修改數據庫,並把我們的兩個實體類加到配置文件中
文件內容爲:
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory >
<!-- 數據庫改成hibernate_session-->
<propertyname="hibernate.connection.url">jdbc:mysql://localhost/hibernate_one2many_1</property>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<propertyname="hibernate.connection.username">root</property>
<propertyname="hibernate.connection.password">root</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.show_sql">true</property>
<mapping resource="com/bjsxt/hibernate/Classes.hbm.xml"/>
<mapping resource="com/bjsxt/hibernate/Student.hbm.xml"/>
</session-factory>
</hibernate-configuration>
六、打MySql,創建數據庫
root;
create database hibernate_one2many_1;
use hibernate_one2many_1;
show tables;
七、執行ExportDB.class類,將對象導出爲表
ExportDB.class 文件內容爲:
package com.bjsxt.hibernate;
import org.hibernate.cfg.Configuration;
importorg.hibernate.tool.hbm2ddl.SchemaExport;
//這個類寫完之後,以後就再也不用寫了。
//這個類就是把hibernate的配置文件,轉換成DDL。
public class ExportDB {
publicstatic void main(String[] args){
//讀取hibernate.cfg.xml文件
Configuration cfg=new Configuration().configure(); //hibernate中的一個api,是Configuraton。引入時,將光標定位在Configuration上
//右擊,選擇"Source"命令的"Add Import"命令,找到hibernate的Configuration。
//它是用來讀取hibernate中的配置文件的。即hibernate.cfg.xml文件。(相當於Struts中的
//ActionServlet)
//這樣直接寫 Configuration cfg=newConfiguration();會有問題,因爲
//hibernate有兩個配置文件,一個是property類型的,一個是xml類型的,這樣
//new完,就會默認的讀取property類型的配置文件,這樣就會有問題。所以必須加上它的一個
//方法configure()。
//hibernate中有一個工具類,叫SchemaExport,這個工具類需要傳送configuration.
//這個對象就把我們的類生成(導成)表出來。
SchemaExport export=new SchemaExport(cfg);
export.create(true, true); //script是判斷生成不生成,它實際是生成ddl.
//這兩個參數都設置成true就可以了。
//這個方法的具體含義可以直接看它的api。
//或者關聯上它的源代碼,可以在create上,點擊F3,
//選擇"AttachResource",選擇“ExternalFile",
//再找到F:\javaprogram\Hibernate相關資料\hibernate-3.2.0.ga,就關聯上了。
//export.create(..)是拿到對象,直接生成就可以了。
}
}
八、導出表後,在控制檯顯示:
drop table if exists t_classes
drop table if exists t_student
create table t_classes (id integer not nullauto_increment, name varchar(255), primary key (id))
create table t_student (id integer not nullauto_increment, name varchar(255), classid integer, primary key (id))
注意:在student表中加的字段classid是在寫Classes.hbm.xml映射文件時,key標籤起的作用哦!
alter table t_student add indexFK4B907570C229DCC9 (classid), add constraint FK4B907570C229DCC9 foreign key(classid) references t_classes (id)
這條語句是在t_student這張表中加了約束。也就是classid作爲外鍵參照了t_classes的id字段。
2、到MysQL中
輸入show tables;
desc t_classes;
+-------+--------------+------+-----+---------+----------------+
| Field |Type | Null | Key | Default |Extra |
+-------+--------------+------+-----+---------+----------------+
| id |int(11) | NO | PRI |NULL | auto_increment |
| name | varchar(255) | YES | | NULL | |
+-------+--------------+------+-----+---------+----------------+
輸入命令:
desc t_student;
顯示結果爲:我們發現在t_student這張表中加了一個字段classid。
+---------+--------------+------+-----+---------+----------------+
| Field |Type | Null | Key | Default |Extra |
+---------+--------------+------+-----+---------+----------------+
| id |int(11) | NO | PRI |NULL | auto_increment |
| name | varchar(255) |YES | | NULL | |
| classid |int(11) | YES | MUL |NULL | |
+---------+--------------+------+-----+---------+----------------+
九、所以我們項目上用hibernate進行持久層映射時,最重要的是要發現對象,然後建立對象之間的關係
十、readme.xml文件內容爲:
hibernate 一對多關聯映射。(單向日葵 Classed----->Student)
一對多關聯映射利用了多對一關聯映射原理。
多對一關聯映射,在多的一端加入一個外鍵,指向一的一端。但是它維護的關係是多指向一的。
一對多關聯映射,在多的一端加入一個外鍵,指向一的一端。但是它維護的關係是一指向多的。
也就是說,一對多與多對一映射策略是一樣的,只不過站的角度不同。
hibernate一對多關聯映射——單向(繼。空間不夠了)
十一、接着看存儲與加載
建立單元測試類One2ManyTest
package com.bjsxt.hibernate;
import java.util.HashSet;
import java.util.Set;
import org.hibernate.Session;
import junit.framework.TestCase;
public class One2ManyTest extends TestCase {
}
十二、測試save(保存)
一對多的存儲方式應該與多對一不一樣。一對多存儲的應該是classes。因爲班級與學生的關係是在classes類裏面的students集合。所以應該是先有學生,把學生建立好之後,再放到Classes類的students這個集合裏面。
1、testSave()方法內容如下:
public void testSave(){
Session session=null;
try{
session=HibernateUtils.getSession();
session.beginTransaction();
Student student1=new Student();
student1.setName("菜10");
Student student2=new Student();
student2.setName("容祖兒");
Set<Student> students=new HashSet<Student>();
students.add(student1);
students.add(student2);
Classes classes=new Classes();
classes.setName("尚學堂");
classes.setStudents(students);
session.save(classes);
session.getTransaction().commit();
}catch(Exceptione){
e.printStackTrace();
session.getTransaction().rollback();
}finally{
HibernateUtils.closeSession(session);
}
}
當testSave()方法執行時,會出現這個org.hibernate.TransientObjectException: object references an unsaved transientinstance - save the transient instance before flushing:com.bjsxt.hibernate.Student 異常。
2、正確的保存testSave2()
public void testSave2(){
Session session=null;
try{
session=HibernateUtils.getSession();
session.beginTransaction();
//先建立學生對象集合
Student student1=new Student();
student1.setName("菜10");
session.save(student1);
Student student2=new Student();
student2.setName("容祖兒");
session.save(student2);
//用泛型了
//因爲班級裏的學生是一個集合,所以要構造一個集合出來
Set<Student> students=new HashSet<Student>();
//向集合裏面加數據
students.add(student1);
students.add(student2);
//要知道哪個學生所在的班級,要new 班級出來
Classes classes=new Classes();
classes.setName("尚學堂");
//這個班級裏面有哪些學生,要用set方法加上去。
//這樣這個尚學堂班級裏面就有兩個學生了
classes.setStudents(students);
session.save(classes);
session.getTransaction().commit();
}catch(Exceptione){
e.printStackTrace();
session.getTransaction().rollback();
}finally{
HibernateUtils.closeSession(session);
}
}
2.1執行後出現SQL語句:
insert into t_student (name) values (?)
只保存了學生的name,此時classesid字段值爲null.
insert into t_student (name) values (?)
insert into t_classes (name) values (?)
hibernate又連接發了兩條update,因爲只有兩個學生
update就是要把學生的classesid字段值更新上
根據學生id把classesid更新上。
也就是說,要由班級(一的一端)來維護這種關係:你菜10是我們班的,你容祖兒是我們班的。
在一的一端維護這種關係,會發現很多udpate。因爲班級不知道會有多少學生。
就是說,由班級(一的端)主動記住這種關係
就如記人的名字一樣,就如讓姚明來記我們十三億的人,這個關係要由姚來維護,
他要主動來記,把十三億人的名字全部記下來。這肯定比較困難。因爲由他一個人來更新,來記。
實際上,這種關係在多的一端來維護很容易,就如讓我們記住姚明的名字很容易一樣的。這是在一的端維護關係的一個缺點。另一個缺點是後面說的,如果在Student.hbm.xml映射文件中,將classesid字段設置爲非空的話,則保存時會出錯。
update t_student set classid=? where id=?
update t_student set classid=? where id=?
2.2執行結果數據庫顯示:
mysql> select * from t_classes;
+----+--------+
| id | name |
+----+--------+
| 1 | 尚學堂 |
+----+--------+
1 row in set (0.00 sec)
mysql> select * from t_student;
+----+--------+--------
| id | name |classesid
+----+--------+-------
| 1 | 菜10 | 1
+----+--------+-------
| 2 | 容祖兒 | 1
+----+--------+-------
1 row in set (0.00 sec)
2.3 我們現在分析一下數據庫的結構,如這個測試方法,我們是先保存學生對象集合的。可是這時,這個學生對象裏面只有名字與id,卻沒有classesid .可是表字段裏是有classesid字段的。是在映射的時候加進去的。
可是如我們的測試方法,還是可以正確執行,班級裏面可以加進去兩個學生。這是爲什麼呢?
原因是,我們的classesid字段是可以爲空的。
也就是說,我們先把學生保存進去,可是這時 classesid 字段是沒有值的。等到保存Classes時,再更新這個字段,將班級id值更新(update)上去。
如果我們在此Students.hbm.xml文件加上這樣一條,讓 classesid字段不能爲空,再執行這個方法時,在存儲時就會失敗了,因爲clssesid字段在保存時必須要值,不然就存不進去了。
<set name=”students”>
<key column=”classesid”not-null=”true”/>
<ont-to-manyclass=”com.bjsxt.hibernate.Student”/>
</set>
注:當修改完Students.hbm.xml文件時,要重新執行ExportDB.class文件,因爲這裏數據庫中的表結構發生改變了。所以要重新生成表。
十三、測試加載
public voidtestLoad1(){
Session session=null;
try{
session=HibernateUtils.getSession();
Classes classes=(Classes)session.load(Classes.class,1);
Set students=classes.getStudents();
for(Iterator iter=students.iterator();iter.hasNext();){
Student student=(Student)iter.next();
System.out.println("students.name="+student.getName());
}
session.getTransaction().commit();
}catch(Exceptione){
e.printStackTrace();
session.getTransaction().rollback();
}finally{
HibernateUtils.closeSession(session);
}
}
public void testLoad2(){
Session session=null;
try{
session=HibernateUtils.getSession();
session.beginTransaction();
//注:load與get只能根據主鍵加載,根據別的字段是不可以的。
//第一個參數是相應的類
Classes classes=(Classes)session.load(Classes.class, 1);
System.out.println("classes.name="+classes.getName());
//拿出班級的學生,它是一個集合,然後一個一個輸出學生名字。
Set<Student> students=classes.getStudents();
for(Iterator<Student> iter=students.iterator();iter.hasNext();){
Student student=iter.next();
System.out.println("student.name="+student.getName());
}
session.getTransaction().rollback();
}catch(Exceptione){
e.printStackTrace();
}finally{
HibernateUtils.closeSession(session);
}
}
執行後SQL語句爲:
先根據id到班級裏面來查詢
select classes0_.id as id0_0_, classes0_.name as name0_0_from t_classes classes0_ where classes0_.id=?
班級查上來後,就可以把班級的學生查詢上來。因爲班級維護了一指向多的關係,所以它要找到這個關係字段,把班級的id拿來,然後它到t_student這張表中,找維護兩個表關係的字段,就是classesid=這個班級的學生,把它一個一個的拿上來,它會自動地把這些學生放到學生集合裏面。
select students0_.classesid as classesid1_, students0_.idas id1_, students0_.id as id1_0_, students0_.name as name1_0_ from t_studentstudents0_ where students0_.classesid=?
再回想一下一對多,查詢用戶時,也可以把組加上來。之所以可以這樣,是因爲我們配置了關係,如果沒有在映射文件裏面配置這種關係,則hibernate就不會給我們加上來了。
十四、readme.txt文件
hibernate 一對多關聯映射。(單向日葵Classed----->Student)
一對多關聯映射利用了多對一關聯映射原理。
多對一關聯映射,在多的一端加入一個外鍵,指向一的一端。但是它維護的關係是多指向一的。
一對多關聯映射,在多的一端加入一個外鍵,指向一的一端。但是它維護的關係是一指向多的。
也就是說,一對多與多對一映射策略是一樣的,只不過站的角度不同。
因爲是在一的一端維護的關係(爲什麼知道是在一的一端維護的關係呢?
因爲我們將關係的配置寫到一的一端就是Classes.hbm.xml文件裏面了。
缺點:在一端維護關係的缺點:
*如果將t_student表裏的classesid字段設置爲非空,則無法保存
*因爲在一的一端維護的,所以剛開始建立時,學生是不知道她是哪個班級的。
所以一的一端要發出多餘的update語句。
因爲不是在student這一端維護關係(就像我沒有記住你的電話號,所以找不到你),
所以student不知道自己是哪個班的。所以需要發出多餘的update語句來更新關係。
基於以上的缺點,所以一對多關聯映射通常要做成雙向的。就可以克服這些缺點了。雙向的返過來就是多對一。
爲什麼要做成雙向的,最大的考慮,是將關係交給多的一端來維護,不讓一的一端來做了。
就像不能讓姚明記住十三億人的名字一樣,我們記住姚的就可以了。