文章目录
1. 表关系介绍
Hibernate框架实现了ORM的思想,将关系数据库中表的数据映射成对象,使开发人员把对数据库的操作转化为对对象的操作,Hibernate的关联关系映射主要包括多表的映射配置、数据的增加、删除等。数据库中多表之间存在着三种关系,也就是系统设计中的三种实体关系。如下图所示。
从图可以看出,系统设计的三种实体关系分别为:多对多、一对多和一对一关系。在数据库中实体表之间的关系映射是采用外键来描述的,具体如下。
- 一对多
建表原则:在多的一方创建外键指向一的一方的主键。
- 多对多
建表原则:创建一个中间表,中间表中至少两个字段作为外键分别指向多对多双方的主键。
- 一对一
建表原则有两种:唯一外键对应(假设一对一中的任意一方为多,在多的一方创建外键指向一的一方的主键,然后将外键设置为唯一)。主键对应(一方的主键作为另一方的主键)。
数据库表能够描述的实体数据之间的关系,通过对象也可以进行描述,所谓的关联映射就是将关联关系映射到数据库里,在对象模型中就是一个或多个引用。在Hibernate中采用Java对象关系来描述数据表之间的关系,具体如下图所示:
从图可以看出,一对一的关系就是在本类中定义对方类型的对象,如A中定义B类类型的属性b,B类中定义A类类型的属性a。一对多的关系,图中描述的是一个A对应多个B类类型的情况,需要在A类以Set集合的方式引入B类型的对象,在B类中定义A类类型的属性a。多对多的关系,在A类中定义B类类型的Set集合,在B类中定义A类类型的Set集合,这里用Set集合的目的是避免了数据的重复。以上就是系统模型中实体设计的三种关联关系,由于ー对一的关联关系在开发中不常使用,所以老王接下来就着重介绍下一对多和多对多的关联映射。
2. Hibernate一对多的关联映射案例
2.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;
2.2 创建实体
客户的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;
private Set<Linkman> linkmans = new HashSet<Linkman>();
//省略getter和setter方法
}
联系人的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;
private Customer customer;
//省略getter和setter方法
}
2.3 创建映射
客户的映射配置文件如下:
<hibernate-mapping>
<class name="com.joker.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:多的一方的集合的属性名称
key标签:
* column:多的一方的外键的名称
one-to-many标签:
* class:多的一方的类全路径
-->
<set name="linkmans">
<key column="lkm_cust_id"/>
<one-to-many class="com.joker.domain.Linkman"/>
</set>
</class>
</hibernate-mapping>
联系人的映射配置文件如下
<hibernate-mapping>
<class name="com.joker.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:一的一方的对象的名称
* class:一的一方的类的全路径
* column:表中外键的名称
-->
<many-to-one name="customer" class="com.joker.domain.Customer" column="lkm_cust_id"/>
</class>
</hibernate-mapping>
2.4 将映射添加到配置文件
<mapping resource="com/joker/domain/Customer.hbm.xml"/>
<mapping resource="com/joker/domain/Linkman.hbm.xml"/>
2.5 编写测试代码
public void test1(){
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
// 创建一个客户
Customer customer = new Customer();
customer.setCust_name("张总");
// 创建两个联系人
LinkMan linkMan1 = new LinkMan();
linkMan1.setLkm_name("秦助理");
LinkMan linkMan2 = new LinkMan();
linkMan2.setLkm_name("胡助理");
// 建立关系
customer.getLinkMans().add(linkMan1);
customer.getLinkMans().add(linkMan2);
linkMan1.setCustomer(customer);
linkMan2.setCustomer(customer);
session.save(customer);
session.save(linkMan1);
session.save(linkMan2);
transaction.commit();
session.close();
sessionFactory.close();
}
运行测试代码后控制台输出了三条insert语句和两条update语句,如下图所示:
通过以上的案例可以发现我们建立的关系是双向的,即客户关联了联系人,同时联系人也关联了客户。这就是双向关联,那么既然已经进行了双向的关联关系的设置,而且我们还保存了双方,那如果我们只保存一方是否可以呢?也就是说我们建立了双向的维护关系,只保存客户或者只保存联系人是否可以。那么我们来进行一下测试。
public void test2(){
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
// 创建一个客户
Customer customer = new Customer();
customer.setCust_name("张总");
// 创建一个联系人
LinkMan linkMan1 = new LinkMan();
linkMan1.setLkm_name("秦助理");
// 建立关系
customer.getLinkMans().add(linkMan1);
linkMan1.setCustomer(customer);
session.save(customer); //瞬时对象异常,持久态的对象关联了一个瞬时态对象的异常
//session.save(linkMan1);
transaction.commit();
session.close();
sessionFactory.close();
}
我们执行这段代码,会出现如下错误:
这样操作显然不行,无论从那一方保存都会出现同样的异常:瞬时对象异常,一个持久态对象关联了一个瞬时态对象,那就说明我们不能只保存一方。那如果我们就想只保存一个方向应该如何进行操作呢,那么我们可以使用Hibernate的级联操作,那么我们来看下Hibernate的级联吧。
3. Hibernate一对多的相关操作
3.1 级联保存或更新
级联是有方向性的,所谓的方向性指的是,在保存一的一方级联多的一方或在保存多的一方级联一的一方。
- 保存客户级联联系人
首先要确定我们要保存的主控方是那一方,我们要保存客户,所以客户是主控方,那么需要在客户的映射文件中进行如下的配置:
然后编写测试代码,代码如下:<set name="linkmans" cascade="save-update"> <key column="lkm_cust_id"/> <one-to-many class="com.joker.domain.Linkman"/> </set>
public void test3(){ Configuration configuration = new Configuration().configure(); SessionFactory sessionFactory = configuration.buildSessionFactory(); Session session = sessionFactory.openSession(); Transaction transaction = session.beginTransaction(); // 创建一个客户 Customer customer = new Customer(); customer.setCust_name("张总"); // 创建一个联系人 LinkMan linkMan1 = new LinkMan(); linkMan1.setLkm_name("秦助理"); // 建立关系 customer.getLinkMans().add(linkMan1); linkMan1.setCustomer(customer); session.save(customer); //session.save(linkMan1); transaction.commit(); session.close(); sessionFactory.close(); }
- 保存联系人级联客户
同样我们需要确定主控方,现在我们要保存联系人,所以联系人是主控方,所以需要在联系人的映射文件中进行如下的配置:
然后编写测试代码,代码如下:<many-to-one name="customer" cascade="save-update" class="com.joker.domain.Customer" column="lkm_cust_id"/>
public void test4(){ Configuration configuration = new Configuration().configure(); SessionFactory sessionFactory = configuration.buildSessionFactory(); Session session = sessionFactory.openSession(); Transaction transaction = session.beginTransaction(); // 创建一个客户 Customer customer = new Customer(); customer.setCust_name("张总"); // 创建一个联系人 LinkMan linkMan1 = new LinkMan(); linkMan1.setLkm_name("秦助理"); // 建立关系 customer.getLinkMans().add(linkMan1); linkMan1.setCustomer(customer); //session.save(customer); session.save(linkMan1); transaction.commit(); session.close(); sessionFactory.close(); }
到这我们已经可以看到级联保存或更新的效果了。那么我们维护的时候都是双向的关系维护。那么这种关系到底表述的是什么含义呢?我们可以通过下面的测试来更进一步了解关系的维护(对象导航)的含义。
3.2 关系的维护(对象导航)
我们所说的对象导航其实就是在维护双方的关系。如下所示
customer.getLinkMans().add(linkMan1);
customer.getLinkMans().add(linkMan2);
linkMan1.setCustomer(customer);
linkMan2.setCustomer(customer);
这种关系有什么用途呢?我们可以通过下面的测试来更进一步学习Hibernate。我们在客户和联系人端都配置了cascade=“save-update”,然后进行如下的关系设置。会产生什么样的效果呢?
public void test5(){
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
// 创建一个客户
Customer customer = new Customer();
customer.setCust_name("张总");
// 创建两个联系人
LinkMan linkMan1 = new LinkMan();
linkMan1.setLkm_name("秦助理");
LinkMan linkMan2 = new LinkMan();
linkMan2.setLkm_name("胡助理");
LinkMan linkMan3 = new LinkMan();
linkMan3.setLkm_name("王助理");
// 建立关系
linkMan1.setCustomer(customer);
customer.getLinkMans().add(linkMan2);
customer.getLinkMans().add(linkMan3);
//条件是双方都配置了cascade="save-update"
session.save(linkMan1); //数据库中有几条记录?发送了几条insert语句? 4条
//session.save(customer); //发送了几条insert语句? 3条
//session.save(linkMan2); //发送了几条insert语句? 1条
transaction.commit();
session.close();
sessionFactory.close();
}
我们执行第25行的时候,问会执行几条insert语句呢?其实发现会有4条insert语句的,因为联系人1关联了客户,客户又关联了联系人2和联系人3,所以当保存联系人1的时候,联系人1是可以进入到数据库的,它关联的客户也是会进入到数据库的(因为联系人一端配置了级联),那么客户进入到数据库以后,联系人2和联系人3也同样会进入到数据库(因为客户一端配置了级联),所以会有4条 Insert语句。
我们执行26行的时候,问会有几条insert语句?这时我们保存的客户对象,可以对象关联了联系人2和联系人3,那么客户在保存进入数据库的时候,联系人2和联系人3也会进入到数据库,所以是3条insert语句。
同理我们执行27行代码的时候,只会执行1条insert语句,因为联系人2会保存到数据库,但是联系人2没有客户客户建立关系。所以客户不会保存到数据库
到这我们应该更加理解了Hibernate的这种关系的建立和级联的关系了。那么级联还有那些操作呢?接下来且听老王继续介绍级联删除的操作。
3.3 级联删除
上面我们介绍了级联保存或更新,那么再来看级联删除也就不难理解了,级联删除也是有方向性的,删除客户同时级联删除联系人,也可以删除联系人同时级联删除客户(这种需求很少)。
原来JDBC中删除客户和联系人的时候,如果有外键的关系是不可以删除的,但是现在我们使用了Hibernate,其实Hibernate可以实现这样的功能,但是不会删除客户同时删除联系人,默认情况下Hibernate会怎么做呢?我们来看下面的测试。
public void test6(){
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
//没有配置级联删除,删除有关联关系的对象,先将关联对象的外键置为null,然后再去删除客户对象
Customer customer = session.get(Customer.class, 1L);
session.delete(customer);
transaction.commit();
session.close();
sessionFactory.close();
}
默认的情况下如果客户下面还有联系人,Hibernate会将联系人的外键置为null,然后去删除客户。那么其实有的时候我们需要删除客户的时候,同时将客户关联的联系人一并删除。这个时候我们就需要使用Hibernate的级联保存操作了。
- 删除客户的时候同时删除客户的联系人
确定删除的主控方是客户,所以需要在客户端配置:
如果还想有之前的级联保存或更新,同时还想拥有级联删除,那么我们可以进行如下配置:<set name="linkmans" cascade="delete"> <key column="lkm_cust_id"/> <one-to-many class="com.joker.domain.Linkman"/> </set>
配置完成以后我们就可以来编写测试代码了,代码如下:<set name="linkmans" cascade="save-update,delete"> <key column="lkm_cust_id"/> <one-to-many class="com.joker.domain.Linkman"/> </set>
public void test7(){ Configuration configuration = new Configuration().configure(); SessionFactory sessionFactory = configuration.buildSessionFactory(); Session session = sessionFactory.openSession(); Transaction transaction = session.beginTransaction(); //级联删除:必须是先查询在删除的 //因为查询到客户,这个时候客户的联系人的集合中就会有数据 Customer customer = session.get(Customer.class, 1L); session.delete(customer); transaction.commit(); session.close(); sessionFactory.close(); }
- 删除联系人的时候同时删除客户
同样我们删除的是联系人,那么联系人是主控方,所以需要在联系人端配置:
如果还想有之前的级联保存或更新,同时还想拥有级联删除,那么我们可以进行如下配置:<many-to-one name="customer" cascade="delete" class="com.joker.domain.Customer" column="lkm_cust_id"/>
配置完成以后我们就可以来编写测试代码了,代码如下:<many-to-one name="customer" cascade="save-update,delete" class="com.joker.domain.Customer" column="lkm_cust_id"/>
public void test8(){ Configuration configuration = new Configuration().configure(); SessionFactory sessionFactory = configuration.buildSessionFactory(); Session session = sessionFactory.openSession(); Transaction transaction = session.beginTransaction(); //级联删除:必须是先查询在删除的 //因为查询到联系人,这个时候联系人的客户对象中就会有数据 LinkMan linkMan = session.get(LinkMan.class, 3L); session.delete(linkMan); transaction.commit(); session.close(); sessionFactory.close(); }
3.4 双向关联产生多余的SQL语句
到这我们已经了解了Hibernate级联的基本配置和使用。但是有些时候我们需要进行如下的操作,数据库中记录如下:
客户表:
联系人表:
需要将2号联系人关联给2号客户。也就是将2号李秘书这个联系人关联给2号刘总这个客户。编写修改2号客户关联的联系人的代码如下:
public void test9(){
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
Customer customer = session.get(Customer.class, 2L);
LinkMan linkMan = session.get(LinkMan.class, 2L);
linkMan.setCustomer(customer);
customer.getLinkMan().add(linkMan);
transaction.commit();
session.close();
sessionFactory.close();
}
运行改代码,控制台会输出如下内容:
我们会发现执行了两次update语句,其实这两个update都是修改了外键的操作,那么为什么发送两次呢?那么我们来分析一下产生这种问题的原因:
我们已经分析过了,因为双向维护了关系,而且持久态对象可以自动更新数据库,更新客户的时候会修改一次外键,更新联系人的时候同样也会修改一次外键。这样就会产生了多余的SQL,那么问题产生了,我们又该如何解决呢?
其实解决的办法很简单,只需要将一方放弃外键维护权即可。也就是说关系不是双方维护的,只需要交给某一方去维护就可以了。通常我们都是交给多的一方去维护的。为什么呢?因为多的一方才是维护关系的最好的地方,举个例子,一个老师对应多个学生,一个学生对应一个老师,这是典型的一对多。那么一个老师如果要记住所有学生的名字很难的,但如果让每个学生记住老师的名字应该不难。其实就是这个道理。所以在一对多中,一的一方都会放弃外键的维护权(关系的维护)。
这个时候如果想让一的一方放弃外键的维护权,只需要进行如下的配置即可:
<set name="linkmans" cascade="save-update,delete" inverse="true">
<key column="lkm_cust_id"/>
<one-to-many class="com.joker.domain.Linkman"/>
</set>
inverse的默认值是false,代表不放弃外键维护权,配置值为true,代表放弃了外键的维护权。这个时候再来执行之前的操作:
这个时候我们会发现就不会出现上述的问题了,不会产生多余的SQL了(因为一的一方已经放弃了外键的维护权)。
那么这个问题我们已经解决了,但是有很多人对cascade和inverse还是不是太懂,我们可以通过下面的案例区分cascade和inverse的区别,因为这两个参数我们以后的开发中会经常使用。
3.5 区分cascade和inverse
// cascade和inverse
// cascade强调的是操作一个对象的时候,是否操作其关联对象
// inverse强调的是外键的维护权
public void test10(){
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
Customer customer = new Customer();
customer.setCust_name("张总");
LinkMan linkMan = new LinkMan();
linkMan.setLkm_name("秦助理");
//在Customer.hbm.xml中的set上配置cascade="save-update" inverse="true"
customer.getLinkMans().add(linkMan);
//客户会插入到数据库,联系人也会插入到数据库,但是外键字段为null
session.save(customer);
transaction.commit();
session.close();
sessionFactory.close();
}
这个时候我们会发现,如果在set集合上配置cascade=“save-update” inverse="true"了,那么执行保存客户的操作,会发现客户和联系人都进入到数据库了,但是外键字段为null,是因为配置了cascade了,所以客户关联的联系人会进入到数据库,但是客户一端放弃了外键维护权,所以联系人插入到数据库以后是没有外键的。
一对多就说到这里了,欲知Hibernate中的多对多的关系的配置,且看老王的下篇博文。