首页 > 代码库 > hibernate--关联映射(多对一,一对一)
hibernate--关联映射(多对一,一对一)
多对一 关联映射 --- many-to-one
场景:用户和组;从用户角度来,多个用户属于一个组(多对一 关联)
使用hibernate开发的思路:先建立对象模型(领域模型),把实体抽取出来。
目前两个实体:用户和组两个实体,多个用户属于一个组,那么一个用户都会对应于一个组,所以用户实体中应该有一个持有组的引用。
对象模型图:
关联映射的本质:
将关联关系映射到数据库,所谓的关联关系是对象模型在内存中一个或多个引用。
User实体类: public class User { private int id; private String name; private Group group; get...set...}
public class User { private int id; private String name; private Group group; get...set...}
Group实体类:
public class Group { private int id; private String name; GET...SET}
实体类建立完后,开始创建映射文件,先建立简单的映射文件:
Group实体类的映射文件:
<hibernate-mapping> <class name="h.one.bean.Group" table="t_group"> <id name="id" column="id"> <generator class="native"/> </id> <property name="name"/> </class></hibernate-mapping>
User实体类的映射文件:
<hibernate-mapping> <class name="h.one.bean.User" table="t_user"> <id name="id" column="id"> <generator class="native"/> </id> <property name="name"/> <!--<many-to-one> 关联映射 多对一的关系 name:是维护的属性(User.group),这样表示在多的一端表里加入一个字段名称为group, 但group与SQL中的关键字重复,所以需要重新命名字段(column="groupid"). 这样这个字段(groupid)会作为外键参照数据库中group表(t_group也叫一的一端),也就是就在多的一 端加入一个外键指向一的一端。 --> <many-to-one name="group" column="groupid"/> </class></hibernate-mapping>
※<many-to-one>标签※:
例如:<many-to-one name="group" column="groupid"/>
<many-to-one> 关联映射 多对一的关系
name:是维护的属性(User.group),这样表示在多的一端表里加入一个字段名称为group,但group与SQL中的关键字重复,所以需要重新命名字段(column="groupid").这样这个字段(groupid)会作为外键参照数据库中group表(t_group也叫一的一端),也就是就在多的一端加入一个外键指向一的一端。
多对一 存储(先存储group(对象持久化状态后,再保存user)):
session = HibernateUtils.getSession(); tx = session.beginTransaction(); Group group = new Group(); group.setName("wjt276"); session.save(group); //存储Group对象。 User user1 = new User(); user1.setName("菜10"); user1.setGroup(group);//设置用户所属的组 User user2 = new User(); user2.setName("容祖儿"); user2.setGroup(group);//设置用户所属的组 //开始存储 session.save(user1);//存储用户 session.save(user2); tx.commit();//提交事务
注意:如果上面的session.save(group)不执行,则存储不存储不成功。则抛出TransientObjectException异常。
因为Group为Transient状,Object的id没有分配值。
结果:persistent状态的对象是不能引用Transient状态的对象
以上代码操作,必须首先保存group对象,再保存user对象。我们可以利用cascade(级联)方式,不需要先保存group对象。而是直接保存user对象,这样就可以在存储user之前先把group存储了。
利用cascade属性是解决TransientObjectException异常的一种手段。
重要属性-cascade(级联):
级联的意思是指定两个对象之间的操作联运关系,对一个 对象执行了操作之后,对其指定的级联对象也需要执行相同的操作,取值:all、none、save_update、delete
1、 all:代码在所有的情况下都执行级联操作
2、 none:在所有情况下都不执行级联操作
3、 save-update:在保存和更新的时候执行级联操作
4、 delete:在删除的时候执行级联操作。
例如:<many-to-one name="group" column="groupid" cascade="save-update"/>
多对一 加载数据
代码如下:
session = HibernateUtils.getSession(); tx = session.beginTransaction(); User user = (User)session.load(User.class, 3); System.out.println("user.name=" + user.getName()); System.out.println("user.group.name=" + user.getGroup().getName()); //提交事务 tx.commit();
可以加载Group信息:因为采用了<many-to-one>这个标签,这个标签会在多的一端(User)加一个外键,指向一的一端(Group),也就是它维护了从多到一的这种关系,多指向一的关系。当你加载多一端的数据时,它就能把一的这一端数据加载上来。当加载User对象后hibernate会根据User对象中的groupid再来加载Group信息给User对象中的group属性。
一对一 主键关联映射_单向(one-to-one)
2 两个对象之间是一对一的关系,如Person-IdCard(人—身份证号)
2 有两种策略可以实现一对一的关联映射
? 主键关联:即让两个对象具有相同的主键值,以表明它们之间的一一对应的关系;数据库表不会有额外的字段来维护它们之间的关系,仅通过表的主键来关联。
? 唯一外键关联:外键关联,本来是用于多对一的配置,但是如果加上唯一的限制之后,也可以用来表示一对一关联关系。
实例场景:人—-> 身份证号(PersonàIdCard),从IdCard看不到Person对象
对象模型(主键关联映射-单向):
(站在人的角度看)
IdCard实体类:
public class IdCard { private int id; private String cardNo;}
Person实体类:
public class Person { private int id; private String name; private IdCard idCard;//持有IdCard对象的引用}
因为是person引用idcard,所以idcard要求先有值。而person的主键值不是自己生成的。而是参考idcard的值,person即是主键,同时也是外键。
IdCard实体类的映射文件:
<hibernate-mapping> <class name="h.one.bean.IdCard" table="t_idcard"> <id name="id" column="id"> <generator class="native"/> </id> <property name="cardNo"/> </class></hibernate-mapping>
Persion实体类的映射文件:
<hibernate-mapping package="h.one.bean"> <class name="Person" table="t_person"> <id name="id" column="id"> <!-- 因为主键不是自己生成的,而是作为一个外键(来源于其它值),所以使用foreign生成策略 foreign:使用另外一个相关联的对象的标识符,通常和<one-to-one>联合起来使用。 再使用元素<param>的属性值指定相关联对象(这里Person相关联的对象为idCard,则标识符为idCard的id)为了能够在加载person数据同时加载IdCard数据,所以需要使用一个标签<one-to-one>来设置这个功能。 --> <generator class="foreign"> <!-- 元素<param>属性name的值是固定为property --> <param name="property">idCard</param> </generator> </id> <property name="name" /> <!-- <one-to-one>标签 表示如何加载它的引用对象(这里引用对象就指idCard这里的name值是idCard),同时也说是一对一的关系。 默认方式是根据主键加载(把person中的主键取出再到IdCard中来取相关IdCard数据。) 我们也说过此主键也作为一个外键引用 了IdCard,所以需要加一个数据库限制(外键约束)constrained="true" --> <one-to-one name="idCard" constrained="true" /> </class></hibernate-mapping>
※<one-to-one>标签※
现在是使用一对一主键关联映射,因为主键不是自己生成的,而是作为一个外键(来源于其它值),所以使用foreign生成策略(使用另外一个相关联的对象的标识符,通常和<one-to-one>联合起来使用)。再使用元素<param>的属性值指定相关联对象(这里Person相关联的对象为idCard,则标识符为idCard的id)为了能够在加载person数据同时加载IdCard数据,所以需要使用一个标签<one-to-one>来设置这个功能。
一对一 主键关联映射 存储测试
@Test public void testOneToOneSave() { session = HibernateUtil.getSession(); tx = session.beginTransaction(); IdCard idCard = new IdCard(); idCard.setCardNo("88888888888888888888888"); Person person = new Person(); person.setName("菜10"); person.setIdCard(idCard); //不会出现TransientObjectException异常 //因为一对一主键关键映射中,默认了cascade属性。 session.save(person); tx.commit(); }
注:不会出现TransientObjectException异常,因为一对一主键关键映射中,默认了cascade属性。
一对一 主键关联映射 加载测试
@Test public void testLoad() { session = HibernateUtil.getSession(); tx = session.beginTransaction(); User user = (User) session.load(User.class, "4028718146f232890146f2328b660001"); System.out.println("user.name=" + user.getName()); System.out.println("user.group.name=" + user.getGroup().getName()); // 提交事务 tx.commit(); }
一对一 主键关联映射_双向(one-to-one)
2 两个对象之间是一对一的关系,如Person-IdCard(人—身份证号)
2 有两种策略可以实现一对一的关联映射
? 主键关联:即让两个对象具有相同的主键值,以表明它们之间的一一对应的关系;数据库表不会有额外的字段来维护它们之间的关系,仅通过表的主键来关联。
? 唯一外键关联:外键关联,本来是用于多对一的配置,但是如果加上唯一的限制之后,也可以用来表示一对一关联关系。
实例场景:人<—-> 身份证号(Person<->IdCard)双向:互相持有对方的引用
对象模型(主键关联映射-双向):
IdCard实体类:
public class IdCard { private int id; private String cardNo; private Person person; //持有Person对象的引用}
Person实体类:
public class Person { private int id; private String name; private IdCard idCard;//持有IdCard对象的引用}
IdCard实体类映射文件:
<hibernate-mapping> <class name="h.one.bean.IdCard" table="t_idcard"> <id name="id" column="id"> <generator class="native"/> </id> <property name="cardNo"/> <!—one-to-one标签的含义:指示hibernate怎么加载它的关联对象(这里的关联对象为person),默认根据主键加载--> <one-to-one name="person"/> </class></hibernate-mapping>
Person实体类映射文件不变:
注意:此双向的SQL语句,与单向的SQL语句没有任何变化,也就是说数据库中的表单向双向没有任何区别。<one-to-one>的单向、双向对数据库表没有影响,只是告诉Hibernate如何加载数据对象。
需要在idcard映射文件中加入<one-to-one>标签指向hibernate,指示hibernate如何加载person(默认根据主键加载。)
session_flush
在hibernate中也存在flush这个功能,在默认的情况下session.commit()之前时,其实执行了一个flush命令。
Session.flush功能:
①清理缓存;
②执行sql(确定是执行SQL语句(确定生成update、insert、delete语句等),然后执行SQL语句。)
Session在什么情况下执行flush:
① 默认在事务提交时执行;
② 可以显示的调用flush;
③ 在执行查询前,如:iterate.
注:如果主键生成策略是uuid等不是由数据库生成的,则session.save()时并不会发出SQL语句,只有flush时才会发出SQL语句,但如果主键生成策略是native由数据库生成的,则session.save的同时就发出SQL语句。在flush时会满不在缓存。实体对象只有发出SQL语句保存在数据库中时,session缓存中的一个existsInDatabase才会为true.
uuid主键生成策略:
// 提交事务 // 利用Hibernate将实体类对象保存到数据库中 // 因为user主键生成策略采用的是uuid,所以调用完成save后,只是将user纳入session的管理 // 不会发出insert语句,但是id已经生成,session中的existsInDatabase状态为false session.save(user); // 调用flush,hibernate会清理缓存,执行sql // 如果数据库的隔离级别设置为未提交读,那么我们可以看到flush过的数据,并且session中的exitsInDatabase为true session.flush(); // 提交事务 // 默认情况下commit操作会先执行flush清理缓存,所以不用显示调用flush // commit后数据是无法回滚的。 tx.commit();
native主键生成策略:
// 利用Hibernate将实体类对象保存到数据库中 // 因为user的主键生成策略为native(自动添加),所以调用session.save()后,将执行insert语句,返回由数据库生成的id // 纳入了session的管理,修改了session中existsInDatabase状态为true。 // 如果数据库的隔离级别设置为未提交读,那么我们可以看到save过的数据 session.save(user); // 提交事务 tx.commit();
数据库的隔离级别:并发性作用。
1、 Read Uncommited(未提交读):没有提交就可以读取到数据(发出了Insert,但没有commit就可以读取到。)很少用
2、 Read Commited(提交读):只有提交后才可以读,常用,
3、 Repeatable Read(可重复读):mysql默认级别, 必需提交才能见到,读取数据时数据被锁住。
4、 Serialiazble(序列化读):最高隔离级别,串型的,你操作完了,我才可以操作,并发性特别不好,
数据库的隔离级别:并发性作用。
1、 Read Uncommited(未提交读):没有提交就可以读取到数据(发出了Insert,但没有commit就可以读取到。)很少用
2、 Read Commited(提交读):只有提交后才可以读,常用,
3、 Repeatable Read(可重复读):mysql默认级别, 必需提交才能见到,读取数据时数据被锁住。
4、 Serialiazble(序列化读):最高隔离级别,串型的,你操作完了,我才可以操作,并发性特别不好,
数据库的隔离级别:并发性作用。
1、 Read Uncommited(未提交读):没有提交就可以读取到数据(发出了Insert,但没有commit就可以读取到。)很少用
2、 Read Commited(提交读):只有提交后才可以读,常用,
3、 Repeatable Read(可重复读):mysql默认级别, 必需提交才能见到,读取数据时数据被锁住。
4、 Serialiazble(序列化读):最高隔离级别,串型的,你操作完了,我才可以操作,并发性特别不好,
隔离级别 | 是否存在不可重复读 | 是否存在不可重复读 | 是否存在幻读 |
Read Uncommitted(未提交读) | Y | Y | Y |
Read Commited(提交读) | N | Y(可采用悲观锁解决) | Y |
Repeatable Read(可重复读) | N | N | Y |
Serialiazble(序列化读) |
|
|
脏读:没有提交就可以读取到数据称为脏读
不可重复读:再重复读一次,数据与你上的不一样。称不可重复读。
幻读:在查询某一条件的数据,开始查询的后,别人又加入或删除些数据,再读取时与原来的数据不一样了。
Mysql查看数据库隔离级别:
方法:select @@tx_isolation;
Mysql数据库修改隔离级别:
方法:set transaction isolation level 隔离级别名称;
例如:修改为未提交读:set transaction isolation level read uncommitted;
Session.evict(user)方法:
作用:从session缓存(EntityEntries属性)中逐出该对象
但是与commit同时使用,会抛出异常
session = HibernateUtil.getSession(); tx = session.beginTransaction(); User1 user = new User1(); user.setName("李四"); user.setPassword("123"); user.setCreateTime(new Date()); user.setExpireTime(new Date()); // 利用Hibernate将实体类对象保存到数据库中 // 因为user主键生成策略采用的是uuid,所以调用完成save后,只是将user纳入session的管理 // 不会发出insert语句,但是id已经生成,session中的existsInDatabase状态为false session.save(user); session.evict(user);// 从session缓存(EntityEntries属性)中逐出该对象 // 无法成功提交,因为hibernate在清理缓存时,在session的临时集合(insertions)中取出user对象进行insert操作后需要更新entityEntries属性中的existsInDatabase为true,而我们采用evict已经将user从session中逐出了,所以找不到相关数据,无法更新,抛出异常。 tx.commit();
解决在逐出session缓存中的对象不抛出异常的方法:
在session.evict()之前进行显示的调用session.flush()方法就可以了。
// 利用Hibernate将实体类对象保存到数据库中 // 因为user主键生成策略采用的是uuid,所以调用完成save后,只是将user纳入session的管理 // 不会发出insert语句,但是id已经生成,session中的existsInDatabase状态为false session.save(user); // flush后hibernate会清理缓存,会将user对象保存到数据库中,将session中的insertions中的user对象清除,并且会设置session中的existsInDatabase状态为false session.flush(); session.evict(user);// 从session缓存(EntityEntries属性)中逐出该对象 // 可以成功提交,因为hibernate在清理缓存时,在Session的insertions中集合中无法找到user对象所以不会发出insert语句,也不会更新session中existsInDatabase的状态。 tx.commit();