JavaWeb 之 Hibernate 关联关系映射

Hibernate 关联关系映射

Hibernate 一对多映射

客户关系管理系统(CRM) 为例:

Java WEB中一对多的设计及其建表原则

多方表建一个字段作为外键,指向一方表的主键。

先导入SQL的建表语句

  • 创建数据库:create database hibernate_03;

  • 执行 SQL 语句进行建表:

客户表:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;

联系人表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 程序

(注意一对多的编写规则)

一方 Set 集合,必须自己初始化;多方编写一个对象,不要自己 new !

  • 客户的 JavaBean 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

private Set<Linkman> linkmans = new HashSet<Linkman>();
}
  • 联系人的 JavaBean 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
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;

private Customer customer;
}

编写客户和联系人的映射配置文件

注意一对多的配置编写:

  • 多方:
1
<many-to-one name="customer" class="com.renkaigis.domain.Customer" column="lkm_cust_id"/>

其中:

name:当前 JavaBean 中的属性
class:属性的全路径
column:外键的字段

  • 一方:
1
2
3
4
5
6
7
<!--配置一方-->
<set name="linkmans">
<!--需要出现两个子标签-->
<!--外键的字段-->
<key column="lkm_cust_id"/>
<one-to-many class="com.renkaigis.domain.Linkman"/>
</set>

客户的映射配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<class name="com.renkaigis.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="linkmans">
<key column="lkm_cust_id"/>
<one-to-many class="com.renkaigis.domain.Linkman"/>
</set>
</class>

联系人的映射配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<class name="com.renkaigis.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"/>

<many-to-one name="customer" class="com.renkaigis.domain.Customer" column="lkm_cust_id"/>
</class>

保存客户和联系人的数据

双向关联数据保存(麻烦)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 测试双向关联
*/
@Test
public void run1() {
Session session = HibernateUtils.getCurrentSession();
Transaction tr = session.beginTransaction();

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.setCustome1r(c1);
l2.setCustomer(c1);

// 保存
session.save(c1);
session.save(l1);
session.save(l2);

tr.commit();
}

级联保存

测试:

如果现在代码只插入其中的一方的数据

  • 如果只保存其中的一方的数据,那么程序会抛出异常。

  • 如果想完成只保存一方的数据,并且把相关联的数据都保存到数据库中,那么需要配置级联!!

  • 级联保存是方向性

级联保存效果

  • 级联保存:保存一方同时可以把关联的对象也保存到数据库中!!

  • 使用 cascade="save-update"

注意:级联保存有方向性

1
2
3
4
5
<!--客户级联联系人-->
<set name="linkmans" cascade="save-update">
<key column="lkm_cust_id"/>
<one-to-many class="com.renkaigis.domain.Linkman"/>
</set>

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 测试级联关联
*/
@Test
public void run2() {
Session session = HibernateUtils.getCurrentSession();
Transaction tr = session.beginTransaction();

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);

// 保存客户级联保存联系人
session.save(c1);

tr.commit();
}
  • 相反的,要使用联系人去关联客户,那么需要在联系人的映射里面配置 cascade="save_update",这是只要保存了联系人,与其对应的客户也会保存或更新。

级联删除

  • 1.数据库中删除含有外键的客户时,SQL 语句会报出错误的:delete from customers where cid = 1;

  • 2.如果使用 Hibernate 框架直接删除客户的时候,测试发现是可以删除的;

    • Hibernate 框架删除有外键的信息是,会将外键先查出来置为 null,然后执行删除操作;
  • 3.上述的删除是普通的删除,那么也可以使用级联删除,注意:级联删除也是有方向性的!!(轻易不要使用)

1
<many-to-one cascade="delete" />

级联的取值(cascade的取值)

  • none – 不使用级联
  • save-update – 级联保存或更新
  • delete – 级联删除
  • delete-orphan – 孤儿删除(注意:只能应用在一对多关系)
  • all – 除了 delete-orphan 的所有情况(包含save-update delete)
  • all-delete-orphan – 包含了 delete-orphan 的所有情况(包含save-update delete delete-orphan)

孤儿删除(孤子删除)

