Description: Hibernate appears to be end up in an inconsistent state when working with a many-to-many mapping using cascade = ALL.
The test below shows Hibernate persisting a Root (with a Value mapped as many-to-many), then later deleting the Root (which also deletes the Value). When attempting to delete the Value, hibernate's "listing entities" logging suggests that the Value instance is still persisted, but the Root deletion has removed this. StaleStateException is then thrown.
Hibernate version: 3.2
Annotated classes:
Code:
@Entity
public class RootOfManyToMany
{
@Id
@GeneratedValue( generator = "hibseq" )
@GenericGenerator( name = "hibseq", strategy = "hilo" )
public Long id;
@ManyToMany( fetch = FetchType.EAGER, cascade = javax.persistence.CascadeType.ALL )
@JoinTable(
name = "MAPPING_TABLE",
joinColumns = {@JoinColumn(name = "ROOT_ID")},
inverseJoinColumns = {@JoinColumn(name = "VALUE_ID")}
)
public List< ValueOfManyToMany > values;
}
Code:
@Entity
public class ValueOfManyToMany
{
@Id
@GeneratedValue( generator="hibseq" )
@GenericGenerator( name="hibseq", strategy="hilo" )
public Long id;
}
Spring config:
Code:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<!-- HSQL for local tests -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName"> <value>org.hsqldb.jdbcDriver</value> </property>
<property name="username"> <value>sa</value> </property>
<property name="password"> <value></value> </property>
<property name="maxActive"> <value>2</value> </property>
<property name="poolPreparedStatements"> <value>false</value> </property>
<property name="url" value="jdbc:hsqldb:mem:test"/>
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
<prop key="hibernate.show.sql">true</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
<property name="annotatedClasses">
<list>
<value>RootOfManyToMany</value>
<value>ValueOfManyToMany</value>
</list>
</property>
</bean>
<bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate">
<constructor-arg ref="sessionFactory"/>
</bean>
</beans>
Code between sessionFactory.openSession() and session.close():Code:
public void test_manyToManyRelationship() throws Exception
{
RootOfManyToMany rootOfManyToMany = new RootOfManyToMany();
rootOfManyToMany.values = new ArrayList<ValueOfManyToMany>();
rootOfManyToMany.values.add( new ValueOfManyToMany() );
assertEquals( 0, hibernateTemplate.find( "from RootOfManyToMany" ).size() );
Long id = (Long)hibernateTemplate.save( rootOfManyToMany );
hibernateTemplate.flush();
assertEquals( 1, hibernateTemplate.find( "from RootOfManyToMany" ).size() );
RootOfManyToMany root = (RootOfManyToMany)hibernateTemplate.get( RootOfManyToMany.class, id );
ValueOfManyToMany value = root.values.get( 0 );
hibernateTemplate.delete( root );
hibernateTemplate.flush();
hibernateTemplate.delete( value ); //Exception thrown here
}
Full stack trace of any exception that occurs:org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException: Batch update returned unexpected row count from update: 0 actual row count: 0 expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update: 0 actual row count: 0 expected: 1
Caused by: org.hibernate.StaleStateException: Batch update returned unexpected row count from update: 0 actual row count: 0 expected: 1
at org.hibernate.jdbc.BatchingBatcher.checkRowCount(BatchingBatcher.java:93)
at org.hibernate.jdbc.BatchingBatcher.checkRowCounts(BatchingBatcher.java:79)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:58)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:242)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:235)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:144)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:297)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:993)
at org.springframework.orm.hibernate3.HibernateAccessor.flushIfNecessary(HibernateAccessor.java:388)
at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:363)
at org.springframework.orm.hibernate3.HibernateTemplate.delete(HibernateTemplate.java:774)
at org.springframework.orm.hibernate3.HibernateTemplate.delete(HibernateTemplate.java:770)
at RootOfManyToManyTest.test_manyToManyRelationship(RootOfManyToManyTest.java:40)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:128)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Name and version of the database you are using:HSQL 1.8.0.2
The generated SQL (notable log entries with <returned values>:Code:
select rootofmany0_.id as id4_ from ROOT_OF_MANY_TO_MANY rootofmany0_
<returns empty list>
insert into ROOT_OF_MANY_TO_MANY (id) values (?)
binding '1' to parameter: 1
insert into VALUE_OF_MANY_TO_MANY (id) values (?)
binding '32768' to parameter: 1
insert into MAPPING_TABLE (ROOT_ID, VALUE_ID) values (?, ?)
binding '1' to parameter: 1
binding '32768' to parameter: 2
listing entities:
RootOfManyToMany{values=[ValueOfManyToMany#32768], id=1}
ValueOfManyToMany{id=32768}
<returns id=1>
select rootofmany0_.id as id4_ from ROOT_OF_MANY_TO_MANY rootofmany0_
result set row: 0
returning '1' as column: id4_
select values0_.ROOT_ID as ROOT1_1_, values0_.VALUE_ID as VALUE2_1_, valueofman1_.id as id5_0_ from MAPPING_TABLE values0_ left outer join VALUE_OF_MANY_TO_MANY valueofman1_ on values0_.VALUE_ID=valueofman1_.id where values0_.ROOT_ID=?
returning '1' as column: ROOT1_1_
returning '32768' as column: VALUE2_1_
done processing result set (1 rows)
<returns singleton list>
select rootofmany0_.id as id4_1_, values1_.ROOT_ID as ROOT1_3_, valueofman2_.id as VALUE2_3_, valueofman2_.id as id5_0_ from ROOT_OF_MANY_TO_MANY rootofmany0_ left outer join MAPPING_TABLE values1_ on rootofmany0_.id=values1_.ROOT_ID left outer join VALUE_OF_MANY_TO_MANY valueofman2_ on values1_.VALUE_ID=valueofman2_.id where rootofmany0_.id=?
binding '1' to parameter: 1
returning '1' as column: ROOT1_3_
returning '32768' as column: VALUE2_3_
done processing result set (1 rows)
<returns previously persisted root>
delete from MAPPING_TABLE where ROOT_ID=?
binding '1' to parameter: 1
delete from VALUE_OF_MANY_TO_MANY where id=?
binding '32768' to parameter: 1
delete from ROOT_OF_MANY_TO_MANY where id=?
binding '1' to parameter: 1
listing entities:
ValueOfManyToMany{id=32768}
<deletes all DB content including ValueOfManyToMany instance>
delete from VALUE_OF_MANY_TO_MANY where id=?
binding '32768' to parameter: 1
<throws exception>
Debug level Hibernate log excerpt:Cropped to the tail of first delete:
Code:
DEBUG: Flushed: 0 insertions, 0 updates, 1 deletions to 1 objects
DEBUG: Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
DEBUG: listing entities:
DEBUG: ValueOfManyToMany{id=32768}
DEBUG: executing flush
DEBUG: registering flush begin
DEBUG: Deleting entity: [ValueOfManyToMany#32768]
DEBUG: about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
DEBUG: opening JDBC connection
DEBUG: delete from ValueOfManyToMany where id=?
DEBUG: preparing statement
DEBUG: binding '32768' to parameter: 1
DEBUG: Adding to batch
DEBUG: Executing batch size: 1
ERROR: Exception executing batch:
org.hibernate.StaleStateException: Batch update returned unexpected row count from update: 0 actual row count: 0 expected: 1
at org.hibernate.jdbc.BatchingBatcher.checkRowCount(BatchingBatcher.java:93)
at org.hibernate.jdbc.BatchingBatcher.checkRowCounts(BatchingBatcher.java:79)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:58)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:242)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:235)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:144)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:297)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:993)
at org.springframework.orm.hibernate3.HibernateAccessor.flushIfNecessary(HibernateAccessor.java:388)
at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:363)
at org.springframework.orm.hibernate3.HibernateTemplate.delete(HibernateTemplate.java:774)
at org.springframework.orm.hibernate3.HibernateTemplate.delete(HibernateTemplate.java:770)
at RootOfManyToManyTest.test_manyToManyRelationship(RootOfManyToManyTest.java:40)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:128)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
DEBUG: about to close PreparedStatement (open PreparedStatements: 1, globally: 1)
DEBUG: closing statement
ERROR: Could not synchronize database state with session
org.hibernate.StaleStateException: Batch update returned unexpected row count from update: 0 actual row count: 0 expected: 1
at org.hibernate.jdbc.BatchingBatcher.checkRowCount(BatchingBatcher.java:93)
at org.hibernate.jdbc.BatchingBatcher.checkRowCounts(BatchingBatcher.java:79)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:58)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:242)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:235)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:144)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:297)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:993)
at org.springframework.orm.hibernate3.HibernateAccessor.flushIfNecessary(HibernateAccessor.java:388)
at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:363)
at org.springframework.orm.hibernate3.HibernateTemplate.delete(HibernateTemplate.java:774)
at org.springframework.orm.hibernate3.HibernateTemplate.delete(HibernateTemplate.java:770)
at RootOfManyToManyTest.test_manyToManyRelationship(RootOfManyToManyTest.java:40)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:128)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
DEBUG: registering flush end
DEBUG: closing session
DEBUG: performing cleanup
DEBUG: releasing JDBC connection [ (open PreparedStatements: 0, globally: 0) (open ResultSets: 0, globally: 0)]
DEBUG: after transaction completion
DEBUG: transaction completed on session with on_close connection release mode; be sure to close the session to release JDBC resources!
DEBUG: after transaction completion
Question: Is this a bug in Hibernate? My understanding is that Hibernate's state becomes different to that of the DB, though all deletions are explicitly mentioned in DEBUG logging.