在使用hibernate时,经常会碰到一级联操作的问题,一般来说,在一对多的情况下,都是应该使用级联操作的,不管是级联删除还是添加都应该支持。不过,主导方都应该是多这边,那在进行配置时,在一这边使用下面代码将主导权交给多这一边:
mappedby="xxxxx"
在这种情况下,我们在添加多这一边的数据,都是经常使用以下的代码来操作的(引用班级与学生的关系,其中班级以持久化):
Clazz c = Clazz.getDomain(班级,1);//班级 Student stu = new Student();//学生 stu.setClazz(c);//设置班级属性 stu.save();//保存操作
这样的操作,可以避免将主导权交由班级时,在进行学生信息修改时出现的过多的sql问题。然而使用以下代码时,会发生什么情况呢:
Student stu = new Student(); Clazz c = Clazz.getDomain(班级,1); stu.setClazz(c);//设置班级属性 c.getStuList().add(stu);//添加学生 c.update();//更新
简单一看,好像一定会级联保存,因为我们已经对班级强制性的更新了嘛,而且班级的stuList在属性里面也应该是dirty数据,应该被flush,触发级联操作了。然后,事实不是这样的,最终会不会保存学生信息,取决于在班级上对学生的cascade属性。只有当cascade为save_update(包括 配置成all)时,才会触发级联操作,否则会出现意想不到的问题。
具体原因就在于,实际对班级的更新操作,并不会触发对于stuList这个脏数据的判断,真正的更新操作发生在session的flush阶段,并且对于级联操作,也并不是由update进行负责的,而是由hibernate内部在flush阶段根据cascade的配置来决定是否在进行级联的。
更新操作
首先我们来看相应的更新操作,即调用update方法时,hibernate作了哪些工作。
SessionImpl.update 方法 fireUpdate( new SaveOrUpdateEvent(entityName, object, this) );//通知更新事件 //以下就是取得相对应的事件监听器,并执行相应的方法 SaveOrUpdateEventListener[] updateEventListener = listeners.getUpdateEventListeners(); for ( int i = 0; i < updateEventListener.length; i++ ) { updateEventListener[i].onSaveOrUpdate(event); } //这里我们进入了 DefaultUpdateEventListener的onSaveOrUpdate方法 event.setResultId( performSaveOrUpdate( event ) )//执行saveOrUpdate操作,并设置操作的主键,即entity主键 //进入DefaultUpdateListner.performSaveOrUpdate方法,因为这里的班级是作更新操作,所以是已经持久化的,就会调用以下方法 entityIsPersistent(event) //进入此方法之后,我们会看到,其实这个方法没有作任何事情,只是简单地作了一些判断,并直接将entity的id返回即OK了。详细代码如下所示: protected Serializable entityIsPersistent(SaveOrUpdateEvent event) throws HibernateException { log.trace( "ignoring persistent instance" ); EntityEntry entityEntry = event.getEntry(); 。。。。。。 final SessionFactoryImplementor factory = event.getSession().getFactory(); Serializable requestedId = event.getRequestedId(); Serializable savedId; if ( requestedId == null ) { savedId = entityEntry.getId(); } 。。。。。。 return savedId; } }
由如上的跟踪代码可知,hibernate在进行update操作时,并没有作任何事件,它仅仅是判断当前对象是否为受管对象(managed),如果不对,则获取相应数据并初始化相应数据,将其放入hibernate上下文中。那么最终的更新操作在哪儿呢,答案就在于session的flush。
提交(Commit)和刷新(Flush)
我们在作基本的hibernate例子时,都经常会使用transaction.commit来最终将事件提交。如果仅仅理解为此提交仅会处理保存动作的话,那就太简单了。其实commit会处理很多操作,除发送数据库的commit操作之外,还会进行flush操作。详细的处理代码如下所示:
代码来自于JdbcTransaction public void commit() throws HibernateException { //此处为刷新上下文,即执行flush操作 if ( !transactionContext.isFlushModeNever() && callback ) { transactionContext.managedFlush(); //if an exception occurs during flush, user must call rollback() } ......其它操作 }
在以上的代码中,对程序中最重要的即是它的managedFlush操作,此操作最终会调用到sessionImpl的managedFlush->flush操作。即进行上下文的刷新。刷新操作会把所有的脏数据及未持久化的信息持久化到数据库当中。包括 插入,更新,删除等。因此,对于班级信息中的更新和级联,都在flush中集中处理。
session刷新
整个flush会执行以下的操作
代码来自于DefaultFlushEventListener的onFlush(event)方法 flushEverythingToExecutions(event);//准备flush之前的一些操作,即将要进行的操作添加到执行列表中,以在下面一行代码中进行处理,此方法会预先处理cascade操作 performExecutions(source);//执行当前上下文的对象的插入,更新,删除操作 postFlush(source);//其它清理操作
我们要关心的即是其中的flushEverythingToExecutions(event)方法,因为此操作将影响到cascade对于级联属性的操作,见代码如下:
EventSource session = event.getSession(); final PersistenceContext persistenceContext = session.getPersistenceContext(); session.getInterceptor().preFlush( new LazyIterator( persistenceContext.getEntitiesByKey() ) ); prepareEntityFlushes(session);//执行cascade操作 prepareCollectionFlushes(session);//设置要处理的上下文集合的操作,即在集合中由于各种lazy操作时未作实时处理的信息(如执行add时,并没有实时添加到集合中,而是添加到集合的队列中) try { flushEntities(event);//处理上下文实体对象 flushCollections(session);//处理上下文中的集合对象 }
最终影响cascade的处理即在方法prepareEntityFlushes中,此方法代码如下:
cascadeOnFlush( session, entry.getPersister(), me.getKey(), anything );//处理实体的cascade操作 #进入AbstractFlushingEventListener的cascadeOnFlush方法 new Cascade( getCascadingAction(), Cascade.BEFORE_FLUSH, session ) .cascade( persister, object, anything ); #进入Cascade.cascade方法 CascadeStyle[] cascadeStyles = persister.getPropertyCascadeStyles();//找到所有cascade定义的属性 for ( int i=0; i<types.length; i++) { final CascadeStyle style = cascadeStyles[i]; //执行cascade if ( style.doCascade( action ) ) { cascadeProperty( parent, persister.getPropertyValue( parent, i, entityMode ), types[i], style, propertyName, anything, false ); }
对于以上代码的cascadeProprty方法,最终会调用到类CascadingAction的cascade方法,其中如果我们配置为saveOrUpdate的话,则最终会调用到对象CascadingAction.SAVE_UPDATE(匿名对象)的cascade方法,其内部实现即为
public void cascade(EventSource session, Object child, String entityName, Object anything, boolean isCascadeDeleteEnabled) throws HibernateException { if ( log.isTraceEnabled() ) { log.trace( "cascading to saveOrUpdate: " + entityName ); } session.saveOrUpdate(entityName, child); }
这样才行实现我们的级联添加操作。
session刷新集合一定会更新集合吗?
有的同学注意到了类AbstractFlushingEventListener中的flushCollections(session)方法,此方法会准备刷新上下文中的集合对象,在本文中即学生列表对象。没错,这是对的。它最终会调用到ActionQueue.executeActions( collectionUpdates )方法,即执行集合更新方法。但这个方法会更新我们的学生列表吗?答案在方法实现中:
#跟踪代码,最终进入到类AbstractCollectionPersister.updateRows方法,即执行更新集合列表方法 if ( !isInverse && collection.isRowUpdatePossible() ) { ...... //update all the modified entries int count = doUpdateRows( id, collection, session ); }
答案再明白不过了,如果想要更新集合对象,必须要求isInverse为false,即采用一这边为主导。这和我们的一对多配置不同,因为使用了mappedby,所以isInverse为true,即表示将主导权交由多这一边维护。既然这样,那新添加的学生对象就什么事情也不会发生了。
结论
对于一对多这种关系,一般情况下,将主导权交由多这一边维护,这是正常的,且是推荐的。但如果想要在有些时候往集合中使用getXXXList.add(obj).update()类似的操作的话,则必须配置cascade属性,表示级联操作。否则,在运行过程中,你会发现明明往集合中添加了对象,而且getXXList的size也正常,但数据就是保存不到数据库。这个时候,就要检查你的配置了。
#没有配置cascade的奇怪现象 int i = 班级.getStuList().size();//初始为0 Student stu = new Student(); stu.setClazz(班级); 班级.getStuList().add(班级); 班级.update();//更新操作 i = 班级.getStuList.size();//现在为1了。 but,重新刷新界面,重新执行查询方法,查询班级中学生列表信息, i = 班级.getStuList().size;//怎么又是0呢?为什么没保存到数据库????
如果你碰到上面的现象,不妨看看本文,希望对你有用。
转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201111220001.html