一、前言
这些天都在为公司框架重构做准备,浏览了一下代码,挑了几个不熟或者没接触过的知识点进行攻坚,hibernate是其中之一。其实接触hibernate是在大学期间,应该是在2012年,已经2017-2012=5年时间了,当初给我的印象就是hibernate难学(特别是关联关系的配置这块内容),没学好,很多概念当时理解不了,于是我经手的项目基本都是使用mybatis,不再去碰这个“麻烦”(所以,给人的第一印象很重要,平时要注意一下形象了)。但是呢,我发现,编程的世界就是这么小,兜兜转转最后还是需要照面,于是乎,我决定,啃下这块骨头~我翻出了2012年的大学课件,也在网上搜索了一大堆的博文,算是理清了hibernate关联关系配置这块内容,果真是“会当凌绝顶,一览众山小”,现在回头想想,hibernate并没有第一印象那么难,只是有些细节需要注意~
本篇博文持续更新,主要是记录一些hibernate使用细节、难点,知识点顺序不分难易,想到哪记到哪儿,有需要自行全文搜索,如有错误之处,还望斧正~
本文运行环境:
jdk1.8.0_131
Eclipse Mars.2 Release (4.5.2)
Hibernate-release-5.2.11.final
Mysql 5.6.14
二、正文
写这篇博文的起因是研究hibernate的关联关系配置过程中,发现很多细节问题,对于新手或者一知半解的人来说,理解起来很困难,作为“过来人”,我希望能用通俗一点的描述加上自己写的实例代码解决同行的疑惑,所以这边就先记录一下“关联关系”配置过程中的问题~
数据库中表与表之间的关系分为三种:一对一,一对多,多对多。数据表是如何体现这种关系的呢?对于一对一和一对多,会在其中一张表增加一个外键字段(有可能和这张表的主键同一字段),关联另外一张表的主键,多对多则会建立一张中间表,存储了两张关系表的主键,hibernate中的关联关系是建立在数据库中表的这层关系之上。至于hibernate中单向、双向问题,完全是业务需求决定的,因为从数据库层面来讲,A表和B表有关联关系,那么必定可以通过连接查询,从A表查询出B表的信息,或者从B表查询出A表的信息,所以,从数据库的层面来说,就是双向的。而到了程序里面,有些时候我们只需要从A表对应(映射)的ClassA查询出B表对应(映射)的ClassB,而不需要从ClassB查询出ClassA,这时我们用单向就行,如果需要双向查询,这样的情况,就需要双向的关联关系。所以希望初学者不要迷惑hibernate中单双向配置问题,这个完全是业务需求决定,要单向就配置单向,要双向就配置双向。
1)cascade和inverse之间的区别
cascade定义的是级联,也就是说对某个对象A做CRUD(增删改查)操作是否同样对其所关联的另外一个对象B做同样的操作。而inverse定义的是维护关联关系的责任,这个怎么理解呢?现有一个数据表Student如下,其中cid表示的是Classes表的id:
Classes表:
表Student中的cid是外键,关联Classes的主键id,这两张表的关联关系就体现在cid字段上,如果某条记录cid为空,那么当条记录就与Classes中的任何记录无关联关系,假如整个表这个字段都为空,那么这张表就和Classes无关联关系。inverse定义的就是谁去维护这个cid字段的责任!就是由谁去设置这个值!这样说可能也不太确切,应该这样表述:哪个类对应的映射配置了inverse="false"(默认都是false,并且只有集合标记“set/map/list/array/bag”才有inverse属性”),那么就是对这个类进行CRUD的时候,触发hibernate去维护这个字段!如果还是不太清楚,那么请看下面代码~
假设现在有一个班级类(Classes),学生类(Student),他们之间是“一对多”的关系,在学生类(Student)中包含一个队Classes类的引用,Classes不包含对学生类的引用,两个类以及对应的映射文件分别如下:
Student类:
1 package com.hibernate.beans; 2 3 public class Student { 4 private int id; 5 private String name; 6 private Classes cls; 7 public int getId() { 8 return id; 9 }10 public void setId(int id) {11 this.id = id;12 }13 public String getName() {14 return name;15 }16 public void setName(String name) {17 this.name = name;18 }19 public Classes getCls() {20 return cls;21 }22 public void setCls(Classes cls) {23 this.cls = cls;24 }25 26 }
Student.hbm.xml:
1 2 56 7 148 9 1110 12 13
Classes类:
1 package com.hibernate.beans; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 public class Classes { 7 private int id; 8 private String clsName; 9 10 public int getId() {11 return id;12 }13 public void setId(int id) {14 this.id = id;15 }16 public String getClsName() {17 return clsName;18 }19 public void setClsName(String clsName) {20 this.clsName = clsName;21 }22 23 24 }
Classes.hbm.xml:
1 2 56 7 8 149 10 1211 13
再附加一个hibernate.cfg.xml的配置吧~
hibernate.cfg.xml:
1 2 56 7 8 24com.mysql.jdbc.Driver 9 10jdbc:mysql://localhost:3306/test 11 12root 13root.123 14 15org.hibernate.dialect.MySQL5InnoDBDialect 16 17true 18 19true 20 2122 23
由Student.hbm.xml中配置可知,配置的是“多对一”关系中的单向关联。注意:在<many-to-one />关联关系中,没有inverse属性,但是默认就是由配置<many-to-one />这端去维护关联关系(也就是设置外键字段的值),相当于默认inverse="false",在<many-to-one />节点有个cascade属性,其取值有如下几个(多个cascade属性之间可以用英文逗号隔开,比如:cascade="save-update,delete"):
1.none :默认值,Session操作当前对象时,忽略其他关联的对象2.delete:当通过Session的delete()方法删除当前的对象时,会级联删除所有关联的对象3.delete-orphan:接触所有和当前对象解除关联关系的对象 例如:customer.getOrders().clear(); 执行后,数据库中的先前与该customer相关联的order都被删除。4.save-update:当通过Session的save()、update()及saveOrUpdate()方法更新或保存当前对象 时,级联保存所有关联的新建的临时对象,并且级联更新所有关联的游离对象5.persist:当通过Session的persist()方法来保存当前对象时,会级联保存所关联的 新建的临时对象6.merge:当通过Session的merge()方法来保存当前对象时,会级联融合所有关联的游离对象7.lock:当通过Session的lock()方法把当前游离对象加入到Session()缓存中时,会把所有关联的游离对象也加入到 Session缓存中。8.replicate:当通过Session的replicate()方法赋值当前对象时,会级联赋值所有关联的对象9.evict:当通过Session的evict()方法从Session缓存中清除当前对象时,会级联清除所有关联的对象10.refresh:当通过Session的refresh()方法刷新当前对象时,会级联刷新所有关联的对象,所为刷新是指读取数据库中相应的数据 然后根据数据库中的最新的数据去同步更新Session缓存中的数据11.all:包含save-update、persist、merge、delete、lock、replicate、evict及refresh的行为12.all-delete-orphan:包含all和delete-orphan的行为
这边配置了cascade="all"属性之后,如果Student中cls有值,那么在保存Student对象的时候,也会保存cls引用的Classess对象到表Classes中,默认cascade="none",此时保存Student对象时,就算cls有值,也不会保存到表Classes中,这就是级联的作用:
HibernateMain类:
1 package com.hibernate.main; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 8 import com.hibernate.beans.Classes; 9 import com.hibernate.beans.Student;10 11 public class HibernateMain {12 13 public static void main(String[] args) {14 // TODO Auto-generated method stub15 Configuration cfg = new Configuration().configure();16 SessionFactory factory = cfg.buildSessionFactory();17 Session session = factory.openSession();18 Transaction ts = session.getTransaction();19 ts.begin();20 21 Student st1 = new Student();22 st1.setName("学生甲");23 24 Student st2 = new Student();25 st2.setName("学生乙");26 27 Classes cls = new Classes();28 cls.setClsName("班级2");29 30 st1.setCls(cls);31 st2.setCls(cls);32 33 session.save(st1);34 session.save(st2);35 36 ts.commit();37 System.exit(0);38 }39 40 }
控制台输出的sql语句执行顺序(一对多关联关系,save时,先save“一”的一方,然后才是“多”的一方,删除的时候,先删除“多”的一方,然后才是“一”的一方):
Hibernate: insert into classes (name) values (?)Hibernate: insert into Student (name, cid) values (?, ?)Hibernate: insert into Student (name, cid) values (?, ?)
数据库中的数据:
classes: student:
现在将两张表数据删除,并且将文件Student.hbm.xml中<many-to-one />节点的cascade属性删除(默认cascade=“none”),然后再执行上面的代码,这个时候你会发现如下报错,这是什么原因呢?前面我有说过,<many-to-one />节点虽然没有inverse属性,但是hibernate默认赋予配置<many-to-one />的一端,在对这个类进行CRUD的时候,触发hibernate去维护体现关联关系的字段(也就是设置“外键”cid的值),在执行的代码里面,Student类实例st1和st2都设置了cls属性,这就向Hibernate表明,需要维护体现关联关系那个字段(因为<many-to-one />默认本端维护,无法修改),但是cascade属性并没有设置(默认为cascade="none"),也就是在保存st1和st2的时候,并不会先保存cls引用的Classes对象,而要维护cid这个“外键”字段时,又必须要先保存Class对象才能获取到这个cid,这边就出现冲突(这边是个人理解,仅供参考,我觉得这边应该还涉及到hibernate中持久化对象状态问题,但是现象上来说可以这儿解释)。如果我们不去设置st1和st2的cls属性,那么我们是能够保存成功的(这边就不贴执行结果了)
十月 11, 2017 3:50:28 下午 org.hibernate.internal.ExceptionMapperStandardImpl mapManagedFlushFailureERROR: HHH000346: Error during managed flush [org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.hibernate.beans.Classes]Exception in thread "main" java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.hibernate.beans.Classes at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:146) at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:157) at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:164) at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1443) at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:493) at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3207) at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2413) at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:467) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:156) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$100(JdbcResourceLocalTransactionCoordinatorImpl.java:38) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:231) at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:68) at com.hibernate.main.HibernateMain.main(HibernateMain.java:36)Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.hibernate.beans.Classes at org.hibernate.engine.internal.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:279) at org.hibernate.type.EntityType.getIdentifier(EntityType.java:462) at org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:315) at org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:326) at org.hibernate.type.TypeHelper.findDirty(TypeHelper.java:325) at org.hibernate.persister.entity.AbstractEntityPersister.findDirty(AbstractEntityPersister.java:4218) at org.hibernate.event.internal.DefaultFlushEntityEventListener.dirtyCheck(DefaultFlushEntityEventListener.java:528) at org.hibernate.event.internal.DefaultFlushEntityEventListener.isUpdateNecessary(DefaultFlushEntityEventListener.java:215) at org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:142) at org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:216) at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:85) at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:38) at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1437) ... 9 more
接下来,我们再测试一下“一对多”双向关联关系,Student类和student.hbm.xml都不需要改变,我们将Classes类和classes.hbm.xml修改如下:
Classes类:
1 package com.hibernate.beans; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 public class Classes { 7 private int id; 8 private String clsName; 9 private Setstudents = new HashSet ();10 11 public int getId() {12 return id;13 }14 public void setId(int id) {15 this.id = id;16 }17 public String getClsName() {18 return clsName;19 }20 public void setClsName(String clsName) {21 this.clsName = clsName;22 }23 public Set getStudents() {24 return students;25 }26 public void setStudents(Set students) {27 this.students = students;28 }29 30 31 }
Classes.hbm.xml:
1 2 56 7 8 199 10 1211 13 14 17 1815 16
然后执行的代码改为:
1 package com.hibernate.main; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 8 import com.hibernate.beans.Classes; 9 import com.hibernate.beans.Student;10 11 public class HibernateMain {12 13 public static void main(String[] args) {14 // TODO Auto-generated method stub15 Configuration cfg = new Configuration().configure();16 SessionFactory factory = cfg.buildSessionFactory();17 Session session = factory.openSession();18 Transaction ts = session.getTransaction();19 ts.begin();20 21 Student st1 = new Student();22 st1.setName("学生甲");23 24 Student st2 = new Student();25 st2.setName("学生乙");26 27 Classes cls = new Classes();28 cls.setClsName("班级2");29 30 cls.getStudents().add(st1);31 cls.getStudents().add(st2);32 session.save(cls);33 ts.commit();34 System.exit(0);35 }36 37 }
运行之后,控制台的sql语句执行顺序如下:
Hibernate: insert into classes (name) values (?)Hibernate: insert into Student (name, cid) values (?, ?)Hibernate: insert into Student (name, cid) values (?, ?)Hibernate: update Student set cid=? where id=?Hibernate: update Student set cid=? where id=?
这个时候你会发现,本来在insert student的时候已经设置了cid,为什么,最后还会有个update操作?这是因为两边的配置默认都要维护表示关联关系的字段cid!之前我提过(往前翻),凡是可以设置inverse属性的地方(只有集合标记“set/map/list/array/bag”才有inverse属性”),如果没有设置,那么默认都是inverse="false",也就是说在操作本端对象的CRUD时,会触发维护体现关联关系字段的操作。在文件Classes.hbm.xml中有配置set节点,但是没有设置inverse属性,默认就是inverse="false",也就是本端负责维护关联关系的那个字段,又因为对端配置的是<many-to-one />默认就赋予它inverse="false"的效果,所以变成两端都维护这个字段。
如果我们此时在文件Classes.hbm.xml中的set节点,配置inverse="true",也就是明确表示自己不参与维护体现关联关系的字段,这时候,我们再执行程序,控制台的sql执行顺序如下:
Hibernate: insert into classes (name) values (?)Hibernate: insert into Student (name, cid) values (?, ?)Hibernate: insert into Student (name, cid) values (?, ?)
这时并没有update的语句!至此,cascade和inverse的使用和区别,我想我已经在上面讲清楚了,如果有错误或者不能理解的地方,请加我建立的群进行探讨~
三、链接
1、
四、联系本人
为方便没有博客园账号的读者交流,特意建立一个企鹅群(纯公益,非利益相关),读者如果有对博文不明之处,欢迎加群交流:261746360,小杜比亚-博客园