Hallo Forum,
obwohl ich hier noch nie was gepostet habe bin ich jetzt schon seit einigen Jahren Hibernate-User.
Im Moment bin ich dabei bei einem Projekt etwas "aufzuraeumen". Dabei moechte ich auch saubere Cascade-Definitionen machen, und bin dabei auf seltsame Probleme gestossen. Das ganze hab ich mal in ein kleines Beispiel gepackt:
Hier mal die Eckdaten:
Hibernate version:Hibernate Core 3.3.2 GA, Hibernate Annotation 3.4.0 GA, Hibernate EntityManager 3.4.0 GA, ej3b-persistance 1.0.2GA
Mappings / Pojo's:ParentPojo:
Code:
package org.tests.deleteOrphan;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.hibernate.annotations.Cascade;
@Entity
@Table(name="TST_DO_PARENT")
@SequenceGenerator(name="PK",sequenceName="SQ_TST_DO_PARENT_ID",allocationSize=1)
public class ParentPojo {
@Column(name = "id", nullable=false)
@Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="PK") // Firebird
//@Id @GeneratedValue // MySQL Style
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@OneToMany(fetch=FetchType.LAZY, cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
@JoinColumn(name="PARENT_ID")
@Cascade( { org.hibernate.annotations.CascadeType.DELETE_ORPHAN })
protected List<ClientPojo> clients;
@Column(name="S_VALUE")
protected String value;
public List<ClientPojo> getClients() {
return clients;
}
public void setClients(List<ClientPojo> clients) {
this.clients = clients;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
ClientPojo:
Code:
package org.tests.deleteOrphan;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.hibernate.annotations.ForeignKey;
/**
* @author marvin
*/
@Entity
@Table(name="TST_DO_CLIENT")
@SequenceGenerator(name="PK",sequenceName="SQ_TST_DO_CLIENT_ID",allocationSize=1)
public class ClientPojo extends SimpleDatabaseObject {
private static final long serialVersionUID = 20091222001L;
@Column(name = "id", nullable=false)
@Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="PK") // Firebird style
//@Id @GeneratedValue // MySQL Style
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@ManyToOne(fetch=FetchType.LAZY, optional=false)
@JoinColumn(name="PARENT_ID", nullable=false)
@ForeignKey(name = "FK_PA_ROLEGRANT_ASSIGNEE_ID")
private ParentPojo parent;
@Column(name="S_VALUE")
private String value;
public void setParent(ParentPojo parent) {
this.parent = parent;
}
public ParentPojo getParent() {
return parent;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
Also im grossen und ganzen nix grossartiges. Es ist halt JPA-Mapping mit dem entsprechenden "Hibernate DELETE_ORPHAN" hack der an diversen Stellen dokumentiert ist.
Hier mein Testcase:Code:
@Test
public void testPojoDeleteOrphaned() throws EntityNotFoundException {
ParentPojo pp = new ParentPojo();
pp.setClients(new ArrayList<ClientPojo>());
pp.setValue("Parent");
ClientPojo cp = new ClientPojo();
cp.setValue("Client");
cp.setParent(pp);
pp.getClients().add(cp);
parentPojoDAO.persist(pp);
parentPojoDAO.remove(pp);
}
Der Test baut eine einfache Struktur auf: ein Parent, das ein Child in der Liste hat. Und das Child auf das Parent verlinkt.
Beim persist() auf das Parent wird das Child auch sauber mit angelegt und gespeichert.
Allerdings beim remove() gibt es diesen Fehler:
Und hier mein "Fehler"-Stacktrace:Code:
Dec 22, 2009 4:58:42 PM org.hibernate.util.JDBCExceptionReporter logExceptions
WARNING: SQL Error: 335544347, SQLState: HY000
Dec 22, 2009 4:58:42 PM org.hibernate.util.JDBCExceptionReporter logExceptions
SEVERE: GDS Exception. 335544347. validation error for column PARENT_ID, value "*** null ***"
Dec 22, 2009 4:58:42 PM org.hibernate.event.def.AbstractFlushingEventListener performExecutions
SEVERE: Could not synchronize database state with session
org.hibernate.exception.GenericJDBCException: could not delete collection: [org.tests.deleteOrphan.ParentPojo.clients#16]
at org.hibernate.exception.SQLStateConverter.handledNonSpecificException(SQLStateConverter.java:126)
at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:114)
at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
at org.hibernate.persister.collection.AbstractCollectionPersister.remove(AbstractCollectionPersister.java:1093)
at org.hibernate.action.CollectionRemoveAction.execute(CollectionRemoveAction.java:107)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:279)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:263)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:169)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:50)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1028)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:366)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137)
at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:54)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:456)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:732)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:701)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:321)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:116)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:635)
at org.tests.deleteOrphan.ParentPojoDAO$$EnhancerByCGLIB$$55fa4a6.remove(<generated>)
at org.tests.PartnerTest.testPojoDeleteOrphaned(PartnerTest.java:81)
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 org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:82)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:240)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:180)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:46)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
Caused by: org.firebirdsql.jdbc.FBSQLException: GDS Exception. 335544347. validation error for column PARENT_ID, value "*** null ***"
at org.firebirdsql.jdbc.AbstractPreparedStatement.internalExecute(AbstractPreparedStatement.java:712)
at org.firebirdsql.jdbc.AbstractPreparedStatement.executeUpdate(AbstractPreparedStatement.java:172)
at org.hibernate.jdbc.NonBatchingBatcher.addToBatch(NonBatchingBatcher.java:46)
at org.hibernate.persister.collection.AbstractCollectionPersister.remove(AbstractCollectionPersister.java:1070)
... 47 more
Meine Findings:Ich hab schon probiert das Problem zu isolieren. Der Stacktrace wird bei folgendem Statement ausgeloest:
Code:
Hibernate: update TST_DO_CLIENT set PARENT_ID=null where PARENT_ID=?
Das verstoest natuerlich ganz klar gegen die annotations und das angelegte Datenbank-Constraint:
Code:
@ManyToOne(fetch=FetchType.LAZY, optional=false)
@JoinColumn(name="PARENT_ID", nullable=false)
Ich hab dann mal meinen Debugger bemueht um die Quelle fuer dieses SQL zu finden:
org.hibernate.persister.collection.OneToManyPersister#generateDeleteRowString()Das scheint also 'works as designed' zu sein. Entferne ich das Datenbank-Constraint, funktioniert der DELETE_ORPHAN. Es ist auch so, das nach diesem "set PARENT_ID=null" ein drop ausgefuehrt wird.
Ich hab jetzt in der OneToManyPersister-Klasse etwas gewuehlt, und bin noch auf die Methode isRowDeleteEnabled() gekommen. Diese ueberprueft, ob "keyIsNullable" gesetzt ist.
Und dies scheint die Ursache des Uebels zu sein:
keyIsNullable ist true, obwohl nullable=false bzw. optional=false in den Annotations gesetzt ist. Waere naemlich keyIsNullable=false, wuerde dieser seltsame Update nicht ausgefuehrt und der (spaetere) Drop wuerde mir den Eintrag sauber entfernen.
Zur Vollstaendigkeit die generierten Statements (ohne constraint auf der DB):
persist():
Code:
Hibernate: select gen_id( SQ_TST_DO_PARENT_ID, 1 ) from RDB$DATABASE
Hibernate: select gen_id( SQ_TST_DO_CLIENT_ID, 1 ) from RDB$DATABASE
Hibernate: insert into TST_DO_PARENT (S_VALUE, id) values (?, ?)
Hibernate: insert into TST_DO_CLIENT (PARENT_ID, S_VALUE, id) values (?, ?, ?)
Hibernate: update TST_DO_CLIENT set PARENT_ID=? where id=?
remove():
Code:
Hibernate: select parentpojo0_.id as id1_1_, parentpojo0_.S_VALUE as S2_1_1_, clients1_.PARENT_ID as PARENT3_3_, clients1_.id as id3_, clients1_.id as id0_0_, clients1_.PARENT_ID as PARENT3_0_0_, clients1_.S_VALUE as S2_0_0_ from TST_DO_PARENT parentpojo0_ left outer join TST_DO_CLIENT clients1_ on parentpojo0_.id=clients1_.PARENT_ID where parentpojo0_.id=?
Hibernate: update TST_DO_CLIENT set PARENT_ID=null where PARENT_ID=?
Hibernate: delete from TST_DO_CLIENT where id=?
Hibernate: delete from TST_DO_PARENT where id=?
Nun steh ich da und weiss nicht weiter....
Was kann ich machen? Die Constraints wuerde ich nur sehr ungerne entfernen...
Danke & Gruesse,
marvin