I will use the same Employer - Employee - Benefit model as in my previous JPA posts (ex. JPA Demystified (episode 1) - @OneToMany and @ManyToOne mappings).
Let's take a look at the Employer entity:
@Entity
@Table(name = "EMPLOYERS")
public class Employer implements Serializable {
...
private List<Employee> employees = new ArrayList<Employee>();
...
public void addEmployee(Employee employee) {
employee.setEmployer(this);
employees.add(employee);
}
...
@OneToMany(mappedBy = "employer", cascade = CascadeType.PERSIST)
public List<Employee> getEmployees() {
return Collections.unmodifiableList(employees);
}
...
public void setEmployees(List<Employee> employees) {
this.employees = employees;
}
...
}
As you see addEmployee and getEmployees method usage should protect the employees list from the modifying outside the Employer.Let's use following test:
@Test
@Transactional(readOnly = false)
public void test03() throws Exception {
Employer employer = employerDAO.get(1L);
assertEquals(3, employer.getEmployees().size());
employerDAO.merge(employer);
}
Nothing special, we fetch the Employer from database, doesn't change anything, and try to merge the state into database. Simple, isn't it? Well, when you try to use Hibernate as JPA provider you'll encounter:java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableCollection.clear(Collections.java:1037)
at org.hibernate.type.CollectionType.replaceElements(CollectionType.java:501)
at org.hibernate.type.CollectionType.replace(CollectionType.java:574)
at org.hibernate.type.TypeFactory.replace(TypeFactory.java:505)
at org.hibernate.event.def.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:392)
at org.hibernate.event.def.DefaultMergeEventListener.entityIsPersistent(DefaultMergeEventListener.java:200)
at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:173)
at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:81)
at org.hibernate.impl.SessionImpl.fireMerge(SessionImpl.java:704)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:688)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:692)
at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:235)
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.merge(Unknown Source)
at [...].dao.jpa.DefaultEmployerDAO.merge(DefaultEmployerDAO.java:22)
at [...].dao.EmployerDAOTest.test03(EmployerDAOTest.java:76)
Good Lord, why it is trying to clear the collection!? Patrycja mentioned in her talk, that when you return the unmodifiable view of collection, Hibernate will treat the property as "dirty" (because both collections are different as objects) and try to persist the changes - which in this case means deleting all collection elements and persist same collection elements again!! ...OK, maybe OpenJPA will behave better ...
java.lang.IllegalAccessError: class org.apache.openjpa.util.java$util$Collections$UnmodifiableRandomAccessList$0$proxy cannot access its superclass java.util.Collections$UnmodifiableRandomAccessList
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
at java.lang.ClassLoader.defineClass(ClassLoader.java:466)
at serp.bytecode.BCClassLoader.findClass(BCClassLoader.java:50)
at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
at java.lang.ClassLoader.loadClass(ClassLoader.java:248)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:247)
at org.apache.openjpa.util.GeneratedClasses.loadBCClass(GeneratedClasses.java:67)
at org.apache.openjpa.util.ProxyManagerImpl.getFactoryProxyCollection(ProxyManagerImpl.java:363)
at org.apache.openjpa.util.ProxyManagerImpl.newCollectionProxy(ProxyManagerImpl.java:189)
at org.apache.openjpa.kernel.StateManagerImpl.newFieldProxy(StateManagerImpl.java:1824)
at org.apache.openjpa.kernel.StateManagerImpl.newProxy(StateManagerImpl.java:1790)
at org.apache.openjpa.jdbc.meta.strats.StoreCollectionFieldStrategy.load(StoreCollectionFieldStrategy.java:543)
at org.apache.openjpa.jdbc.meta.FieldMapping.load(FieldMapping.java:934)
at org.apache.openjpa.jdbc.kernel.JDBCStoreManager.load(JDBCStoreManager.java:691)
at org.apache.openjpa.kernel.DelegatingStoreManager.load(DelegatingStoreManager.java:117)
at org.apache.openjpa.kernel.ROPStoreManager.load(ROPStoreManager.java:78)
at org.apache.openjpa.kernel.StateManagerImpl.loadFields(StateManagerImpl.java:3047)
at org.apache.openjpa.kernel.StateManagerImpl.loadField(StateManagerImpl.java:3121)
at org.apache.openjpa.kernel.StateManagerImpl.beforeAccessField(StateManagerImpl.java:1606)
at org.apache.openjpa.kernel.StateManagerImpl.accessingField(StateManagerImpl.java:1591)
at org.apache.openjpa.enhance.RedefinitionHelper.accessingField(RedefinitionHelper.java:59)
at org.apache.openjpa.enhance.[...]$entities$domain$Employer$pcsubclass.getEmployees(Unknown Source)
at [...].dao.EmployerDAOTest.test03(EmployerDAOTest.java:75)
Nope :(. OpenJPA fails much sooner - it is unable to fetch the Employees list!Our last hope - EclipseLink ... Works :) it performs appropriate selects, to fetch the entity, then on merge verifies that there were no changes in it, and simply does nothing.
Of course all those 3 providers work perfectly when you simply return the collection, not the unmodifiable view.