Hibernate级联之一对多和inverse解析
hibernate的级联可以说是hibernate最重要的部分,只有深入了解了级联的特性与用法,才能运用自如。
这次讨论一对多的情况,所以就使用博客项目的用户表和博客表作为示例,来一起学习hibernate的级联
基本准备
文件结构:
hibernate核心配置文件hibernate.cfg.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <!-- 先配置SessionFactory标签,一个数据库对应一个SessionFactory标签 --> <session-factory> <!-- 必须的配置的参数5个,4个连接参数,1个数据库方言 --> <!-- #hibernate.connection.driver_class com.mysql.jdbc.Driver #hibernate.connection.url jdbc:mysql:///test #hibernate.connection.username gavin #hibernate.connection.password 数据库方言 #hibernate.dialect org.hibernate.dialect.MySQLDialect --> <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property> <property name="hibernate.connection.url">jdbc:mysql:///blog</property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">123456</property> <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property> <!-- 可选配置 --> <!-- 显示sql语句 --> <property name="hibernate.show_sql">true</property> <!-- 格式化sql语句 --> <property name="hibernate.format_sql">false</property> <!-- 生成数据库的表结构 (hbm2dd全称hibernate mapping to db define language auto create) update 没表会自动创建,有表添加数据。 如果开发中间需要添加字段,可以在实体类添加属性。update会自动在数据库添加字段,并且不改变原来数据库值 validate 校验实体属性和数据库是否一致 --> <property name="hibernate.hbm2ddl.auto">update</property> <!-- 映射配置文件,可以在map配置文件右键copy qualified name--> <mapping resource="com/cky/domain/User.hbm.xml"/> <mapping resource="com/cky/domain/Blog.hbm.xml"/> </session-factory> </hibernate-configuration>
如果对hibernate的配置还不是很清楚,可以看看这里
实体类的创建
Hibernate中,可以直接将表的关系用对象表示。
如本例中,一个博客只能有一个作者,所以Blog就可以添加一个User对象。
一个用户有多个博客,所以可以在User中添加一个Blog的Set集合。
这里需要注意的是如果关联的是一个对象,那么不能在类中进行初始化new操作。
如果关联的是一个集合,那么必须用HashSet在类中进行初始化new操作
实体类Blog.java
package com.cky.domain; import java.sql.Timestamp; public class Blog { private int bId; private String bSubject; private String bContent; private Timestamp createtime; private Timestamp updatetime; //hibernate中关联对象不能初始化 private User user; //...getter setter 方法省略 public int getbId() { return bId; } public void setbId(int bId) { this.bId = bId; } public String getbSubject() { return bSubject; } public void setbSubject(String bSubject) { this.bSubject = bSubject; } public String getbContent() { return bContent; } public void setbContent(String bContent) { this.bContent = bContent; } public Timestamp getCreatetime() { return createtime; } public void setCreatetime(Timestamp createtime) { this.createtime = createtime; } public Timestamp getUpdatetime() { return updatetime; } public void setUpdatetime(Timestamp updatetime) { this.updatetime = updatetime; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } }
实体类User.java
package com.cky.domain; import java.sql.Timestamp; import java.util.HashSet; import java.util.Set; public class User { private Integer uId; private String uEmail; private String uName; private String uUsername; private String uPassword; private String uAge; private String uDetail; private String uAvatar; private String isAdmin; private Timestamp createtime; private Timestamp updatetime; //hibernate的集合必须初始化 private Set<Blog> blogs=new HashSet<Blog>(); //...getter setter 方法省略 public Integer getuId() { return uId; } public void setuId(Integer uId) { this.uId = uId; } public String getuEmail() { return uEmail; } public void setuEmail(String uEmail) { this.uEmail = uEmail; } public String getuName() { return uName; } public void setuName(String uName) { this.uName = uName; } public String getuUsername() { return uUsername; } public void setuUsername(String uUsername) { this.uUsername = uUsername; } public String getuPassword() { return uPassword; } public void setuPassword(String uPassword) { this.uPassword = uPassword; } public String getuAge() { return uAge; } public void setuAge(String uAge) { this.uAge = uAge; } public String getuDetail() { return uDetail; } public void setuDetail(String uDetail) { this.uDetail = uDetail; } public String getuAvatar() { return uAvatar; } public void setuAvatar(String uAvatar) { this.uAvatar = uAvatar; } public String getIsAdmin() { return isAdmin; } public void setIsAdmin(String isAdmin) { this.isAdmin = isAdmin; } public Timestamp getCreatetime() { return createtime; } public void setCreatetime(Timestamp createtime) { this.createtime = createtime; } public Timestamp getUpdatetime() { return updatetime; } public void setUpdatetime(Timestamp updatetime) { this.updatetime = updatetime; } public Set<Blog> getBlogs() { return blogs; } public void setBlogs(Set<Blog> blogs) { this.blogs = blogs; } }
编写基础映射文件
多对一情况映射文件的编写
多对一时,使用<many-to-one>标签,只需要指定三个属性:
name:指定此标签所映射的属性名
class:关联的表所对应的实体类的全限定类名
column:关联表的外键名
Blog.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> <class name="com.cky.domain.Blog" table="blog"> <id name="bId" column="b_id"> <generator class="native"></generator> </id> <!-- 普通属性 --> <property name="bSubject" column="b_subject"></property> <property name="bContent" column="b_content"></property> <property name="createtime" column="createtime"></property> <property name="updatetime" column="updatetime"></property> <!-- private User user; 多对一 配置--> <many-to-one name="user" class="com.cky.domain.User" column="u_id" ></many-to-one> </class> </hibernate-mapping>
一对多情况映射文件的编写
与多对一情况不同的是,一对多时关联对象是一个set集合。
配置文件需要使用<set>标签来和集合对象建立联系,其中的name指定对应的属性名
在<set>中,需要指定查询关联对象所需要的表(实体类)和比较字段(外键)
User.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> <class name="com.cky.domain.User" table="user"> <!-- 配置id name实体类属性,column表字段,如果一样,column可以省略。--> <id name="uId" column="u_id"> <!-- 主键生成策略 --> <generator class="native"></generator> </id> <!-- 普通属性--> <property name="uEmail" column="u_email"></property> <property name="uName" column="u_name"></property> <property name="uUsername" column="u_username"></property> <property name="uPassword" column="u_password"></property> <property name="uAge" column="u_age"></property> <property name="uDetail" column="u_detail"></property> <property name="uAvatar" column="u_avatar"></property> <property name="isAdmin" column="is_admin"></property> <property name="createtime" column="createtime"></property> <property name="updatetime" column="updatetime"></property> <!-- private Set<Blog> blogs=new HashSet<Blog>(); 集合的配置 name:这个类中对应的属性名 --> <set name="blogs"> <!--column: 外键,hibernate会根据这个字段来查询与这个对象对应的多端的所有对象 --> <key column="u_id"></key> <!--class:集合代表的实体类,同时也代表要查询的表。 与上面的条件结合,就可以查询出表中所有外键字段为指定值的所有结果的集合。 --> <one-to-many class="com.cky.domain.Blog"/> </set> </class> </hibernate-mapping>
为了方便使用,还需要一个工具类HibernateUtils.java,很简单就不介绍了,下面是代码:
package com.cky.utils; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; public class HibernateUtils { //ctrl+shift+x private static final Configuration CONFIG; private static final SessionFactory FACTORY; //编写静态代码块 static { //加载XML的配置文件 CONFIG =new Configuration().configure(); //构造工作 FACTORY=CONFIG.buildSessionFactory(); } /** * 从工厂获取session对象 */ public static Session getSession() { return FACTORY.openSession(); } }
测试基础配置(不使用级联)
到这里,基本的配置都设置完了,接下来测试配置的怎么样
package com.cky.Demo; import org.hibernate.Session; import org.hibernate.Transaction; import org.junit.Test; import com.cky.domain.Blog; import com.cky.domain.User; import com.cky.utils.HibernateUtils; public class CascadeTest { @Test public void testMTO2() { Session session = HibernateUtils.getSession(); Transaction tr = session.beginTransaction(); //保存用户和博客 User user=new User(); user.setuName("王五"); Blog blog1=new Blog(); blog1.setbSubject("王五日常一"); blog1.setbContent("看电视"); Blog blog2=new Blog(); blog2.setbSubject("王五日常二"); blog2.setbContent("玩游戏"); //为用户添加博客 user.getBlogs().add(blog1); user.getBlogs().add(blog2); //保存用户 session.save(user); tr.commit(); session.close(); } }
什么,居然报错了:TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing
翻译一下,大致意思就是user对象引用了一个瞬时对象,因为当save(user)时,user已经被保存到缓存成为持久态对象,而给他添加的blog1和blog2,因为没有设置级联,所以不会被自动添加到缓存中,依然是瞬时态对象。
解决方法就是把两个blog1和blog2也进行save(),保存到session中:
@Test public void testMTO2() { //.....上面省略 //保存用户 session.save(user); session.save(blog1); session.save(blog2); tr.commit(); session.close(); }
关于级联
hibernate中用cascade属性设置级联。
在基础的配置中,因为没有设置级联,默认是none,也就是不进行级联操作。
就如上面的代码一样,我们需要手动的保证对象和他级联的对象都在同一状态,才能正确运行,这显然是很麻烦的,下面就看看如何通过设置级联属性来让代码更简单。
cascade取值共有5个:
none 默认值,不级联
save-update 在保存、更新操作时自动级联保存更新关联对象
delete 在删除时自动级联删除关联对象
all 类似save-update-delete,即所以的操作都会级联
all-delete-orphan 解除某一节点的关系时删除该节点(默认只是清除外键关系)
接下来就在上面的基础配置上添加上面的属性看看有什么区别:
save-update:
为user配置文件的添加cascade属性
<set name="blogs" cascade="save-update"> <key column="u_id"></key> <one-to-many class="com.cky.domain.Blog" /> </set>
此时我们运行上次报错的那段代码:
@Test public void testMTO() { Session session = HibernateUtils.getSession(); Transaction tr = session.beginTransaction(); //保存用户和博客 User user=new User(); user.setuName("王五"); Blog blog1=new Blog(); blog1.setbSubject("王五日常一"); blog1.setbContent("看电视"); Blog blog2=new Blog(); blog2.setbSubject("王五日常二"); blog2.setbContent("玩游戏"); user.getBlogs().add(blog1); user.getBlogs().add(blog2); blog1.setUser(user); blog2.setUser(user); //自动关联 session.save(user); //删除掉保存blog的代码 tr.commit(); session.close(); }
发现可以正确执行,因为保存user时,会自动级联保存两个blog,所以他们就全是持久态。
我们同时为blog配置文件添加cascade属性
<many-to-one name="user"
class="com.cky.domain.User"
column="u_id"
cascade="save-update"></many-to-one>
然后保存一个blog看看会发生什么
@Test public void testMTO() { Session session = HibernateUtils.getSession(); Transaction tr = session.beginTransaction(); User user=new User(); user.setuName("王五"); Blog blog1=new Blog(); blog1.setbSubject("王五日常一"); blog1.setbContent("看电视"); Blog blog2=new Blog(); blog2.setbSubject("王五日常二"); blog2.setbContent("玩游戏"); user.getBlogs().add(blog1); user.getBlogs().add(blog2); blog1.setUser(user); blog2.setUser(user); /*session.save(user); session.save(blog1);*/ //只保存blog2 session.save(blog2); tr.commit(); session.close(); }
运行成功,不过更有意思的是他保存了三条信息,而不是两条。
因为当保存 blog2 时,会级联保存 user ,而user又会级联把 blog1 保存
删除也是同样的道理,就不演示了,下面再研究一个all-delete-orphan,传说的孤儿删除
关于all-delete-orphan
all-delete-orphan上面已经简单介绍过,就是解除关系时会把节点删除而不只是删除外键。
我们把使用和不使用孤儿删除分别用代码实现,并做一次比较:
正常情况下的解除关系:
原来的blog表中两条数据都和user id=1产生关系
现在我们把user和其中一个blog id=1解除关系
//普通解除关系 @Test public void testMTO4() { Session session = HibernateUtils.getSession(); Transaction tr = session.beginTransaction(); User user=(User) session.get(User.class, 1); Blog blog=(Blog) session.get(Blog.class, 1); //解除关系只需要把user集合中的blog移除即可 user.getBlogs().remove(blog); tr.commit(); session.close(); }
运行sql:
再看看表情况:
正常情况,解除关系只是删除外键。
使用all-delete-orphan时解除关系:
为user配置文件添加all-delete-orphan
<set name="blogs" cascade="all-delete-orphan"> <key column="u_id"></key> <one-to-many class="com.cky.domain.Blog" /> </set>
执行同样的代码解除关系:
//孤儿删除 @Test public void testMTO4() { Session session = HibernateUtils.getSession(); Transaction tr = session.beginTransaction(); User user=(User) session.get(User.class, 1); Blog blog=(Blog) session.get(Blog.class, 1); //解除关系只需要把user集合中的blog移除即可 user.getBlogs().remove(blog); tr.commit(); session.close(); }
sql的执行情况
数据表变化:
关于inverse(外键维护)
什么是外键维护呢?
就是在两个关联对象中,如果关系发生改变需要修改外键。这么一说感觉这个功能肯定是必备的,要不然这么保证对象之间的关系呢?
在hibernate是根据对象关系来判断是否要维护外键。
这里有两个关键字,对象关系和外键。
什么是对象关系?在hibernate中就是你这个对象A存的有对象B的引用,那么对象A就有对象B的的对象关系。有趣的是,对象关系可以是单向的,即A有B的对象关系,B不一定有A的对象关系。Hibernate是根据对象的对象关系来进行外键处理的。如果两边的对象关系都改变,那么默认hibernate都会进行外键处理(处理两次)。
举个例子
user1有blog1和blog2俩对象关系 、user2有blog3和blog4俩对象关系
1.现在我们把blog3添加到user1中(对象关系改变)
2.因为这时blog3中的user还是user2,还要把blog3的user换成user1(对象关系改变)
上面两个操作都改变了对象关系,如之前说的,session的缓存和快照不一致了,对于User对象,需要更新外键,对于Blog对象,也需要更新外键。
但是,他们更新的是同一外键,也就是说对同一外键更新了两次,多了一个无意义的操作无疑增加了数据库的压力。
也许有人可能会说,我不执行步骤2不就行了,结果还是正确的,还减少了sql。
但是按照人的思维定式,在不知道的情况还是会按上面两个步骤走,感觉更合理。
所以解决方法就在一方放弃外键维护。并且在多对多的情况下必须有一方需要放弃外键,否者程序无法运行。