Criteria queries are constructed in terms of query definition objects. As with JPQL static and dynamic queries, criteria query definition objects are passed to the EntityManager createQuery method to create Query objects—and then executed using the methods of the Query API.
Loosely speaking, a QueryDefinition object can be thought of as a set of nodes corresponding to the semantic constructs of the query:
- domain objects, which correspond to the range variables and other identification variables of the JPQL FROM clause
- where clause predicates, which comprise one or more conditional expression objects
- select clauses, which comprise one or more "select item" objects
- order-by and group-by items
- subqueries
- and so on...
Constructing a Query Definition
The QueryBuilder interface is the factory for QueryDefinition objects. A QueryBuilder instance is obtained from either the EntityManager or the EntityManagerFactory. For example,
EntityManager em = ... ;
QueryBuilder queryBuilder = em.getQueryBuilder();
QueryDefinition qdef = queryBuilder.createQueryDefinition();
Using the query builder, you can create either an "empty" QueryDefinition object or a QueryDefinition object with a root entity class. The specification of query roots is the first step in constructing a query definition. Query roots correspond to the range variables of SQL. They specify the domain objects on which the query is based and which are not reachable by navigation or join. Query root are instances of the DomainObject interface. In the simple case a query definition has a single root, so the QueryBuilder interface has a convenience method to support this.
DomainObject customer = queryBuilder.createQueryDefinition(Customer.class);
This method returns the DomainObject instance corresponding to the root. It is semantically equivalent to the following in which the addRoot method is used to add a query root to an empty QueryDefinition object:
QueryDefinition qdef = queryBuilder.createQueryDefinition();
DomainObject customer = qdef.addRoot(Customer.class);
The DomainObject interface extends the QueryDefinition interface, and QueryDefinition operations applied to domain objects operate on the query definition as a whole. Thus, for example, if you invoke the where method on a domain object, the query definition is modified to include the specified restriction predicate.
A query with a single root entity is assumed to select entities of that type, unless the select method of the QueryDefinition interface is used to specify otherwise. Thus, the DomainObject customer in the query above represents a complete query definition. We can pass it to the createQuery method and execute the resulting query, causing all instances of the Customer class to be returned:
Query q = em.createQuery(customer);
List myCustomers = q.getResultList();
The addition of further query roots, like additional range variables in JPQL, has the effect of inducing a cartesian product.
Given one or more query roots, a query domain can be further refined by the addition of other domain objects through join operations. The argument to the join method is the name of the attribute in the referencing class. The result is the added DomainObject instance. This, like the domain objects that are roots, again references the underlying query definition object.
For example, to modify the above query definition to operate over customers and their orders, we would add:
DomainObject order = customer.join("orders");
When executed, this query returns all customers that have orders. Note further that because the variables order and customer are referencing the same underlying query definition, the following calls to the EntityManager createQuery method are semantically equivalent:
em.createQuery(customer);
em.createQuery(order);
Defining the SELECT and WHERE Clauses
Assume we have the following classes:
@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;
...
}
Using JPQL, a query to return all customers ordering products of type 'printer' would look something like this:
SELECT c.name
FROM Customer c JOIN c.orders o JOIN o.items i
WHERE i.product.productType = 'printer'
To construct a semantically equivalent query using the Criteria API, we proceed as follows:
1) Create a query definition object. This query is rooted in the Customer class, so we again write:
DomainObject customer = queryBuilder.createQueryDefinition(Customer.class);
2) Join Customer to Order and Order to LineItem. Since the result of the join method is also a DomainObject, we chain the method invocations:
DomainObject item = customer.join("orders").join("items");
3) Define the select list of the query and the restrictions over the results. These modify the underlying query definition object, and can thus be defined in either order. Let's assume we're defining the restrictions first:
customer.where(item.get("productType").equal("printer"));
The argument to the where method is a conditional predicate. In this example, the arguments to the condition are a PathExpression instance (derived from invoking get("productType") on the item DomainObject) and the string "printer". The where method returns this, so it supports method chaining.
The select method of the QueryDefinition interface modifies the query definition to specify the query result. It also returns this:
customer.select(customer.get("name"));
Putting it all together, and using method chaining we have:
DomainObject customer = queryBuilder.createQueryDefinition(Customer.class);
DomainObject item = customer.join("orders").join("items");
customer.where(item.get("productType").equal("printer"))
.select(customer.get("name"));
While this example of course just illustrates the basics, the Criteria API provides support for all the functionality of JPQL.
In the following sections we examine how we might rewrite the JPQL examples in part I of this blog. I'm just going to reproduce the JPQL queries here. To see the entity classes, please refer to part I.
Navigation
Using JPQL:
SELECT DISTINCT p.billedTo
FROM Employee e JOIN e.contactInfo.phones p
WHERE e.contactInfo.address.zipcode = '95054'
AND p.phonetype = PhoneType.OFFICE
Or, equivalently:
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.OFFICE
This query navigates through the contactInfo embeddable in the FROM clause and again in the WHERE clause. Because navigation in the FROM clause has the semantics of inner join, the Criteria API requires the use of the join method to construct the query domain. The where method uses a compound predicate.
DomainObject e = queryBuilder.createQueryDefinition(Employee.class);
DomainObject p = e.join("contactInfo").join("phones");
e.where(e.get("contactInfo").get("address").get("zipcode").equal("95054")
.and(p.get("phonetype").equal(PhoneType.OFFICE)))
.selectDistinct(p.get("billedTo"));
Maps
Here's the first query over maps:
SELECT p
FROM PictureCategory c JOIN c.photos p
WHERE c.name = 'birds' AND KEY(p) LIKE '%egret%'
DomainObject c = queryBuilder.createQueryDefinition(PictureCategory.class);
DomainObject p = c.join("photos");
p.where(c.get("name").equal("birds").and(p.key().like("%egret%")))
.select(p);
Note that, as in the JPQL query, a variable referring to a map type corresponds to the map value.
Here are the next two queries:
SELECT v.location.street
FROM VideoStore v JOIN v.videoInventory i
WHERE v.location.zipcode = '95054' AND KEY(i).title = 'Vertigo' AND VALUE(i) > 0
and
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, these queries are written as follows:
DomainObject v = queryBuilder.createQueryDefinition(VideoStore.class);
DomainObject i = v.join("videoInventory");
v.where(v.get("location").get("zipcode").equal("95054")
.and(i.key().get("title").equal("Vertigo"))
.and(i.value().greaterThan(0)))
.select(v.get("location").get("street"));
and
DomainObject v = queryBuilder.createQueryDefinition(VideoStore.class);
DomainObject i = v.join("videoInventory");
v.where(v.get("location").get("zipcode").equal("95054")
.and(i.key().get("director").equal("Hitchcock"))
.and(i.value().greaterThan(0)))
.select(v.get("location").get("street"), i.entry());
Ordered Lists
Here's the query over ordered lists:
SELECT e
FROM Employee e JOIN e.dept d
WHERE d.name='Marketing' AND INDEX(e) < 5
DomainObject e = queryBuilder.createQueryDefinition(Employee.class);
DomainObject d = e.join("dept");
e.where(d.get("name").equal("Marketing").and(e.index().lessThan(5)));
Non-polymorphic Queries
The following two examples illustrate non-polymorphic queries:
Example 1:
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:
DomainObject e = queryBuilder.createQueryDefinition(Employee.class);
DomainObject d = e.join("dept");
e.where(d.get("name").equal("Marketing")
.and(e.type().in(PartTimeEmployee.class, Contractor.class)));
Example 2:
SELECT e
FROM Employee e
WHERE TYPE(e) IN :empTypes
In the criteria query below, the param method of the QueryDefinition interface defines the parameter name. As with JPQL, this parameter must be bound to a collection of entity class objects for the query execution. Note that in a criteria query the parameter name is not prefixed with a colon (:).
DomainObject e = queryBuilder.createQueryDefinition(Employee.class);
e.where(e.type().in(e.param("empTypes")));
Example 3:
This query uses a TYPE expression to return the entity class:
SELECT TYPE(e), e.name
FROM Employee e JOIN e.dept d
WHERE d.name='Marketing'
As in the JPQL query, the entity class object is returned in the result of the criteria query:
DomainObject e = queryBuilder.createQueryDefinition(Employee.class);
DomainObject d = e.join("dept");
e.where(d.get("name").equal("Marketing"))
.select(e.type(), e.get("name"));
Case Expressions
These are the case expression examples. The Criteria API does not currently support update and delete operations.
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
DomainObject c = queryBuilder.createQueryDefinition(Customer.class);
c.select(c, c.case().when(c.get("annualSpending").greaterThan(10000))
.then("Premier")
.when(c.get("annualSpending").greaterThan(5000))
.then("Gold")
.when(c.get("annualSpending").greaterThan(2000))
.then("Silver")
.else("Bronze"));
This one uses a NULLIF expression:
SELECT AVG(NULLIF(e.salary, -99999))
FROM Employee e
DomainObject e = queryBuilder.createQueryDefinition(Employee.class);
e.select(e.avg(e.nullif(e.get("salary"), -999999)));
Subqueries
The criteria API supports the use of both correlated and non-correlated subqueries, as does JPQL. Like a top-level query, a subquery is constructed through the creation and modification of a QueryDefinition object.
The following JPQL query returns those customers whose unpaid balance is less than half of the average:
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 QueryDefinition objects for both the subquery and the containing query.
DomainObject goodCustomer = queryBuilder.createQueryDefinition(Customer.class);
DomainObject customer = queryBuilder.createQueryDefinition(Customer.class);
goodCustomer.where(goodCustomer.get("balanceOwed)
.lessThan(customer.select(customer.get("balancedOwed")
.dividedBy(2.0)
.avg())));
In a correlated subquery, the subquery and the containing query share variables.
The following JPQL query selects those employees that make more than all of the managers in their department:
SELECT emp
FROM Employee emp
WHERE emp.salary > ALL (
SELECT m.salary
FROM Manager m
WHERE m.department = emp.department)
Here's the criteria API equivalent. I'm using QueryDefinition variables to make this very explicit, although like the earlier queries, this query can be written without them.
QueryDefinition qOuter = queryBuilder.createQueryDefinition();
DomainObject emp = qOuter.addRoot(Employee.class);
QueryDefinition qSubq = queryBuilder.createQueryDefinition();
DomainObject mgr = qSubq.addRoot(Manager.class);
qSubq.select(mgr.get("salary"))
.where(mgr.get("department").equal(emp.get("department")));
qOuter.where(emp.get("salary").greaterThan(qSubq.all()));
Current Status
Our work in the Expert Group in designing the Criteria API involved numerous iterations and considerable debate as to the best approach, and we welcome your feedback on the results. Not yet reflected in the draft is our intention to provide additional and/or alternative ways to obtain QueryBuilder objects to support the construction of QueryDefinition objects without the need for an entity manager or entity manager factory in order to support disconnected usage scenarios. Since the provider's implementation of the QueryDefinition object is required to be serializable, this would also allow for QueryDefinition objects to be passed from a remote tier.
When you download the draft and review the spec, you'll notice that it flags a number of open issues and items that are in need of developer feedback. Please feel free to send me and the expert group your thoughts at jsr