Monday Jun 14, 2010

The Typing of Criteria Queries

There were some important changes to the Criteria APIs in the JPA Final Release that probably haven't gotten enough attention. These changes are significant because they tighten up the type safety straight through from query definition to query execution.

These fall into two main areas:

  • Changes to the Criteria API itself.
  • Changes to the APIs for query execution.

In addition, there were a number of more minor changes, many related to improving the naming of interfaces and methods. The most obvious of these from a developer point of view is probably the renaming of the QueryBuilder interface to CriteriaBuilder.

One of the earlier gaps in the typing consistency of criteria queries was that the types of the query results were not carried through into query execution. The JPA Final Release tightens this up by enabling you to type CriteriaQuery objects according to their expected results.

To leverage this, when you create a CriteriaQuery object, you parameterize it according to the expected result type.

For example, a query to return a list of all customers would be defined as follows:

EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
Root<Customer> c = cq.from(Customer.class);
cq.select(c);

Now that the CriteriaQuery object is itself typed, we can carry this typing through to query execution:

TypedQuery<Customer> tq = em.createQuery(cq);
List<Customer> results = tq.getResultList();

The TypedQuery interface extends the Query interface. It can also be used for the execution of JPQL queries by means of new methods on the EntityManager interface:

<T> TypedQuery<T> createQuery(String qlString, Class<T> resultClass);

<T> TypedQuery<T> createNamedQuery(String name, Class<T> resultClass);

Thus, for example:

...
TypedQuery<Customer> tq = em.createQuery("SELECT c FROM Customer c", Customer.class);
List<Customer> allCustomers = tq.getResultList();

This approach, together with the use of the CriteriaBuilder construct method, also allows us to use a constructor in the select list of a criteria query. For example, suppose we have a CustomerDetails class, with a constructor that takes a customer's name, address, and balance owed. This time we'll illustrate with a query that returns a single result:

CriteriaBuilder cb = ...;
CriteriaQuery<CustomerDetails> cq = cb.createQuery(CustomerDetails.class);
Root<Customer> c = cq.from(Customer.class);
cq.where(cb.equal(c.get(Customer_.id), 101));
cq.select(cb.construct(
                CustomerDetails.class,
                c.get(Customer_.name),
                c.get(Customer_.address),
                c.get(Customer_.balanceOwed)));
TypedQuery<CustomerDetails> tq = em.createQuery(cq);
CustomerDetails result = tq.getSingleResult();

Suppose we don't want to use a constructor. How do we type CriteriaQuery objects that return multiple result items?

There are several options.

The one that I prefer is to define such queries in terms of Tuple types. A "tuple" query" can be created either by passing Tuple.class to the CriteriaBuilder createQuery method or by use of the CriteriaBuilder createTupleQuery method.

For example,

CriteriaBuilder cb = ...;
CriteriaQuery<Tuple> cq = cb.createQuery(Tuple.class);

or

CriteriaBuilder cb = ...;
CriteriaQuery<Tuple> cq = cb.createTupleQuery();

To specify the select clause of the query, you can either pass the selection items to the CriteriaBuilder tuple method, like this:

Root<Customer> c = cq.from(Customer.class);
cq.where(cb.equal(c.get(Customer_.id), 101));
cq.select(cb.tuple(
               c.get(Customer_.name),
               c.get(Customer_.address),
               c.get(Customer_.balanceOwed)));

or pass them to the CriteriaQuery multiselect method:

CriteriaBuilder cb = ...;
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Customer> c = cq.from(Customer.class);
cq.where(cb.equal(c.get(Customer_.id), 101));
cq.multiselect(
        c.get(Customer_.name),
        c.get(Customer_.address),
        c.get(Customer_.balanceOwed));

The multiselect method is a convenience method, and requires the persistence provider to aggregate multiple selection items in a way that correponds to the result type that you have specified for the query.

Having defined such a tuple query, there are several ways to extract the results when the query is executed.

By-position is probably the most obvious. Note that we must specify the expected result type of each tuple item.

TypedQuery<Tuple> tq = em.createQuery(cq);
Tuple result = tq.getSingleResult();
String name = result.get(0, String.class);
Address address = result.get(1, Address.class);
Double balanceOwed = result.get(2, Double.class);

Like SQL, the Criteria API allows you to assign "aliases" to selection items for use in tuple result extraction. To do this, we would modify our query as follows:

CriteriaBuilder cb = ...;
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Customer> c = cq.from(Customer.class);
cq.where(cb.equal(c.get(Customer_.id), 101));
cq.multiselect(
        c.get(Customer_.name).alias("customerName"),
        c.get(Customer_.address).alias("customerAddress"),
        c.get(Customer_.balanceOwed).alias("customerBalance"));

We can now extract the tuple items by name:

TypedQuery<Tuple> tq = em.createQuery(cq);
Tuple result = tq.getSingleResult();
String name = result.get("customerName", String.class);
Address address = result.get("customerAddress", Address.class);
Double balanceOwed = result.get("customerBalance", Double.class);

Finally, if you prefer results to be return as an array, that is possible too. To specify the result list, use either the CriteriaBuilder array method in conjunction with select, or use the multiselect method. The criteria query creation can either be specified without a type or with Object or Object[] as the result type. To avoid compiler warnings, I would write the query like this:

CriteriaBuilder cb = ...;
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Customer> c = cq.from(Customer.class);
cq.where(cb.equal(c.get(Customer_.id), 101));
cq.multiselect(
        c.get(Customer_.name),
        c.get(Customer_.address),
        c.get(Customer_.balanceOwed));
TypedQuery<Object[]> tq = em.createQuery(cq);
Object[] result = tq.getSingleResult();

About

Linda DeMichiel is the Specification Lead for Java EE 7 and for Java Persistence 2.1, and a member of the Java Platform, Enterprise Edition team at Oracle. She was formerly the Specification Lead for JPA 2.0 and for EJB 3.0.

Search

Top Tags
Archives
« June 2010 »
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today