只有在一对多的环境下才有孤儿删除

  • 在一对多的关系中,可以将一的一方认为是父方。将多的一方认为是子方。孤儿删除:在解除了父子关系的时候,将子方记录就直接删除。
1
<many-to-one cascade="delete-orphan" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 测试孤儿删除
*/
@Test
public void run3() {
Session session = HibernateUtils.getCurrentSession();
Transaction tr = session.beginTransaction();

Customer c1 = session.get(Customer.class, 1L);
Linkman l1 = session.get(Linkman.class, 1L);

// 解除关系
c1.getLinkmans().remove(l1);

tr.commit();
}

放弃外键的维护

双方都维护外键的时候,会产生多余的 SQL 语句

  • 想修改客户和联系人的关系,进行双向关联,双方都会维护外键,会产生多余的 SQL 语句。

  • 产生的原因:session 的一级缓存中的快照机制,会让双方都更新数据库,产生了多余的 SQL 语句。

放弃外键维护

如果不想产生多余的 SQL 语句,那么需要一方来放弃外键的维护!

只有 一方 可以放弃,多方 没有哪个属性

  • <set> 标签上配置一个 inverse="true",true:放弃,false:不放弃,默认值是false
1
2
3
4
5
<!-- 放弃外键的维护 -->
<set name="linkmans" inverse="true">
<key column="lkm_cust_id"/>
<one-to-many class="com.renkaigis.domain.Linkman"/>
</set>

cascade 和 inverse 的区别

  • cascade 用来级联操作(保存、修改和删除);

  • inverse 用来维护外键的。

一般情况下,在 一方 配置 inverse 放弃外键维护,在 多方 配置 cascade 来进行级联保存操作。


Hibernate 多对多映射


多对多的建表原则

  • 需要创建一个中间表,至少需要包含两个字段,作为这个表外键,分别指向两张表的主键。

  • 两个外键一起叫联合主键。

  • 使用 Hibernate 框架,只要编写两个 JavaBean,编写两个映射的配置文件,中间表会自动生成。

多对多 JavaBean 的编写

以用户和角色为例,一个用户可以有多个角色,一个角色可以被多个用户扮演。

编写用户和角色的 JavaBean

多对多都要用 Set 集合:

  • 用户的 JavaBean 代码如下
1
2
3
4
5
6
7
8
9
10
public class User {
private Long user_id;
private String user_code;
private String user_name;
private String user_password;
private String user_state;

private Set<Role> roles = new HashSet<Role>();
// 省略 get / set
}
  • 角色的 JavaBean 代码如下
1
2
3
4
5
6
7
8
public class Role {
private Long role_id;
private String role_name;
private String role_memo;

private Set<User> users = new HashSet<User>();
// 省略 get / set
}

用户和角色的映射配置

  • 用户的映射配置文件如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<class name="com.renkaigis.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"/>

<!-- 配置多对多
name 集合的名称
table 中间表的名称
-->
<set name="roles" table="sys_user_role">
<!-- 当前对象在中间表的外键名称 -->
<key column="user_id"/>
<!--
class 集合中存入对象,对象的全路径
column 集合中对象在中间表的外键的名称
-->
<many-to-many class="com.renkaigis.domain.Role" column="role_id"/>
</set>
</class>
  • 角色的映射配置文件如下
1
2
3
4
5
6
7
8
9
10
11
12
<class name="com.renkaigis.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="role_id"/>
<many-to-many class="com.renkaigis.domain.User" column="user_id"/>
</set>
</class>

关联

多对多进行双向关联的时候:必须有一方去放弃外键维护权

否则会发生异常!

级联保存

级联保存

1
<set cascade="save-update">

级联删除

(在多对多中是很少使用的)

开发中不用!

1
<set cascade="delete"/>

操作中间表

操作集合,就是操作中间表。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 假设:张三用户,有2个角色,演员和导演,让张三没有演员这个角色
*/
public void run(){
Session session = HibernateUtils.getCurrentSession();
Transaction tr = session.beginTransaction();
// 获取用户和角色
User u1 = session.get(User.class, 1L);
Role r2 = session.get(Role.class, 2L);
// 移除用户的某一个角色,那么中间表就会修改了
u1.getRoles().remove(r2);
tr.commit();
}