We did. We received quite a few responses to the criteria API—many of you pleased that we had addressed this area—others suggesting that we should look at providing a typesafe API rather than a string-based API.
Gavin King immediately took up the challenge. Gavin's proposal to the group, described here in his blog, was a typesafe API based on a metamodel of the managed classes in the persistence unit.
The expert group was in agreement that the metamodel would be valuable to have, independent of its use to support a criteria API. (Actually, we had discussed the usefulness of a metamodel API back in the JPA 1.0 days, although understandably it was low on our priority list back then). The group was in less agreement on the form of the criteria API itself. After many discussions on the pluses and minuses of a metamodel-based API compared to a string-based API, we decided to adopt the typesafe API, but to provide developers the option of using a string-based approach if they prefer. More on that below—but first, some background on the metamodel API and how to use the typesafe API.
Metamodel API
From a query point of view, the metamodel captures what the spec refers to in JPQL terms as the "abstract schema" of the persistence unit — that is, the logical view over the persistent state and relationships of the managed classes of the persistence unit (entities, mapped superclasses, embeddables).
The metamodel can be browsed dynamically, using the new javax.persistence.metamodel interfaces. These interfaces can be used directly to write typesafe queries with the new API or, more conveniently, metamodel classes that capture the metamodel of the persistence unit can be generated at the time the corresponding managed classes are compiled. To facilitate this, we plan to release an annotation processor to be run in conjunction with javac to generate these classes.
To illustrate what such metamodel classes look like, consider the following set of interrelated entity classes that I used in my earlier post :
@Entity public class
Customer {
@Id int custId;
String name;
...
@OneToMany(mappedBy="customer") Set<Order>
orders;
...
}
@Entity public class Order {
@Id int orderId;
...
@ManyToOne Customer customer;
@OneToMany(mappedBy="order") Set<LineItem>
items;
...
}
@Entity public class LineItem {
@Id int id;
@ManyToOne Order order;
@ManyToOne Product product;
...
}
@Entity public class Product {
@Id int productId;
String name;
String productType;
...
}
The corresponding metamodel classes look like this:
import
javax.persistence.metamodel.*;
@TypesafeMetamodel
public class Customer_ {
public static volatile
Attribute<Customer, Integer> custId;
public static volatile
Attribute<Customer, String> name;
public static volatile
Set<Customer, Order> orders;
...
}
import javax.persistence.metamodel.*;
@TypesafeMetamodel
public class Order_ {
public static volatile
Attribute<Order, Integer> orderId;
public static volatile
Attribute<Order, Customer> customer;
public static volatile Set<Order,
LineItem> items;
...
}
import javax.persistence.metamodel.*;
@TypesafeMetamodel
public class LineItem_ {
public static volatile
Attribute<LineItem, Integer> id;
public static volatile
Attribute<LineItem, Order> order;
public static volatile
Attribute<LineItem, Product> product;
...
}
import javax.persistence.metamodel.*;
@TypesafeMetamodel
public class Product_ {
public static volatile
Attribute<Product, Integer> productId;
public static volatile
Attribute<Product, String> name;
public static volatile
Attribute<Product, String> productType;
...
}The advantage of using generated metamodel classes, rather than using the javax.persistence.metamodel API directly, is that it makes the writing of typesafe queries very easy, as I'll describe in the sections below. The API is otherwise much in the spirit of that in the Public Draft. It improves over this earlier API in that it provides a more "semantic" factorization of interfaces as well as the ability to browse the constitute parts of a criteria query object.
For the sake of providing a more direct comparison with our earlier version, I'm going to take what I wrote about our earlier version of the criteria API and translate sample queries to the new criteria API.
Writing Queries with the New Criteria API
The core interfaces of the criteria API are the CriteriaQuery interface, which provides the methods that construct the constituent parts of a criteria query (select clauses, where clauses, etc.) and the QueryBuilder interface—which serves as the factory for criteria query objects and for the operations that are used to construct expressions and predicates.
The QueryBuilder interface is obtained from the EntityManager or EntityManagerFactory. Using the QueryBuilder object, you create an "empty" CriteriaQuery object:
EntityManager em =
... ;
QueryBuilder qb = em.getQueryBuilder();
CriteriaQuery q = qb.create();As before, the specification of query roots is the first step in constructing a query. These correspond to the range variables defined in a SQL FROM clause, and specify the domain objects on which the query is based. The from method of the CriteriaQuery interface is used to add a query root to a CriteriaQuery instance. The from method is additive. The addition of further query roots, like additional range variables in JPQL, creates a cartesian product with the existing roots.
CriteriaQuery q =
qb.create();
Root<Customer> customer = q.from(Customer.class);Query roots are instances of the Root interface. Notice that the Root variable customer is parameterized by the type of the entity that it represents.
Given one or more query roots, the query domain can be modified using join operations. The argument to the join method is a metamodel attribute which captures both the source of the join and the join target. For an entity relationship or for an element collection this attribute is of type javax.persistence.metamodel.Collection, javax.persistence.metamodel.Set, javax.persistence.metamodel.List, or javax.persistence.metamodel.Map. When performing a join to an embeddable, it is of type javax.persistence.metamodel.Attribute.
For example, to modify the above query to operate over customers and their orders, we would add:
Join<Customer,Order>
order = customer.join(Customer_.orders);The argument to the join method here is of type Set<Customer, Order>. The typesafe parameterization of the join method insures that it is not possible to construct an invalid join from the customer root.
When writing queries using the typesafe API in this way, in general the only places that you will use the attributes of metamodel classes is to specify navigation (joins in the from-clause, and path navigation in the other clauses of the query). From a typing point of view, these objects play the role of Java member literals (if only the Java language had member literals!) in providing type information.
The SELECT and WHERE Clauses
Every criteria query needs a select clause. Every non-trivial query has a where clause.
Continuing our earlier example, a query to return all customers ordering products of type 'printer' would look something like this with JPQL:
SELECT c.name
FROM Customer c JOIN c.orders o JOIN o.items i
WHERE i.product.productType = 'printer' The criteria API equivalent is the following:
QueryBuilder qb =
em.getQueryBuilder();
CriteriaQuery q = qb.create();
Root<Customer> customer = q.from(Customer.class);
Join<Order, LineItem> item =
customer.join(Customer_.orders).join(Order_.items);
q.where(qb.equals(item.get(Item_.product).get(Product_.productType),
"printer"))
.select(customer.get(Customer_.name));
There are a couple of things to note:
The argument to the where method is a boolean expression (of type Expression<Boolean>). As mentioned above, the QueryBuilder interface serves as a factory for such expressions.
In this particular example, the first argument to the equals method is a Path instance. The get method is used to traverse a path. Like the join method, it relies on use of a metamodel attribute to achieve typesafe navigation. In this case, the path is derived from invoking get(Item_.product) on the item object and is then further chained to navigate to the productType attribute.
The where method returns the modified CriteriaQuery instance. If the where method is applied again, the restrictions are replaced. If the where method is invoked without an argument, they are removed.
The select method also returns the modified CriteriaQuery instance. If the select method is applied again, the select list is replaced. If the select method is invoked without an argument, they are removed (and the query will not be executable).
Coverage
As with the version presented in the Public Draft, the new criteria API is intended to provide support for all the functionality of JPQL.
In the following sections we examine how we might rewrite some of the JPQL examples in my earlier blog. I'm just going to show the JPQL queries here and assume the existence of the obvious managed classes and their corresponding metamodel classes.
Navigation
JPQL:
SELECT DISTINCT
p.billedTo
FROM Employee e JOIN e.contactInfo c JOIN c.phones p
WHERE e.contactInfo.address.zipcode = '95054'
AND p.phonetype = PhoneType.OFFICEUsing the criteria API, this query can be written as follows.
CriteriaQuery q =
qb.create();
Root<Employee> e = q.from(Employee.class);
Join<Employee, ContactInfo> c =
e.join(Employee_.contactInfo);
Join<ContactInfo, Phone> p = c.join(ContactInfo_.phones);
q.where(qb.equal(c.get(ContactInfo_.address).get(Address_.zipcode),
"95054"),
qb.equal(p.get(Phone_.phonetype), PhoneType.OFFICE))
.select(p.get(Phone_.billedTo)).distinct(true); Maps
JPQL:
SELECT p
FROM PictureCategory c JOIN c.photos p
WHERE c.name = 'birds' AND KEY(p) LIKE '%egret%'
Using the criteria API:
CriteriaQuery q = qb.create();
Root<PictureCategory> c = q.from(PictureCategory.class);
MapJoin<PictureCategory, String, Photo> p =
c.join(PictureCategory_.photos);
q.where(qb.equal(c.get(PictureCategory_.name), "birds"),
qb.like(p.key(), "%egret%"))
.select(p);
As in the JPQL query, a variable referring to a map type corresponds to the map value. Notice that the MapJoin instance is parameterized on the type of the source entity, and the types of the map key and map value.
JPQL:
SELECT
v.location.street, ENTRY(i)
FROM VideoStore v JOIN v.videoInventory i
WHERE v.location.zipcode = '95054'
AND KEY(i).director = 'Hitchcock' AND VALUE(i)
> 0
Using the criteria API:
CriteriaQuery q =
qb.create();
Root<VideoStore> v = q.from(VideoStore.class);
MapJoin<VideoStore, Movie, Integer> i =
v.join(VideoStore_.videoInventory);
q.where(qb.equal(v.get(VideoStore_.location).get(Address_.zipcode),
"95054"),
qb.equal(i.key().get(Movie_.director), "Hitchcock"),
qb.gt(i, 0))
.select(v.get(VideoStore_.location).get(Address_.street),
i.entry());Ordered Lists
JPQL:
SELECT e
FROM Employee e JOIN e.dept d
WHERE d.name='Marketing' AND INDEX(e) < 5Using the criteria API:
CriteriaQuery q =
qb.create();
Root<Department> d = q.from(Department.class);
ListJoin<Department, Employee> e =
d.join(Department_.members);
q.where(qb.equal(d.get(Department_.name), "Marketing),
qb.lt(e.index, 5))
.select(e); Notice that the index operation here is properly typed as applying to a ListJoin instance. Instead of joining from Employee to Department, as in the JPQL query, we therefore join from Department to Employee. From a database point of view, of course, the two are identical.
Restricted Polymorphism
JPQL:
SELECT e
FROM Employee e JOIN e.dept d
WHERE d.name = 'Marketing' AND TYPE(e) IN (PartTimeEmployee, Contractor)In the criteria query, entity class objects are used to specify entity type arguments to the in method. The in method supports use of a builder pattern, which this query illustrates:
CriteriaQuery
q = qb.create();
Root<Employee> e = q.from(Employee.class);
Join<Employee, Department> d = e.join(Employee_.dept);
q.where(qb.equal(d.get(Department_.name), "Marketing"),
qb.in(e.type()).value(PartTimeEmployee.class).value(Contractor.class))
.select(e);
Case Expressions
This query shows the use of the general form of case expressions.
SELECT c, CASE WHEN
c.annualSpending > 10000 THEN 'Premier'
WHEN c.annualSpending > 5000 THEN 'Gold'
WHEN c.annualSpending > 2000 THEN 'Silver'
ELSE 'Bronze'
END
FROM Customer c
CriteriaQuery q = qb.create();
Root<Customer> c = q.from(Customer.class);
Expression<Integer> annualSpending =
c.get(Customer_.annualSpending);
q.select(c, qb.selectCase()
.when(qb.gt(annualSpending, 10000), "Premier")
.when(qb.gt(annualSpending, 10000), "Gold")
.when(qb.gt(annualSpending, 10000), "Silver")
.otherwise("Bronze")); This one uses a NULLIF expression:
SELECT
AVG(NULLIF(e.salary, -99999))
FROM Employee e
CriteriaQuery q = qb.create();
Root<Employee> e = q.from(Employee.class);
q.select(qb.avg(qb.nullif(e.get(Employee_.salary), -99999)));
Subqueries
The following JPQL query returns those customers whose unpaid balance is less than half of the average. This makes use of a non-correlated subquery.
SELECT goodCustomer
FROM Customer goodCustomer
WHERE goodCustomer.balanceOwed < (
SELECT AVG(c.balanceOwed)/2.0 FROM Customer c)Using the criteria API, the corresponding query can be written as shown below. There are Root objects for both the subquery and the containing query. The subquery method of the CriteriaQuery interface creates a Subquery instance. This instance is typed according to its expected result type.
CriteriaQuery q =
qb.create();
Root<Customer> goodCustomer = q.from(Customer.class);
Subquery<Double> sq = q.subquery(Double.class);
Root<Customer> c = sq.from(Customer.class);
q.where(qb.lt(goodCustomer.get(Customer_.balanceOwed),
sq.select(qb.toDouble(qb.quot(qb.avg(c.get(Customer_.balanceOwed)),
2.0)))))
.select(goodCustomer);The following JPQL query selects those employees that make more than all of the managers in their department. This has a correlated subquery, as it uses the emp range variable of the containing query.
SELECT emp
FROM Employee emp
WHERE emp.salary > ALL (
SELECT m.salary
FROM Manager m
WHERE m.department = emp.department) This is the criteria API equivalent.
CriteriaQuery q =
qb.create();
Root<Employee> emp = q.from(Employee.class);
Subquery<BigDecimal> sq = q.subquery(BigDecimal.class);
Root<Manager> m = sq.addRoot(Manager.class);
sq.select(m.get(Manager_.salary))
.where(qb.equal(m.get(Manager_.dept),
emp.get(Employee_.dept)));
q.select(emp)
.where(qb.gt(emp.get(Employee_.salary), qb.all(sq)));String-based Use of the Criteria API
In addition to the typesafe API, the criteria API also supports string-based navigation, as did the API of the Public Draft. As I mentioned, the Expert Group was somewhat divided over the need for string-based support, but many members felt strongly that some developers would prefer it.
As you might have already inferred from the examples above, the main difference in the string-based usage of the criteria API is that strings are used to specify joins and path navigation.
The following queries, taken from those above, show how a criteria query would be written to use the methods that take strings rather than metamodel types as arguments:
CriteriaQuery
q = qb.create();
Root<Employee> e = q.from(Employee.class);
Join<Employee, ContactInfo> c =
e.join("contactInfo");
Join<ContactInfo, Phone> p = c.join("phones");
q.where(qb.equal(c.get("address").get("zipcode"), "95054"),
qb.equal(p.get("phonetype"), PhoneType.OFFICE))
.select(p.get("billedTo")).distinct(true);
CriteriaQuery q = qb.create();
Root<VideoStore> v = q.from(VideoStore.class);
MapJoin<VideoStore, Movie, Integer> i =
v.join("videoInventory");
q.where(qb.equal(v.get("location").get("zipcode"), "95054"),
qb.equal(i.key().get("title"), "Vertigo"),
qb.gt(i, 0))
.select( v.get("location").get("street"));
If you prefer to use raw types rather than the parameterized types shown above, you can do so, however this will result in compiler warnings unless @SuppressWarnings("unchecked") is used:
CriteriaQuery q =
qb.create();
Root e = q.from(Employee.class);
Join c = e.join("contactInfo");
Join p = c.join("phones");
q.where(qb.equal(c.get("address").get("zipcode"), "95054"),
qb.equal(p.get("phonetype"), PhoneType.OFFICE))
.select(p.get("billedTo")).distinct(true);Send Us Your Feedback
This draft of the spec contains some important changes, particularly with regard to the Criteria API, and can be downloaded here. As always, we welcome your feedback and suggestions. If you would like to send feedback to the expert group, you can reach us at jsr
thanks!