本文包括:
1、一對多結構的準備
2、雙向關聯與單向關聯
3、級聯保存
4、級聯刪除
5、cascade 屬性——級聯
6、inverse 屬性——放棄外鍵的維護
7、多對多結構的準備
8、多對多結構的級聯
1、一對多結構的準備
需求分析:假設客戶和聯系人是一對多的關系,所以要在有客戶的情況下,要完成聯系人的添加保存操作。
-
回想數據庫相關的知識,在建表的時候要注意客戶與聯系人的關系,一個客戶可以有多個聯系人,而聯系人必須依賴客戶而存在,所以客戶是“一”方,聯系人是“多”方。相應地,在數據庫中的“聯系人”表中,應增加一個字段,并且設為“客戶”表的外鍵。
-
客戶表:
CREATE TABLE `cst_customer` ( `cust_id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '客戶編號(主鍵)', `cust_name` varchar(32) NOT NULL COMMENT '客戶名稱(公司名稱)', `cust_user_id` bigint(32) DEFAULT NULL COMMENT '負責人id', `cust_create_id` bigint(32) DEFAULT NULL COMMENT '創建人id', `cust_source` varchar(32) DEFAULT NULL COMMENT '客戶信息來源', `cust_industry` varchar(32) DEFAULT NULL COMMENT '客戶所屬行業', `cust_level` varchar(32) DEFAULT NULL COMMENT '客戶級別', `cust_linkman` varchar(64) DEFAULT NULL COMMENT '聯系人', `cust_phone` varchar(64) DEFAULT NULL COMMENT '固定電話', `cust_mobile` varchar(16) DEFAULT NULL COMMENT '移動電話', PRIMARY KEY (`cust_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
聯系人表:
CREATE TABLE `cst_linkman` ( `lkm_id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '聯系人編號(主鍵)', `lkm_name` varchar(16) DEFAULT NULL COMMENT '聯系人姓名', `lkm_cust_id` bigint(32) NOT NULL COMMENT '客戶id', `lkm_gender` char(1) DEFAULT NULL COMMENT '聯系人性別', `lkm_phone` varchar(16) DEFAULT NULL COMMENT '聯系人辦公電話', `lkm_mobile` varchar(16) DEFAULT NULL COMMENT '聯系人手機', `lkm_email` varchar(64) DEFAULT NULL COMMENT '聯系人郵箱', `lkm_qq` varchar(16) DEFAULT NULL COMMENT '聯系人qq', `lkm_position` varchar(16) DEFAULT NULL COMMENT '聯系人職位', `lkm_memo` varchar(512) DEFAULT NULL COMMENT '聯系人備注', PRIMARY KEY (`lkm_id`), KEY `FK_cst_linkman_lkm_cust_id` (`lkm_cust_id`), CONSTRAINT `FK_cst_linkman_lkm_cust_id` FOREIGN KEY (`lkm_cust_id`) REFERENCES `cst_customer` (`cust_id`) ON DELETE NO ACTION ON UPDATE NO ACTION ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-
-
JavaBean 代碼(省略 get 和 set 方法):
-
客戶的 JavaBean 如下
public class Customer { private Long cust_id; private String cust_name; private Long cust_user_id; private Long cust_create_id; private String cust_source; private String cust_industry; private String cust_level; private String cust_linkman; private String cust_phone; private String cust_mobile; // 注意:在“一”方的 JavaBean 中要添加 set 集合! private Set<Linkman> linkmans = new HashSet<Linkman>(); }
-
聯系人的 JavaBean 如下:
public class Linkman { private Long lkm_id; private String lkm_name; private String lkm_gender; private String lkm_phone; private String lkm_mobile; private String lkm_email; private String lkm_qq; private String lkm_position; private String lkm_memo; // 注意:這里不寫外鍵字段而寫 Customer 對象,方便訪問客戶對象,而且是 Hibernate 要求的! private Customer customer; // 注意:千萬不要 new! }
-
-
編寫客戶和聯系人的映射配置文件(注意一對多的配置編寫)
-
客戶的映射配置文件如下:
<class name="com.itheima.domain.Customer" table="cst_customer"> <id name="cust_id" column="cust_id"> <generator class="native"/> </id> <property name="cust_name" column="cust_name"/> <property name="cust_user_id" column="cust_user_id"/> <property name="cust_create_id" column="cust_create_id"/> <property name="cust_source" column="cust_source"/> <property name="cust_industry" column="cust_industry"/> <property name="cust_level" column="cust_level"/> <property name="cust_linkman" column="cust_linkman"/> <property name="cust_phone" column="cust_phone"/> <property name="cust_mobile" column="cust_mobile"/> <!-- 配置一方 --> <!-- set標簽name屬性:表示集合的名稱 --> <set name="linkmans"> <key column="lkm_cust_id"/> <one-to-many class="com.itheima.domain.Linkman"/> </set> </class>
-
聯系人的映射配置文件如下:
<class name="com.itheima.domain.Linkman" table="cst_linkman"> <id name="lkm_id" column="lkm_id"> <generator class="native"/> </id> <property name="lkm_name" column="lkm_name"/> <property name="lkm_gender" column="lkm_gender"/> <property name="lkm_phone" column="lkm_phone"/> <property name="lkm_mobile" column="lkm_mobile"/> <property name="lkm_email" column="lkm_email"/> <property name="lkm_qq" column="lkm_qq"/> <property name="lkm_position" column="lkm_position"/> <property name="lkm_memo" column="lkm_memo"/> <!-- 先配置多方 name 當前JavaBean中的屬性 class 屬性的全路徑 column 外鍵的字段 --> <many-to-one name="customer" class="com.itheima.domain.Customer" column="lkm_cust_id"/> </class>
-
2、雙向關聯與單向關聯
-
雙向關聯:
-
假設現在要保存一個客戶,而且需要同時保存聯系人字段,假設代碼如下:
Customer c1 = new Customer(); c1.setCust_name("美美"); Linkman l1 = new Linkman(); l1.setLkm_name("熊大"); Linkman l2 = new Linkman(); l2.setLkm_name("熊二"); // 演示雙向關聯 c1.getLinkmans().add(l1); c1.getLinkmans().add(l2); l1.setCustomer(c1); l2.setCustomer(c1); session.save(l1); session.save(l2); session.save(c1);
執行代碼,程序正常運行,查詢數據庫,客戶表新增一條記錄,聯系人表新增兩條記錄。
-
注意:c1 一定要最后保存,否則會報錯,因為瞬時態 c1 持有瞬時態 l1、l2,必須要 l1、l2 轉變為持久態,c1 才能轉變為持久態。
關于持久化類的三種狀態(瞬時態、持久態、托管態)及其之間的相互轉化參考:http://www.lxweimin.com/p/1a6ca1993b16
由此可見,雙向關聯是最淺顯易懂的,但代碼也最復雜。
-
-
單向關聯:
-
如果只想在客戶表新增記錄,而聯系人表不變,這就叫單向關聯。
Customer c1 = new Customer(); c1.setCust_name("美美"); // 創建2個聯系人 Linkman l1 = new Linkman(); l1.setLkm_name("熊大"); Linkman l2 = new Linkman(); l2.setLkm_name("熊二"); // 單向關聯 c1.getLinkmans().add(l1); c1.getLinkmans().add(l2); // 保存數據 session.save(c1);
-
如果不配置級聯保存(見下節),則程序出現異常,異常第一行如下:
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.itheima.domain.Linkman
簡單分析:在調用 save 方法時,c1 要由瞬時態轉變為持久態,而 l1、l2仍然是瞬時態,而 c1 持有 l1、l2,所以程序出錯。
-
3、級聯保存
-
級聯保存
-
測試:如果現在代碼只插入其中的一方的數據(單向關聯)
如果只保存其中的一方的數據,那么程序會拋出異常。
如果想完成只保存一方的數據,并且把相關聯的數據都保存到數據庫中,那么需要配置級聯!!
級聯保存是方向性
-
級聯保存效果
級聯保存:保存一方同時可以把關聯的對象也保存到數據庫中!!
使用
cascade="save-update"
-
客戶級聯聯系人
-
如果想在保存客戶時,同時也保存聯系人,在客戶的配置文件中這樣編寫:
<set name="linkmans" cascade="save-update"> <!-- 需要出現子標簽 --> <!-- 外鍵的字段 --> <key column="lkm_cust_id"/> <one-to-many class="com.itheima.domain.Linkman"/> </set>
-
再執行如下代碼,正常運行,客戶表新增一條記錄,聯系人表新增兩條記錄。
Customer c1 = new Customer(); c1.setCust_name("美美"); // 創建2個聯系人 Linkman l1 = new Linkman(); l1.setLkm_name("熊大"); Linkman l2 = new Linkman(); l2.setLkm_name("熊二"); // 單向關聯 c1.getLinkmans().add(l1); c1.getLinkmans().add(l2); // 保存數據 session.save(c1);
-
-
聯系人級聯客戶
-
同樣地,如果想在保存聯系人時,同時也保存客戶,在聯系人的配置文件中這樣編寫:
<!-- 先配置多方 name 當前JavaBean中的屬性 class 屬性的全路徑 column 外鍵的字段 --> <many-to-one name="customer" class="com.itheima.domain.Customer" column="lkm_cust_id" cascade="save-update"/>
-
再執行如下代碼,正常運行,客戶表新增一條記錄,聯系人表新增兩條記錄。
Customer c1 = new Customer(); c1.setCust_name("美美"); // 創建2個聯系人 Linkman l1 = new Linkman(); l1.setLkm_name("熊大"); Linkman l2 = new Linkman(); l2.setLkm_name("熊二"); // 使用聯系人關聯客戶 l1.setCustomer(c1); l2.setCustomer(c1); // 保存 session.save(l1); session.save(l2);
-
-
客戶與聯系人互相級聯保存
如果在兩個配置文件中都配置
cascade="save-update"
,那么兩者互相級聯。-
測試如下代碼,發現程序正常運行,客戶表新增一條記錄,聯系人表新增兩條記錄。
Customer c1 = new Customer(); c1.setCust_name("美美"); // 創建2個聯系人 Linkman l1 = new Linkman(); l1.setLkm_name("熊大"); Linkman l2 = new Linkman(); l2.setLkm_name("熊二"); l1.setCustomer(c1); c1.getLinkmans().add(l2); session.save(l1);
-
4、級聯刪除
- 級聯刪除
-
假設現在要刪除某個客戶,在含有外鍵約束的情況下,是不會成功的。
delete from customers where cid = 1;
error:
[Err] 1451 - Cannot delete or update a parent row: a foreign key constraint fails (`hibernate_day03`.`cst_linkman`, CONSTRAINT `FK_cst_linkman_lkm_cust_id` FOREIGN KEY (`lkm_cust_id`) REFERENCES `cst_customer` (`cust_id`) ON DELETE NO ACTION ON UPDATE NO ACTION)
-
如果使用 Hibernate 直接刪除客戶的時候,測試發現是可以刪除的,這是為什么呢?我們直接看控制臺的輸出:
Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_name as cust_nam2_0_0_, customer0_.cust_user_id as cust_use3_0_0_, customer0_.cust_create_id as cust_cre4_0_0_, customer0_.cust_source as cust_sou5_0_0_, customer0_.cust_industry as cust_ind6_0_0_, customer0_.cust_level as cust_lev7_0_0_, customer0_.cust_linkman as cust_lin8_0_0_, customer0_.cust_phone as cust_pho9_0_0_, customer0_.cust_mobile as cust_mo10_0_0_ from cst_customer customer0_ where customer0_.cust_id=? Hibernate: update cst_linkman set lkm_cust_id=null where lkm_cust_id=? Hibernate: delete from cst_customer where cust_id=?
注意:Hibernate 自動執行了3條 SQL 語句,其中第2條尤其值得一看,它把聯系人表中與該客戶有關的外鍵的值設為 null,也就是說:這個客戶沒有聯系人了,自然也就可以刪除了。
上述的刪除是普通的刪除,那么也可以使用級聯刪除,級聯刪除有個好處:當我們把這個客戶刪除了,那這個客戶的聯系人自然也就沒什么意義了。
-
注意:級聯刪除也是有方向性的!!
-
假設在刪除客戶時,要刪除該客戶所對應的聯系人,則應該在客戶的配置文件中這樣配置:
<set name="linkmans" cascade="save-update,delete"> <!-- 需要出現子標簽 --> <!-- 外鍵的字段 --> <key column="lkm_cust_id"/> <one-to-many class="com.itheima.domain.Linkman"/> </set>
-
相反地,若要在刪除聯系人時,同時刪除對應的客戶,則在聯系人的配置文件中這樣配置:
<many-to-one name="customer" class="com.itheima.domain.Customer" column="lkm_cust_id" cascade="save-update,delete"/>
也可以像級聯保存那樣,互相級聯,那樣就可以實現:刪除一個聯系人 - 刪除對應的客戶 - 刪除該客戶所對應的所有聯系人
注意:級聯刪除要慎重!在實際情況中,很多時候是假刪除,即添加一個字段,用該字段來表示這條記錄是否刪除了,多用 update ,而不會真正的 delete !
-
-
5、cascade 屬性——級聯
之前的級聯保存、級聯刪除,都是在配置文件中配置這樣一個屬性
cascade
,接下來探討一下這個屬性的取值。-
級聯的取值
- 需要掌握的取值如下:
- none -- 不使用級聯
- save-update -- 級聯保存或更新
- delete -- 級聯刪除
- delete-orphan -- 孤兒刪除.(注意:只能應用在一對多關系)
- all -- 除了delete-orphan的所有情況.(包含save-update,delete)
- all-delete-orphan -- 包含了delete-orphan的所有情況.(包含save-update,delete,delete-orphan)
- 需要掌握的取值如下:
-
孤兒刪除
-
孤兒刪除(孤子刪除),只有在一對多的環境下才有孤兒刪除
- 在一對多的關系中,可以將“一”的一方認為是父方,將“多”的一方認為是子方。孤兒刪除:當解除了父子關系的時候,將子方記錄就直接刪除。
-
若想在解除關系時,把聯系人的記錄從數據庫刪除,則應該在客戶的配置文件中中這樣編寫:
<!-- 配置一方 --> <!-- set標簽name屬性:表示集合的名稱 --> <set name="linkmans" cascade="delete-orphan"> <!-- 需要出現子標簽 --> <!-- 外鍵的字段 --> <key column="lkm_cust_id"/> <one-to-many class="com.itheima.domain.Linkman"/> </set>
-
解除父子關系的代碼:
// 先獲取到客戶 Customer c1 = session.get(Customer.class, 1L); Linkman l1 = session.get(Linkman.class, 1L); // 解除 c1.getLinkmans().remove(l1);
最后查詢數據庫,發現聯系人表中 id 為1的記錄被刪除了。
-
6、inverse 屬性——放棄外鍵的維護
-
在之前的例子中,默認雙方都維護外鍵,會產生多余的SQL語句。
現象:想修改客戶和聯系人的關系,進行雙向關聯,雙方都會維護外鍵,會產生多余的SQL語句。
原因:session 的一級緩存中的快照機制,會讓雙方都更新數據庫,產生了多余的 SQL 語句。
-
如果不想產生多余的SQL語句,那么需要一方來放棄外鍵的維護,通常是“一”方來放棄外鍵的維護,于是在“一”方的配置文件中這樣編寫:
-
在<set>標簽上配置一個
inverse="true"
:true:放棄;false:不放棄;默認值是 false<inverse="true">
注意:在一對多的情況下,可以不放棄外鍵的維護,但是下節開始的多對多表結構必須有一方放棄外鍵的維護。
-
-
cascade 和 inverse 的區別
cascade 用來級聯操作(保存、修改和刪除)
-
inverse 用來維護外鍵
舉例:若 Customer 同時配置
cascade="save-update"
、<inverse="true">
,當保存一個 Customer 對象時,數據庫也會同時保存 Linkman 對象(級聯保存的功勞),但是該 Linkman 對象的外鍵是空的。
注意:在實際情況中,大部分情況是:“一”方配置
<inverse="true">
,“多”方配置cascade="save-update"
。
7、多對多結構的準備
需求分析:用戶與角色是多對多的關系,一個用戶可能有多種角色,而某種角色可能被多個用戶共享。
-
數據庫知識回顧:在之前的 Java web 學習中,對于多對多的情況,要創建一個中間表,這個中間表通過外鍵聯系了兩張表,而在 Hibernate 中,不需要手動創建中間表,只需要按照 Hibernate 的規范編寫 JavaBean 和其對應的配置文件即可。
-
JavaBean 代碼(省略 set 和 get 方法):
-
用戶:
public class User { private Long uid; private String username; private String password; // 編寫都是集合 private Set<Role> roles = new HashSet<Role>(); }
-
角色:
public class Role { private Long rid; private String rname; private Set<User> users = new HashSet<User>(); }
-
-
用戶與角色的配置文件編寫:
-
用戶的映射配置文件如下
<class name="com.itheima.domain.User" table="sys_user"> <id name="user_id" column="user_id"> <generator class="native"/> </id> <property name="user_code" column="user_code"/> <property name="user_name" column="user_name"/> <property name="user_password" column="user_password"/> <property name="user_state" column="user_state"/> <set name="roles" table="sys_user_role"> <key column="user_id"/> <many-to-many class="com.itheima.domain.Role" column="role_id"/> </set> </class>
中間表的名字就叫做 sys_user_role
-
角色的映射配置文件如下
<class name="com.itheima.domain.Role" table="sys_role"> <id name="role_id" column="role_id"> <generator class="native"/> </id> <property name="role_name" column="role_name"/> <property name="role_memo" column="role_memo"/> <set name="users" table="sys_user_role"> <key column="rid"/> <many-to-many class="com.itheima.domain.User" column="uid"/> </set> </class>
中間表的名字就叫做 sys_user_role
-
多對多進行雙向關聯的時候:必須有一方去放棄外鍵維護權,如果不放棄中間表會被更新兩次,不允許!
故,可選擇角色的映射配置文件,改為:
<!-- 多對多必須要有一方放棄外鍵的維護的 --> <set name="users" table="sys_user_role" inverse="true"> <key column="rid"/> <many-to-many class="com.itheima.domain.User" column="uid"/> </set>
-
8、多對多結構的級聯
關于級聯的各種取值、概念在前文已有,在此不再贅述。
-
級聯保存:save-update ,大部分情況下都是這個取值。
以下是一個示例,角色放棄外鍵的維護,用戶級聯角色:
// 模擬多對多,雙向的關聯 User u1 = new User(); u1.setUsername("張三"); User u2 = new User(); u2.setUsername("趙四"); // 創建角色 Role r1 = new Role(); r1.setRname("經理"); Role r2 = new Role(); r2.setRname("演員"); u1.getRoles().add(r1); u1.getRoles().add(r2); u2.getRoles().add(r1); // 保存數據 session.save(u1); session.save(u2);
級聯刪除:在多對多情況下,極少使用。