learn and grow up

hibernate自动更新的坑

字数统计: 1.1k阅读时长: 5 min
2019/08/29 Share

昨天自己负责的一个系统的生产环境出现了一个神奇的BUG:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[2019-08-29 19:54:27.819] [ERROR] [org.hibernate.engine.jdbc.batch.internal.BatchingBatch.performExecution(BatchingBatch.java:137)] [qtp1165897474-48619] : HHH000315: Exception executing batch [Batch update returned unexpected row count from update [1]; actual row count: 2; expected: 1]
[2019-08-29 19:54:27.824] [ERROR] [com.sf.hos.itv.biz.impl.CandidatePushBiz.SendEmails(CandidatePushBiz.java:569)] [qtp1165897474-48619] : com.sf.hos.itv.biz.impl.CandidatePushBiz.SendEmails error
org.hibernate.jdbc.BatchedTooManyRowsAffectedException: Batch update returned unexpected row count from update [1]; actual row count: 2; expected: 1
at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:89) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.jdbc.Expectations$BasicExpectation.verifyOutcome(Expectations.java:73) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.jdbc.batch.internal.BatchingBatch.checkRowCounts(BatchingBatch.java:151) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.jdbc.batch.internal.BatchingBatch.performExecution(BatchingBatch.java:128) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.jdbc.batch.internal.BatchingBatch.doExecuteBatch(BatchingBatch.java:111) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.jdbc.batch.internal.AbstractBatchImpl.execute(AbstractBatchImpl.java:163) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.executeBatch(JdbcCoordinatorImpl.java:226) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:482) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:349) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:350) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:67) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1191) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1675) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.internal.CriteriaImpl.list(CriteriaImpl.java:380) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at com.sf.novatar.base.dao.CommonDao.findFirstByAny(CommonDao.java:812) ~[sf-novatar-frame-1.14.20171110.jar:1.14]
at com.sf.novatar.base.dao.BaseBeanDao.findFirstBy(BaseBeanDao.java:181) ~[sf-novatar-frame-1.14.20171110.jar:1.14]
at com.sf.hos.core.dao.impl.HosSapPersonDao.getSapPersonByEmpNum(HosSapPersonDao.java:60) ~[sf-hos-core-1.0.20190719.jar:1.0]
.............

第一次看到日志惊了。第一:以前为什么没出现这个问题,偏偏这条数据出现了问题。第二:这明明是个select语句getSapPersonByEmpNum,为啥会报Batch update ERROR。

  • 第一个问题:找了代码好长时间还是没找到问题,最后没办法,在本地一步步debug,打印出来所有会update的地方,最终发现了猫腻,原来系统里有个地方获取了PERSON对象后调用了set方法,hibernate机制是会对持久化对象的set进行自动执行update:但是set是针对单个实体对象进行的,hibernate自动帮我们执行update时,根据我们在xml内配置的实体主键去找数据并更新,更新前检查更新行数和实体更新数,而我们设置的实体id在数据库里却没做唯一约束,业务数据会出现极少数id重复的情况!这条更新刚刚好遇到了重复id的问题,根据id的update会更新2条数据,所以才会报这个错:actual row count: 2; expected: 1!!

教训:hibernate xml内设置的表主键要按照实际的主键来或者加unique约束,不然可能会上述问题

  • 第二个问题:为什么会在select的时候触发update呢?

首先猜想是因为在service层声明了@Transactional(rollbackFor = Exception.class),所以在service正常结束需要提交事务的时候帮我们提交了该持久化对象的set,但是不对啊,系统后面还有好几个方法块,还没到return。

继续,我们再跟踪上面的异常栈,发现hibernate在查询的时候list方法中,有一层判断:

org.hibernate.internal.SessionImpl.list(String, QueryParameters)–>org.hibernate.internal.SessionImpl.autoFlushIfRequired(Set)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* detect in-memory changes, determine if the changes are to tables
* named in the query and, if so, complete execution the flush
*/
protected boolean autoFlushIfRequired(Set querySpaces) throws HibernateException {
errorIfClosed();
if ( ! isTransactionInProgress() ) {
// do not auto-flush while outside a transaction
return false;
}
AutoFlushEvent event = new AutoFlushEvent( querySpaces, this );
for ( AutoFlushEventListener listener : listeners( EventType.AUTO_FLUSH ) ) {
listener.onAutoFlush( event );
}
return event.isFlushRequired();
}
//大致意思和流程为:获取当前查询涉及到的querySpaces也就是tables,然后去和session缓存内的需要updte的querySpaces进行比较。如果存在则刷新flush至数据库。

原来如此,所以才会导致select的时候出发update的操作。

所以我们还是注意:

需要谨慎调用hibernate加载的实体对象的set方法!。如果不需要hibernate帮我们自动更新持久化对象,一定要:getCurrentSession().evict(entity),通过调用evict(entity)的方法,把缓存持久化对象变成托管状态。变成托管状态后,Hibernate就不会再去自动更新该实体。

CATALOG