hibernate 使用saveOrUpde 報 Batch update returned unexpected row count from update
之前我們使用hibernate3的時候採用xml式配置,如下所示:
<?xml version="1.0" encoding="gb2312"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="com.xx.xx.beans"> <class name="Person" table="person"> <id column="id" name="id" type="java.lang.Long"><generator class="assigned" /></id> <property column="name" length="30" name="name" not-null="true" type="java.lang.String" /> </class> </hibernate-mapping> 複製程式碼
從上面的xml配置檔案中我們可以看出,我們的主鍵使用的是由程式控制的主鍵,也就是說,我們在儲存Person時,必須手動呼叫setter給ID設定一下ID;
後面為了適應註解所以升級了hibernate4,並且改為了註解式配置例項,如下:
@Entity @Table(name = "person") public class Person{ @Id @GenericGenerator(name = "idGenerator", strategy = "assigned") @GeneratedValue(generator = "idGenerator") private Long id; private String name; } 複製程式碼
從程式碼上我們可以看到,為了滿足手動設定ID,我們定義了一個GenericGenerator,strategy為assigned,關於其他的strategy型別,大家可以在網上查閱資料。
這麼配置好後,理論上是沒有什麼問題了,但是當我們呼叫session.saveOrUpdate的時候,會報出標題所示的錯誤:
Batch update returned unexpected row count from update
之前也是一頭霧水,就到網上查閱了各種資料,基本問題有一下幾種:
- 給ID配置了@GeneratedValue但是資料庫中設定為ID自增
- 資料庫中存在重複資料
- 一對多,多對多對映儲存時會出現此異常
關於以上可能性問題我基本一一排除,因為我是手動建表,所以不可能出現ID自增,其次是新表,所以也不可能出現重複資料,然後我是單表,不存在對映關係。
排除以上可能性問題後,沒辦法,只有列印hibernate的查詢語句,通過列印hibernate的sql語句發現,我呼叫saveOrUpdate時,sql為update語句。這就很令人費解,我這個物件基本是如下這種操作:
// service public class PersonServiceImpl implements IPersonService{ @Autowired private IPersonDao personDao; void savePerson(){ // 此處為其他程式碼 Person p = new Person(); p.setId(1L); personDao.saveOrUpdate(p) } } // dao public class PersonDaoImpl implements IPersonDao{ void saveOrUpdate(Person person){ // 虛擬碼,我們採用hibernateTemplate this.hibernateTemplate.saveOrUpdate(person). } } 複製程式碼
從程式碼上看,是不可能會出現update語句的,出現update語句只有一種可能,那就是hibernate認為我new出來的這個物件是遊離態了。沒辦法,只有跟程式碼了。
首先我們看看hibernateTemplate的saveOrUpdate:
public void saveOrUpdate(final Object entity) throws DataAccessException { this.executeWithNativeSession(new HibernateCallback<Object>() { public Object doInHibernate(Session session) throws HibernateException { HibernateTemplate.this.checkWriteOperationAllowed(session); session.saveOrUpdate(entity); return null; } }); } 複製程式碼
我們發現實際上也是呼叫的session的saveOrUpdate,所以通過跟session#saveOrUpdate,發現session的saveOrUpdate是通過類似監聽的機制來實現的:
public final class SessionImpl extends AbstractSessionImpl implements EventSource { private void fireSaveOrUpdate(SaveOrUpdateEvent event) { this.errorIfClosed(); this.checkTransactionSynchStatus(); this.checkNoUnresolvedActionsBeforeOperation(); Iterator i$ = this.listeners(EventType.SAVE_UPDATE).iterator(); while(i$.hasNext()) { SaveOrUpdateEventListener listener = (SaveOrUpdateEventListener)i$.next(); listener.onSaveOrUpdate(event); } this.checkNoUnresolvedActionsAfterOperation(); } } 複製程式碼
繼續深入,我們找到實際會觸發此錯誤的地方:
package org.hibernate.event.internal; public class DefaultSaveOrUpdateEventListener extends AbstractSaveEventListener implements SaveOrUpdateEventListener { protected Serializable performSaveOrUpdate(SaveOrUpdateEvent event) { // 此處為查詢我們儲存的物件的狀態的 EntityState entityState = getEntityState( event.getEntity(), event.getEntityName(), event.getEntry(), event.getSession() ); switch ( entityState ) { case DETACHED: // 遊離態,執行update語句 entityIsDetached( event ); return null; case PERSISTENT: // 持久態,不會執行任何語句 return entityIsPersistent( event ); default: //TRANSIENT or DELETED // 臨時態,會執行insert語句 return entityIsTransient( event ); } } } 複製程式碼
通過閱讀以上程式碼,我們知道問題出在獲取物件狀態的地方,及:getEntityState。讓我們繼續深入挖掘:
package org.hibernate.event.internal; public abstract class AbstractSaveEventListener extends AbstractReassociateEventListener { protected EntityState getEntityState( Object entity, String entityName, EntityEntry entry, //pass this as an argument only to avoid double looking SessionImplementor source) { final boolean traceEnabled = LOG.isTraceEnabled(); if ( entry != null ) { // the object is persistent //the entity is associated with the session, so check its status if ( entry.getStatus() != Status.DELETED ) { // do nothing for persistent instances if ( traceEnabled ) { LOG.tracev( "Persistent instance of: {0}", getLoggableName( entityName, entity ) ); } return EntityState.PERSISTENT; } // ie. e.status==DELETED if ( traceEnabled ) { LOG.tracev( "Deleted instance of: {0}", getLoggableName( entityName, entity ) ); } return EntityState.DELETED; } // the object is transient or detached // the entity is not associated with the session, so // try interceptor and unsaved-value if ( ForeignKeys.isTransient( entityName, entity, getAssumedUnsaved(), source ) ) { if ( traceEnabled ) { LOG.tracev( "Transient instance of: {0}", getLoggableName( entityName, entity ) ); } return EntityState.TRANSIENT; } if ( traceEnabled ) { LOG.tracev( "Detached instance of: {0}", getLoggableName( entityName, entity ) ); } return EntityState.DETACHED; } } 複製程式碼
通過跟蹤程式碼,發現上面判斷型別的程式碼段一個都沒進,預設就返回遊離態(DETACHED)。配合前面的程式碼大家就知道肯定就會執行update語句,但是實際上我們資料庫又沒有這條資料,自然就會報上面的錯誤了。
由於我們知道我們的物件是屬於臨時態(EntityState.TRANSIENT),所以我們來研究ForeignKeys.isTransient
這個方法:
package org.hibernate.engine.internal; public final class ForeignKeys { public static boolean isTransient(String entityName, Object entity, Boolean assumed, SessionImplementor session) { if ( entity == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { // an unfetched association can only point to // an entity that already exists in the db return false; } // 通過攔截器檢查 Boolean isUnsaved = session.getInterceptor().isTransient( entity ); if ( isUnsaved != null ) { return isUnsaved; } // 通過持久程式檢查是否沒有儲存 final EntityPersister persister = session.getEntityPersister( entityName, entity ); isUnsaved = persister.isTransient( entity, session ); if ( isUnsaved != null ) { return isUnsaved; } // we use the assumed value, if there is one, to avoid hitting // the database if ( assumed != null ) { return assumed; } // 獲取資料庫快照 final Object[] snapshot = session.getPersistenceContext().getDatabaseSnapshot( persister.getIdentifier( entity, session ), persister ); return snapshot == null; } } 複製程式碼
通過對以上程式碼的跟蹤,發現persister.isTransient( entity, session );
時返回了false,意思是持久程式已經判斷當前這個物件是已經存在了,那麼這個地方就存在問題,我們繼續深入:
package org.hibernate.persister.entity; public abstract class AbstractEntityPersister implements OuterJoinLoadable, Queryable, ClassMetadata, UniqueKeyLoadable, SQLLoadable, LazyPropertyInitializer, PostInsertIdentityPersister, Lockable { public Boolean isTransient(Object entity, SessionImplementor session) throws HibernateException { // 獲取待儲存物件的ID final Serializable id; if ( canExtractIdOutOfEntity() ) { id = getIdentifier( entity, session ); } else { id = null; } // 如果id為空,預設為臨時態 if ( id == null ) { return Boolean.TRUE; } // 檢查版本號,即樂觀鎖 final Object version = getVersion( entity ); if ( isVersioned() ) { // let this take precedence if defined, since it works for // assigned identifiers Boolean result = entityMetamodel.getVersionProperty() .getUnsavedValue().isUnsaved( version ); if ( result != null ) { return result; } } // 檢查ID是否為臨時態的值 Boolean result = entityMetamodel.getIdentifierProperty() .getUnsavedValue().isUnsaved( id ); if ( result != null ) { return result; } // 檢查是否存在二級快取中 if ( session.getCacheMode().isGetEnabled() && hasCache() ) { final CacheKey ck = session.generateCacheKey( id, getIdentifierType(), getRootEntityName() ); final Object ce = CacheHelper.fromSharedCache( session, ck, getCacheAccessStrategy() ); if ( ce != null ) { return Boolean.FALSE; } } return null; } } 複製程式碼
又跟蹤以上程式碼發現entityMetamodel.getIdentifierProperty().getUnsavedValue().isUnsaved( id );
返回了false。這個方法是幹什麼用的呢,他是獲取我們ID的定義屬性,即我們配置的一些@GeneratedValue等,getUnsavedValue
方法是獲取未儲存的時候的ID包裝,然後通過isUnsaved
方法來對比是否相同。
package org.hibernate.engine.spi; public class IdentifierValue implements UnsavedValueStrategy { /** * 總是假設所有的都是新例項 */ public static final IdentifierValue ANY = new IdentifierValue() { @Override public final Boolean isUnsaved(Object id) { LOG.trace( "ID unsaved-value strategy ANY" ); return Boolean.TRUE; } @Override public Serializable getDefaultValue(Object currentValue) { return (Serializable) currentValue; } @Override public String toString() { return "SAVE_ANY"; } }; /** * 總是假設所有的都不是新例項 */ public static final IdentifierValue NONE = new IdentifierValue() { @Override public final Boolean isUnsaved(Object id) { LOG.trace( "ID unsaved-value strategy NONE" ); return Boolean.FALSE; } @Override public Serializable getDefaultValue(Object currentValue) { return (Serializable) currentValue; } @Override public String toString() { return "SAVE_NONE"; } }; /** * 假設ID為空是,該物件為新例項 */ public static final IdentifierValue NULL = new IdentifierValue() { @Override public final Boolean isUnsaved(Object id) { LOG.trace( "ID unsaved-value strategy NULL" ); return id == null; } @Override public Serializable getDefaultValue(Object currentValue) { return null; } @Override public String toString() { return "SAVE_NULL"; } }; /** * 不假設 */ public static final IdentifierValue UNDEFINED = new IdentifierValue() { @Override public final Boolean isUnsaved(Object id) { LOG.trace( "ID unsaved-value strategy UNDEFINED" ); return null; } @Override public Serializable getDefaultValue(Object currentValue) { return null; } @Override public String toString() { return "UNDEFINED"; } }; @Override public Boolean isUnsaved(Object id) { LOG.tracev( "ID unsaved-value: {0}", value ); return id == null || id.equals( value ); } } 複製程式碼
這裡我們發現,value為null,但是我們的待儲存的物件的ID不為null,肯定就會返回false,問題就出在這裡了。
好了,問題出現原因我們也找到了,現在來想想解決辦法,無非有兩種:
- 設定空物件的value
- 取消IdentifierProperty的配置,讓getIdentifierValue時get到UNDEFINED型別的IdentifierValue
第一種方式直接pass,因為我們系統是業務系統,基本都需要預先設定好ID。那這個空物件的ID值就沒啥用了。綜上所述,所以只有取消掉IdentifierProperty配置,即取消掉bean上的@GenericGenerator和@GeneratedValue:
@Entity @Table(name = "person") public class Person{ @Id private Long id; private String name; } 複製程式碼
OK,問題解決。
附上網上搜集的設定unsaved-value的方式,(未測試)
@Id @GeneratedValue(generator="idGenerator") @GenericGenerator(name="idGenerator", strategy="assigned", parameters = { @Parameter(name = "unsaved-value" , value = "-1") }) private Long id 複製程式碼