Hello all, I currently have a problem with orphanRemoval = true when removing a child entity from the collection of a parent if the parent was just persisted in the current persistence context. I'm using hibernate 3.5.4 final as the JPA2 impl.
Here are my entities:
Code:
@Entity
public class Author
{
@Id
@GeneratedValue
private Long id;
private String firstName;
private String lastName;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books = new ArrayList<Book>();
/* setters, getters, other code and fields*/
}
@Entity()
public class Book
{
@Id
@GeneratedValue
private Long id;
private String name;
@Column(unique = true)
private String isbn;
@ManyToOne(optional = false)
private Author author;
/* setters, getters, other code and fields*/
}
Here is my unit test that produce the problem:
Code:
Author john = new Author();
john.setFirstName("John");
john.setLastName("Doe");
Book book1 = new Book();
book1.setIsbn("1");
book1.setName("Book 1");
Book book2 = new Book();
book2.setIsbn("2");
book2.setName("Book 2");
john.addBook(book1);
john.addBook(book2);
this.authorDAO.persist(john);
Assert.assertEquals(2, john.getBooks().size());
Assert.assertEquals(book1, john.getBooks().get(0));
Assert.assertEquals(book2, john.getBooks().get(1));
Assert.assertEquals(john, book1.getAuthor());
Assert.assertEquals(john, book2.getAuthor());
// this.authorDAO.refresh(john); // refreshing the entity before removing the books will work since the ArrayList of books become a PersistentBag
book1.setAuthor(null);
book2.setAuthor(null);
john.getBooks().clear();
this.authorDAO.flush();
// exception thrown, see stack trace below
I get the following stack trace :
Code:
javax.persistence.PersistenceException: org.hibernate.PropertyValueException: not-null property references a null or transient value: com.acme.serviceplus.bookstore.entity.Book.author
at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1235)
at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1168)
at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1174)
at org.hibernate.ejb.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:794)
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.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:240)
at $Proxy29.flush(Unknown Source)
at com.acme.serviceplus.service.BaseEntityDAOImpl.flush(BaseEntityDAOImpl.java:297)
at com.acme.serviceplus.bookstore.TestDAO.testOneToMany_ManyToOne(TestDAO.java:600)
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.hibernate.PropertyValueException: not-null property references a null or transient value: com.acme.serviceplus.bookstore.entity.Book.author
at org.hibernate.engine.Nullability.checkNullability(Nullability.java:101)
at org.hibernate.event.def.DefaultFlushEntityEventListener.scheduleUpdate(DefaultFlushEntityEventListener.java:309)
at org.hibernate.event.def.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:155)
at org.hibernate.event.def.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:219)
at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:99)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:50)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1206)
at org.hibernate.ejb.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:791)
... 36 more
I understand what is causing the problem. It's the fact that my initial implementation of list<Book> is an ArrayList. Clearing the list will not make hibernate aware of the change since it is not a PersistentCollection implementation. If I do a refresh before removing the books from the list, it works since the list implementation change to a PersistentBag after the refresh. Using a find, a save or a merge will not change anything since the current version in the context is the latest one.
From hibernate's documentation, we have the following information (page 103 of the hibernate core reference pdf):
Quote:
Notice how the instance variable was initialized with an instance of HashSet. This is the best way
to initialize collection valued properties of newly instantiated (non-persistent) instances. When you
make the instance persistent, by calling persist() for example, Hibernate will actually replace
the HashSet with an instance of Hibernate's own implementation of Set. Be aware of the following
errors:
Code:
Cat cat = new DomesticCat();
Cat kitten = new DomesticCat();
....
Set kittens = new HashSet();
kittens.add(kitten);
cat.setKittens(kittens);
session.persist(cat);
kittens = cat.getKittens(); // Okay, kittens collection is a Set
(HashSet) cat.getKittens(); // Error!
which is not true in my case. Is this a bug, an outdated documentation, something wrong on my part? Is there a way to change the ArrayList reference for a PersistentBag instance after the call to persist so that I don't need to refresh (and thus reload the entity from the DB I lost the other change i might have done).
Any help would be appreciated,
Regards