Showing posts with label OpenJPA. Show all posts
Showing posts with label OpenJPA. Show all posts

Saturday, June 25, 2011

JPQL and joins

Have you ever asked yourself if JPQL (Java Persistence Query Language) queries written by you REALLY do what you want, or you just prepare them, commit to the code repository, and forget about them with a little help from pizza and beer ;) ...

Let's take a look at very simple example, we have Person entity:
@Entity
@Table(name = "PERSONS")
public class Person implements Serializable {

    private String firstName;
    private Long id;
    private Person spouse;

    @Column(name = "FIRST_NAME")
    public String getFirstName() {
        return firstName;
    }

    @Id
    public Long getId() {
        return id;
    }

    @OneToOne(optional = true)
    @JoinColumn(name = "SPOUSE_ID")
    public Person getSpouse() {
        return spouse;
    }
...
}
and we want to fetch all persons matching some criteria.

Suppose that PERSONS table has following entries:

ID SPOUSE_ID FIRST_NAME
1 NULL John
2 NULL Mary
3 4 Adam
4 3 Eva

We will build and execute very simple JPQL query:
Query query = entityManager.createQuery(
    "select person from Person person where person.spouse.firstName = 'Eva' or 1 = 1");
Assert.assertEquals("Result list is too small,", 4, query.getResultList().size());
Our JPQL query should return all persons from the database, because of the "1 = 1" expression in where clause. Let's try how it works with different JPA Providers.

EclipseLink (2.2.0) converts our JPQL query into following SQL query:
SELECT t1.ID, t1.FIRST_NAME, t1.SPOUSE_ID 
FROM PERSONS t0, PERSONS t1 
WHERE (((t0.FIRST_NAME = 'Eva') OR (1 = 1)) AND (t0.ID = t1.SPOUSE_ID))
which leads to assertion error:
java.lang.AssertionError: Result list too small, expected:<4> but was:<2>small 
Hibernate (3.4.0 GA) uses following SQL:
select person0_.id as id1_, person0_.FIRST_NAME as FIRST2_1_, person0_.SPOUSE_ID as SPOUSE3_1_ 
from PERSONS person0_, PERSONS person1_ 
where person0_.SPOUSE_ID=person1_.id and (person1_.FIRST_NAME='Eva' or 1=1)
which leads to the same assertion failure.

Only OpenJPA (2.1.0) uses SQL expected by us:
SELECT t0.id, t0.FIRST_NAME, t1.id, t1.FIRST_NAME 
FROM PERSONS t0 
    LEFT OUTER JOIN PERSONS t1 ON t0.SPOUSE_ID = t1.id 
WHERE (t1.FIRST_NAME = 'Eva' OR 1 = 1)
How can we force EclipseLink and Hibernate to use left join? We have to modify JPQL query in following way:
select person 
from Person person 
    left join person.spouse spouse 
where spouse.firstName = 'Eva' or 1 = 1
As you see I've added explicit left join and changed first where clause expression to use this join.
Now JPQL query returns all 4 records regardless of used JPA Provider.

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.