Syntax coloring

quarta-feira, 12 de maio de 2010

JPA 2 Criteria

One of the most expected features in JPA 2 is a Criteria API. Something that Hibernate has had for ages, but a notable absence in JPA 1.

Even better, JPA 2 criteria is compiled (generated from the source code) and type safe. So, for example, whenever an attribute is removed or changed on the entity, the queries stop compiling immediately, instead of having to wait until the application is running to detect errors. Neat, huh?

However, there's a problem. The way it is, queries are unusable. Well, usable, but very, VERY hard to code, read and maintain. Not for the JSR 317 expert group, of course, but everyone I've asked, has the same opinion as me.

Take a look (example extracted from this link, with little changes):
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Person> criteria = builder.createQuery(
    Person.class);
Root<Person> personRoot = criteria.from(Person.class);
criteria.select(personRoot);
ParameterExpression<String> eyeColorParam = builder.
    parameter(String.class);
criteria.where(builder.equal(personRoot.get(
    Person_.eyeColor), eyeColorParam));
TypedQuery<Person> query = em.createQuery(criteria);
query.setParameter(eyeColorParam, "brown");
List<Person> people = query.getResultList();

Is this example anything close to 'easy'? The very same query in JPQL would be:
String jpql = 
    "select p from Person p where p.eyeColor = :eyeColor";
TypedQuery<Person> query =
    em.createQuery(jpql, Person.class);
query.setParameter("eyeColor", "brown");
List<Person> people = query.getResultList();

To make things a bit worse, the Query object returned from the em.createQuery(criteria) never has parameters already set. And parameters are only used when a ParameterExpression is created. Otherwise, the values are passed as literals (so, subject to things like SQL injection). Yikes! There's absolutely no reason for this. Even the plain old Hibernate criteria already converted given literals to bind parameters...

C'mon, how could an expert group do such terrible decisions, impacting the lives of thousands Java programmers out there having to live with this abomination?

Thanks God, there is a very nice solution. It's Querydsl. It has the main advantage of JPA 2 criteria: being type safe (an annotation processor is used to generate a meta model which is used on queries), uses fluent interfaces (code is very readable) and generates queries with bind parameters on all expressions. The Querydsl metamodel has a Q prefix, for example, QEntity, instead of JPA's Entity_. So, let's take a look on the same previous example in Querydsl:
JPAQuery query = new JPAQuery(em);
QPerson person = QPerson.person;
List<Person> people = query.from(person)
  .where(person.eyeColor.eq("brown"))
  .list(person);

Now, that's readable!!! Also, in the project I'm working, I've also extended the query (actually, extending AbstractJPAQuery) and added other useful methods, like page(currentPage, pageSize). Such things can't be done in JPA because all objects (Query, CriteriaQuery, CriteriaBuilder) are interfaces given by the JPA provider, and can't be easily extended.

So, here is my tip to anyone thinking about using a Criteria API: Give Querydsl a try! By the way, did I mention that it can also be used with JDO, Lucene, JDBC and even plain collections?

10 comentários:

Timo Westkämper disse...

Thanks for this post! It is nice to have Querydsl users describing the benefits of Querydsl.

Anônimo disse...

Thanks for this post, what kind of dependencies is needed to use querydsl? additional jar? there any kind of known incompatibility with the current jpa 2.0 version?
can we user this api safety?

Luis Fernando disse...

Currently, Querydsl supports only Hibernate as JPA provider, which was not an issue for me, as the project I'm working was already forcing Hibernate.
However, independent JPA 2 support is ongoing, and should be ready soon.

All conditions I've tried to use so far were supported very well by Querydsl. I did had an issue with instanceOf test, which was only working when no @DiscriminatorValue was specified, but as I'm writing this comment, Timo has already answered that the solution is there. Check it here: http://source.mysema.com/forum/mvnforum/viewthread_thread,71

I'm confident, however, that if we encounter issues, we will have a very helpful and qualified support... Every single contact / request I had have been promptly replied and resolved.

Anônimo disse...

In this link:
http://blogs.sun.com/carolmcdonald/entry/owasp_top_10_number_2
Explicitly says:
"The JPA 2.0 criteria API providies a typesafe object-based Query API based on a metamodel of the Entity classes, rather than a string-based Query API. This allows you to develop queries that a Java compiler can verify for correctness at compile time. Below is an example using the Criteria API for the same query as before"

So, I don't understand why do you say that JPA 2.0 is not typesafe... I guess you're wrong

Luis Fernando disse...

If you read the post again, you'll notice that I said that JPA 2 Criteria IS type safe, but way more complex than it needed to be.

Ralph disse...

Thanks for the post. It's the first time I heard about Querydsl and
I have to say it's a very useful library.

One thing I don't understand is the use of ParameterExpression.
Why use ParameterExpression and not simply insert the value
in the where as a literal.

Root personRoot = criteria.from(Person.class);
criteria.select(personRoot);
criteria.where(builder.equal(personRoot.get(Person_.eyeColor), "brown"));

The code behaves exactly the same if I use this code or the code from your example with
ParameterExpression. The program creates a PreparedStatement and sets the parameter.
If I'm understand this correct then there should be no problem with sql injection.
I used http://code.google.com/p/log4jdbc/ for inspecting and logging the sql statements.

Luis Fernando disse...

What is your JPA provider?
I tested with Hibernate, and it just converted the argument into a literal expression, and used it plain is the jpql string....

Ralph disse...

I use Hibernate 3.5.2

Ralph disse...

To further investigate this issue I created a simple maven project (http://www.ralscha.ch/simplejpa-0.0.1-src.zip). The project contains a unit test that executes the same query with JPQL, JPA Criteria API with and without ParameterExpression and Querydsl. The output from log4jdbc shows that every query is executed with a PreparedStatement.

Even if JPA gets it right, the code with Querydsl is much easier to write and to understand.

Luis Fernando disse...

@Ralph
Sorry for the delay, but just now I could review this...
The point is that the spec does not requires using bind parameters when queries are not explicitly set with parameters.
Cheers for Hibernate for generating all sql queries with bind parameters. However, I've ran your example using EclipseLink as JPA provider and it does not generate bind parameters... For example, these queries are generated:
SELECT ID, EYECOLOR FROM PERSON WHERE (EYECOLOR = 'brown')

The only vendor-safe way to work with JPA criterias (and queries) is using ParameterExpressions...