在OneToMany时一定要使用cascade以级联操作

在使用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

相关文章:

作者: flym

I am flym,the master of the site:)

发表评论

邮箱地址不会被公开。 必填项已用*标注