Sunday, April 10, 2011

JPA and unmodifiable collections

Patrycja Węgrzynowicz (see Yon Labs / Yon Consulting) had very interesting talk on 33rd Degree Conference this year called "Patterns and Anti-Patterns in Hibernate". Inspired by this talk, I decided to verify JPA providers behavior regarding unmodifiable Collections.

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.

4 comments:

  1. It also works in Hibernate if you annotate the field instead of the method.

    ReplyDelete
  2. @Guillaume - Good catch! in this case Hibernate doesn't call the getter, and thus has access to the original Collection, instead of unmodifiable view.

    The question is if we prefer field or property access for some reason ... ;)

    ReplyDelete
  3. Does anyone have a solution to this? Specifically with OpenJPA it seems to be related to the Proxy rather than the unmodifiable collection itself. i have a similar situation where my collection is not unmodifiable and I get same stack track with following error:

    java.lang.IllegalAccessError: class org.apache.openjpa.util.org$apache$xerces$jaxp$datatype$XMLGregorianCalendarImpl$0$proxy cannot access its superclass org.apache.xerces.jaxp.datatype.XMLGregorianCalendarImpl
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:465)
    at serp.bytecode.BCClassLoader.findClass(BCClassLoader.java:50)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
    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.getFactoryProxyBean(ProxyManagerImpl.java:452)
    at org.apache.openjpa.util.ProxyManagerImpl.newCustomProxy(ProxyManagerImpl.java:303)
    at org.apache.openjpa.kernel.SingleFieldManager.proxy(SingleFieldManager.java:133)
    at org.apache.openjpa.kernel.StateManagerImpl.proxyFields(StateManagerImpl.java:2921)
    at org.apache.openjpa.kernel.PNonTransState.initialize(PNonTransState.java:45)
    at org.apache.openjpa.kernel.StateManagerImpl.setPCState(StateManagerImpl.java:294)
    at org.apache.openjpa.kernel.StateManagerImpl.commit(StateManagerImpl.java:1135)

    ReplyDelete
  4. Here is a test case that isolates the issue with openjpa 2.2.0:
    GregorianCalendar gc = new GregorianCalendar();
    gc.setTimeInMillis(new Date().getTime());
    XMLGregorianCalendar cc = DatatypeFactory.newInstance().newXMLGregorianCalendar(gc);
    Properties jpaConf = new Properties();
    jpaConf.put("openjpa.Id", "jpastor");
    OpenJPAEntityManagerFactorySPI emf = (OpenJPAEntityManagerFactorySPI) Persistence.createEntityManagerFactory(jpaConf.getProperty("openjpa.Id"), jpaConf);
    emf.getConfiguration().getProxyManagerInstance().newCustomProxy(cc, false);

    ReplyDelete