Hibernate的一对多关联映射

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 编写实体类

客户的实体类如下:

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

	//省略 get和 set方法
}

联系人的实体类如下:

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;
	
	//省略 get和 set方法
}

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

运行测试代码后控制台输出了3条insert语句和2条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 级联保存或更新

级联是有方向性的,所谓的方向性指的是,在保存一的一方级联多的一方或在保存多的一方级联一的一方。

  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();
    }
    
  2. 保存联系人级联客户
    同样我们需要确定主控方,现在我们要保存联系人,所以联系人是主控方,所以需要在联系人的映射文件中进行如下的配置:

    <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语句?我们发现会有3条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的级联保存操作了。

  1. 删除客户的时候同时删除客户的联系人
    确定删除的主控方是客户,所以需要在客户端配置:

    <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();
    }
    
  2. 删除联系人的时候同时删除客户
    同样我们删除的是联系人,那么联系人是主控方,所以需要在联系人端配置:

    <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中的多对多关联映射,且看老王的下篇博文。

发布了25 篇原创文章 · 获赞 0 · 访问量 475

猜你喜欢

转载自blog.csdn.net/weixin_45990046/article/details/103534048