The problem
When you map a one-to-many association using and Hibernate indexed persistent list with a join table (so we need to use many-to-many with unique="true" as one-to-many does not handle join table) any remove on this list leads to constraint violation during database synchronisation.
Class and mapping
Code:
package org.hibernate.test.collection.list;
import java.util.List;
import java.util.ArrayList;
/**
* {@inheritDoc}
*
* @author Steve Ebersole
*/
public class ListOwner {
private String name;
private ListOwner parent;
private List children = new ArrayList();
public ListOwner() {
}
public ListOwner(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ListOwner getParent() {
return parent;
}
public void setParent(ListOwner parent) {
this.parent = parent;
}
public List getChildren() {
return children;
}
public void setChildren(List children) {
this.children = children;
}
}
Code:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="org.hibernate.test.collection.list">
<class name="ListOwner">
<id name="name" column="NAME" type="string" />
<many-to-one name="parent" class="ListOwner" cascade="none" update="false" insert="false" />
<list name="children" cascade="all, delete-orphan" table="listowners">
<key column="PARENT" />
<list-index column="LIST_INDEX"/>
<many-to-many class="ListOwner" unique="true" />
</list>
</class>
</hibernate-mapping>
Unit TestCode:
package org.hibernate.test.collection.list;
import java.util.ArrayList;
import junit.framework.Test;
import org.hibernate.Session;
import org.hibernate.collection.PersistentList;
import org.hibernate.junit.functional.FunctionalTestCase;
import org.hibernate.junit.functional.FunctionalTestClassTestSuite;
/**
* Tests related to operations on a PersistentList
*
* @author Steve Ebersole
*/
public class PersistentListTest extends FunctionalTestCase {
public PersistentListTest(String name) {
super( name );
}
public String[] getMappings() {
return new String[] { "collection/list/Mappings.hbm.xml" };
}
public static Test suite() {
return new FunctionalTestClassTestSuite( PersistentListTest.class );
}
public void testRandomRemove() {
ListOwner parent = new ListOwner( "root" );
ListOwner child = new ListOwner( "c1" );
ListOwner child1 = new ListOwner( "c2" );
ListOwner child2 = new ListOwner( "c3" );
ListOwner child3 = new ListOwner( "c4" );
parent.getChildren().add( child );
parent.getChildren().add( child1 );
parent.getChildren().add( child2 );
parent.getChildren().add( child3 );
child.setParent( parent );
child1.setParent( parent );
child2.setParent( parent );
Session session = openSession();
session.beginTransaction();
session.save( parent );
session.flush();
parent.getChildren().remove( 1 );
session.update( parent );
session.flush();
session.getTransaction().commit();
session.close();
}
}
Console ouputCode:
Hibernate:
delete
from
listowners
where
PARENT=?
and LIST_INDEX=?
Hibernate:
update
listowners
set
elt=?
where
PARENT=?
and LIST_INDEX=?
14:08:41,470 WARN JDBCExceptionReporter:77 - SQL Error: 0, SQLState: null
14:08:41,470 ERROR JDBCExceptionReporter:78 - Batch entry 0 update listowners set elt=c3 where PARENT=root and LIST_INDEX=1 was aborted. Call getNextException to see the cause.
14:08:41,470 WARN JDBCExceptionReporter:77 - SQL Error: 0, SQLState: 23505
14:08:41,470 ERROR JDBCExceptionReporter:78 - ERROR: duplicate key violates unique constraint "listowners_elt_key"
14:08:41,477 ERROR AbstractFlushingEventListener:301 - Could not synchronize database state with session
org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:71)
at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:253)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:237)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:144)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:298)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000)
at org.hibernate.test.collection.list.PersistentListTest.testRandomRemove(PersistentListTest.java:93)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at junit.framework.TestCase.runTest(TestCase.java:154)
at org.hibernate.junit.functional.FunctionalTestCase.runTest(FunctionalTestCase.java:101)
at junit.framework.TestCase.runBare(TestCase.java:127)
at org.hibernate.junit.UnitTestCase.runBare(UnitTestCase.java:34)
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 org.hibernate.junit.functional.FunctionalTestClassTestSuite.runTest(FunctionalTestClassTestSuite.java:100)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.hibernate.junit.functional.FunctionalTestClassTestSuite.run(FunctionalTestClassTestSuite.java:69)
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)
Caused by: java.sql.BatchUpdateException: Batch entry 0 update listowners set elt=c3 where PARENT=root and LIST_INDEX=1 was aborted. Call getNextException to see the cause.
at org.postgresql.jdbc2.AbstractJdbc2Statement$BatchResultHandler.handleError(AbstractJdbc2Statement.java:2497)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1299)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:349)
at org.postgresql.jdbc2.AbstractJdbc2Statement.executeBatch(AbstractJdbc2Statement.java:2559)
at com.p6spy.engine.logging.P6LogPreparedStatement.executeBatch(P6LogPreparedStatement.java:329)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:48)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:246)
... 28 more
Sql Profiler output (for the remove instruction)
Prepared SQL: delete from listowners where PARENT=? and LIST_INDEX=? SQL Statement: delete from listowners where PARENT='root' and LIST_INDEX=3
Prepared SQL: update listowners set elt=? where PARENT=? and LIST_INDEX=? SQL Statement: update listowners set elt='c4' where PARENT='root' and LIST_INDEX=2
Conclusion
I think that the attribute unique="true" of the many-to-many element in the mapping is not taken into account during the synchronisation with the database.
Should I report to JIRA ?