数据库中多表之间存在着三种关系,也就是系统设计中的三种实体关系,分别是一对一,一对多和多对多。这里我主要详解一对多和多对多。
Hibernate 的一对多关联映射
一对多在表中的表达
例如:
建表原则:在多的一方创建外键,指向一的一方的主键
一对多在实体中的表达
例如客户与联系人之间的关系(一个客户可以对应多个联系人)
在HIbernate 中,是采用 java 对象关系来描述数据表之间的关系
在了解了一对多在数据库表中和实体中的表达后,那么在映射文件中如何表达呢?下面通过一个例子,在例子中学习、理解在映射文件中表达一对多之间的关系。
一个 Hibernate 的一对多关联映射简单例子
1.创建表
在上一篇,我们创建了一个customer表,我们在演示一对多关系映射,还需要一个 联系人表
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;
2.创建实体
//Customer实体
public class Customer {
private Long cust_id;
private String cust_name;
private String cust_source;
private String cust_industry;
private String cust_level;
private String cust_linkman;
private String cust_phone;
private String cust_mobile;
// 使用set集合,表达一对多关系
private Set<LinkMan> linkMens = new HashSet<LinkMan>();
setter/getter方法。。。。
}
//LinkMan实体
public class LinkMan {
private Long lkm_id;
private Character lkm_gender;
private String lkm_name;
private String lkm_phone;
private String lkm_email;
private String lkm_qq;
private String lkm_mobile;
private String lkm_memo;
private String lkm_position;
//表达多对一关系
private Customer customer ;
setter/getter方法。。。。
}
3.创建映射
//Customer.hbm.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.pngyul.domain" >
<class name="Customer" table="cst_customer" >
<id name="cust_id" >
<generator class="native"></generator>
</id>
<property name="cust_name" column="cust_name" ></property>
<property name="cust_source" column="cust_source" ></property>
<property name="cust_industry" column="cust_industry" ></property>
<property name="cust_level" column="cust_level" ></property>
<property name="cust_linkman" column="cust_linkman" ></property>
<property name="cust_phone" column="cust_phone" ></property>
<property name="cust_mobile" column="cust_mobile" ></property>
<!-- 一对多-->
<!--
private Set<LinkMan> linkMens = new HashSet<LinkMan>();
name:对应Customer定义的属性,多的一方的属性名字 linkMens
column:数据表中多的一方的外键,指向一的一方的主键
class:多的一方的完整类名
-->
<set name="linkMens">
<key column="lkm_cust_id"></key>
<!-- 一对多关系 -->
<one-to-many class="LinkMan" />
</set>
</class>
</hibernate-mapping>
//LinkMan.hbm.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.pngyul.domain" >
<class name="LinkMan" table="cst_linkman" >
<id name="lkm_id" >
<generator class="native"></generator>
</id>
<property name="lkm_gender" ></property>
<property name="lkm_name" ></property>
<property name="lkm_phone" ></property>
<property name="lkm_email" ></property>
<property name="lkm_qq" ></property>
<property name="lkm_mobile" ></property>
<property name="lkm_memo" ></property>
<property name="lkm_position" ></property>
<!-- 多对一 -->
<!--
name属性:引用属性名
column属性: 外键列名
class属性: 与我关联的对象完整类名
-->
<many-to-one name="customer" column="lkm_cust_id" class="Customer">
</many-to-one>
</class>
</hibernate-mapping>
4.创建主配置文件,将映射文件添加到配置文件中
//这里我们在上一篇例子的主配置文件的基础上添加两行代码即可
<mapping resource="com/pngyul/domain/Customer.hbm.xml" />
<mapping resource="com/pngyul/domain/LinkMan.hbm.xml" />
5.编写代码测试
// 保存客户 以及客户 下的联系人
@Test
public void fun1() {
Session session = HibernateUtils.openSession();
Transaction transaction = session.beginTransaction();
// 操作
Customer c = new Customer();
c.setCust_name("张三2");
LinkMan lkm1 = new LinkMan();
lkm1.setLkm_name("刘强东2");
LinkMan lkm2 = new LinkMan();
lkm2.setLkm_name("张一鸣2");
// 表达一对多,客户下有多个联系人
c.getLinkMens().add(lkm1);
c.getLinkMens().add(lkm2);
// 表达对对对,联系人属于哪个客户
lkm1.setCustomer(c);
lkm2.setCustomer(c);
session.save(c);
session.save(lkm1);
session.save(lkm2);
transaction.commit();
session.close();
}
这样这三条记录就保存在数据库中了,且客户与联系人之间存在一对多的关系
然而我们发现,如果一个客户对应很多个联系人,在持久化对象(调用 save 方法)时,每一个联系人都要写一行代码转换成持久态,未免有点累赘。所以我们试着注释这两行代码,试着运行是否可行
session.save(c);
//session.save(lkm1);
//session.save(lkm2);
运行发现,这样操作显然不行,无论从哪一方保存都会出现 瞬时对象异常。一个持久态对象关联了一个瞬时态对象,那就说明我们不能只保存一方。那么如果我们就只想保存一方应该如何进行操作呢?Hibernate的级联级别。
级联操作
级联操作是指当主控方执行保存、更新或者删除操作时,其关联对象(被控方)也执行相同的操作。
级联保存或更新
级联是有方向性的,所谓的方向性是指,在保存一的一方级联多的一方和在保存多的一方级联一的一方
一个 [在保存一的一方级联多的一方] 的例子
//在Customer映射文件中的<set>标签添加cascade属性
<set name="linkMens" cascade="save-update">
<key column="lkm_cust_id"></key>
<!-- 一对多关系 -->
<one-to-many class="LinkMan" />
</set>
添加了级联更新属性之后,在运行之前注释了两行代码的程序,发现没毛病。
相反也一样,你在 LinkMan 映射文件中添加级联更新,再注释保存 Customer 的代码,同样该 Customer 也会保存到数据库中。
级联删除
级联删除也是有方向性的,删除客户同时级联删除联系人,也可以删除联系人同时级联删除客户。由于级联删除操作风险很大,在实际开发中一般很少用。
// 级联删除客户 以及客户 下的联系人
@Test
public void fun2() {
Session session = HibernateUtils.openSession();
Transaction transaction = session.beginTransaction();
// 操作
Customer c = session.get(Customer.class, 2l);
session.delete(c);
transaction.commit();
session.close();
}
至于 Customer 映射文件的修改,把 cascade 的属性只改成 delete 即可
PS:cascade 还有一个属性值all,该属性值的意思 [save-update+delete] ,就是同时具备级联更新和级联删除
关系维护
双向关联产生多余的 sql 语句
例如下面这个例子:
@Test
public void fun3() {
Session session = HibernateUtils.getCurrentSession();
Transaction transaction = session.beginTransaction();
// 操作
Customer customer = session.get(Customer.class, 1l);
LinkMan linkMan = session.get(LinkMan.class, 3l);//该linkMan还不是该顾客的联系人
//该该客户添加该联系人
linkMan.setCustomer(customer);
customer.getLinkMens().add(linkMan);
transaction.commit();
}
控制态最后输出了两个update
其实这两条 update 语句都是修改外键的操作,那为什么发两次呢?
因为映射文件中双向维护了关系,产生了多余的 sql,那么该如何解决呢?其实解决方法也很简单,只需要将一方放弃外键维护关系即可,通常我们会交给多的一方去维护。
例如,想让一的一方放弃维护,只需要进行如下配置,
<!-- inverse属性:
true:放弃维护
false:默认值,不放弃维护
-->
<set name="linkMens" cascade="save-update" inverse="true">
<key column="lkm_cust_id"></key>
<one-to-many class="LinkMan" />
</set>
Hibernate 的多对多关联映射
Hibernate 的多对多关联映射与一对多有很多相似之处,相信学习完一对多关联再学多对多,学起来应该还是挺轻松的。
多对多在表中的表达
建表原则:创建一个中间表,中间表中至少两个字段作为外键分别指向多对多双方的主键
多对多在实体中的表达
例如员工与实体之间的关系
同样,下面也是通过一个例子来学习,多对多在映射文件中式如何表达的。说实话,和一对一差不多。。。
一个 Hibernate 的多对多关联映射简单例子
1.创建三个新表
//角色表
CREATE TABLE `sys_role` (
`role_id` bigint(32) NOT NULL AUTO_INCREMENT,
`role_name` varchar(32) NOT NULL COMMENT '角色名称',
`role_memo` varchar(128) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
//员工表
CREATE TABLE `sys_user` (
`user_id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`user_code` varchar(32) NOT NULL COMMENT '用户账号',
`user_name` varchar(64) NOT NULL COMMENT '用户名称',
`user_password` varchar(32) NOT NULL COMMENT '用户密码',
`user_state` char(1) NOT NULL COMMENT '1:正常,0:暂停',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
//员工说角色中间表
CREATE TABLE `sys_user_role` (
`role_id` bigint(32) NOT NULL COMMENT '角色id',
`user_id` bigint(32) NOT NULL COMMENT '用户id',
PRIMARY KEY (`role_id`,`user_id`),
KEY `FK_user_role_user_id` (`user_id`),
CONSTRAINT `FK_user_role_role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`role_id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `FK_user_role_user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`user_id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.创建对应的实体
public class User {
private Long user_id;
private String user_code;
private String user_name;
private String user_password;
private Character user_state;
//表达多对多
private Set<Role> roles = new HashSet<Role>();
setter/getter方法。。。。。
}
//角色对象
public class Role {
private Long role_id;
private String role_name;
private String role_memo;
//表达多对多
private Set<User> users = new HashSet<User>();
一堆setter/getter方法。。。
}
3.编写对应的映射文件
//user映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<!-- 配置表与实体对象的关系 -->
<!-- package属性:填写一个包名.在元素内部凡是需要书写完整类名的属性,可以直接写简答类名了. -->
<hibernate-mapping package="com.pngyul.domain" >
<class name="User" table="sys_user" >
<id name="user_id" >
<generator class="native"></generator>
</id>
<property name="user_code" ></property>
<property name="user_name"></property>
<property name="user_password"></property>
<property name="user_state" ></property>
<!-- 多对多关系表达 -->
<!--
name: 集合属性名
table: 配置中间表名
key
|-column:外键,别人引用"我"的外键列名
class: 我与哪个类是多对多关系
column:外键.我引用比人的外键列名
-->
<!-- cascade级联操作:
save-update: 级联保存更新
delete:级联删除
all:级联保存更新+级联删除
结论: cascade简化代码书写.该属性使不使用无所谓. 建议要用只用save-update.
如果使用delete操作太过危险.尤其在多对多中.不建议使用.
-->
<set name="roles" table="sys_user_role" cascade="save-update">
<key column="user_id" ></key>
<many-to-many class="Role" column="role_id" ></many-to-many>
</set>
</class>
</hibernate-mapping>
//role映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.pngyul.domain" >
<class name="Role" table="sys_role" >
<id name="role_id" >
<generator class="native"></generator>
</id>
<property name="role_name" ></property>
<property name="role_memo"></property>
<!-- inverse="true" 角色放弃关系维护 -->
<set name="users" table="sys_user_role" inverse="true">
<key column="role_id" ></key>
<many-to-many class="User" column="user_id" ></many-to-many>
</set>
</class>
</hibernate-mapping>
4.映射文件添加到主配置文件中
<mapping resource="com/pngyul/domain/User.hbm.xml" />
<mapping resource="com/pngyul/domain/Role.hbm.xml" />
5.测试
@Test
public void fun1() {
Session session = HibernateUtils.getCurrentSession();
Transaction transaction = session.beginTransaction();
// 操作
User u1 = new User();
u1.setUser_name("张三");
User u2 = new User();
u2.setUser_name("李四");
Role r1 = new Role();
r1.setRole_name("运维");
Role r2 = new Role();
r2.setRole_name("测试");
//如果多对多建立了双向关联,一定要有一方放弃维护
// 用户表达关系
u1.getRoles().add(r1);
u1.getRoles().add(r2);
u2.getRoles().add(r1);
u2.getRoles().add(r2);
// 角色表达关系
r1.getUsers().add(u1);
r1.getUsers().add(u2);
r2.getUsers().add(u1);
r2.getUsers().add(u2);
// 调用Save方法一次保存
session.save(u1);
session.save(u2);
session.save(r1);
session.save(r2);
transaction.commit();
}
测试发现数据都保存到了数据库当中,并且员工与角色存在着多对多的关系
ps:在多对多的保存操作中,如果进行了双向维护关系,就必须有一方放弃外键维护权。一般由被动方放弃,员工主动选择角色,角色是被选择的,所以一般角色要放弃外键维护权,但如果只进行单向维护关系,那么就不需要放弃外键维护权了。
级联操作
多对多的级联保存跟一对多的一样,如果只想保存单独的一方是不可以的,还是需要保存双方的。如果就想保存一方就需要设置级联操作了。同样要看保存的主控方是哪一端,就需要在那一端纪进行配置。
// 级联更新--保存用户 以及担任的任务
@Test
public void fun2() {
Session session = HibernateUtils.openSession();
Transaction transaction = session.beginTransaction();
// 操作
User u1 = new User();
u1.setUser_name("张三1");
User u2 = new User();
u2.setUser_name("李四1");
Role r1 = new Role();
r1.setRole_name("运维1");
Role r2 = new Role();
r2.setRole_name("测试1");
// 用户表达关系
u1.getRoles().add(r1);
u1.getRoles().add(r2);
u2.getRoles().add(r1);
u2.getRoles().add(r2);
// 角色表达关系
// r1.getUsers().add(u1);
// r1.getUsers().add(u2);
// r2.getUsers().add(u1);
// r2.getUsers().add(u2);
// 调用Save方法一次保存
session.save(u1);
session.save(u2);
// session.save(r1);
// session.save(r2);
transaction.commit();
session.close();
}
// 级联更新--为张三新增一个角色
@Test
public void fun3() {
Session session = HibernateUtils.openSession();
Transaction transaction = session.beginTransaction();
// 操作
User u = session.get(User.class, 1l);
Role r = new Role();
r.setRole_name("软件开发");
u.getRoles().add(r);
session.save(u);
// session.save(r);
transaction.commit();
session.close();
}
@Test
//为張三解除一个角色
public void fun5(){
Session session = HibernateUtils.openSession();
Transaction tx = session.beginTransaction();
User u = session.get(User.class, 2l);
Role r = session.get(Role.class, 2l);
u.getRoles().remove(r);
tx.commit();
session.close();
}
级联保存说完了,那么我们来看下级联删除的操作,但在多对多中是不会使用的,因为我们不会有这类需求,比如删除员工,将员工关联的角色一起删除,或者删除角色的时候将员工删除。这是不可理的。但是级联删除的功能 Hibernate 已经提供了。所以我们只需要了解一下即可
// 級聯刪除
@Test
public void fun4() {
Session session = HibernateUtils.openSession();
Transaction transaction = session.beginTransaction();
// 操作
User u = session.get(User.class, 1l);
session.delete(u);
transaction.commit();
session.close();
}