Querying in JPA 2: Typesafe and Object Oriented
One of the main reasons for the popularity of the Java Persistence API (JPA) was JPQL, which supported an object-oriented mechanism for querying the database. But JPQL had a major limitation: JPQL queries were constructed as query strings, which were not evaluated at the compile time. The JPA 2.0 release introduced the concept of criteria queries, typesafe and more object oriented queries. Using criteria queries, developers can check the correctness of the queries at the compile time. This feature was previously available only in certain proprietary frameworks such as Hibernate.
In this article I explain criteria queries and explore the concept of metamodel, which is essential for building the criteria queries. The various APIs for criteria queries are also discussed.
The Metamodel Concept
In JPA 2.0, criteria queries are based on the concept of metamodel, which is defined for managed entities (be they entity classes, embedded classes or mapped super classes) of a specific persistence unit. In short: A class that provides meta information about a managed entity is a metamodel class. The static metamodel class that describes the state and the relationship of a managed class can be (1) generated using an annotation processor, (2) created by the programmer or (3) accessed using an EntityManager
.
Consider an Employee
entity defined in the package com.demo.entities
. Suppose this entity has primitive attributes such as id
, name
and age
and an OneToMany
association with the Address class as shown below.
package com.demo.entities;@Entity@Tablepublic class Employee{ private int id; private String name;private int age;@OneToManyprivate List<Address> addresses;// Other code
}
The name of the canonical metamodel class for Employee
(defined in the com.demo.entities
package) will be Employee_
annotated with the annotation javax.persistence.StaticMetamodel
. The properties of the metamodel classes are all static and public. Every attribute of the Employee
entity will be mapped in the respective metamodel class using the following rules as described in the JPA 2.0 specification:
- For a non-collection type such as the
id
,name
andage
properties ofEmployee
, a static propertySingularAttribute<A, B> b
is defined, whereb
is an object of typeB
defined in classA
. - For a collection type such as
addresses
defined incom.entities.Employee
, a static property of typeListAttribute<A, B> b
is defined, where theList
objectb
is of typeB
defined in classA
. The other collection types can be ofSetAttribute
,MapAttribute
orCollectionAttribute
type.
Here is the generated metamodel class using the annotation processor:
package com.demo.entities;import javax.annotation.Generated;import javax.persistence.metamodel.SingularAttribute;import javax.persistence.metamodel.ListAttribute;import javax.persistence.metamodel.StaticMetamodel;@Generated("EclipseLink-2.1.0.v20100614-r7608 @ Tue Jul 27 10:13:02 IST 2010")@StaticMetamodel(Employee.class)public class Employee_ { public static volatile SingularAttribute<Employee, Integer> id; public static volatile SingularAttribute<Employee, Integer> age; public static volatile SingularAttribute<Employee, String> name; public static volatile ListAttribute<Employee, Address> addresses;}
As their name implies, annotation processors process annotations and help in creating source files. Annotation processing can be activated at compile time. The metamodel classes created follow the rules for defining a canonical metamodel class as described in the JPA 2.0 specification. The NetBeans IDE (NetBeans 6.8) currently does not support annotation processing, but the Eclipse IDE and other build tools such as Maven and Ant do.
The biggest advantage of using a canonical metamodel class is that through its instantiation the persistent properties of an entity (the Employee
entity in this case) can be accessed at compile time. This feature makes criteria queries more typesafe at compile time itself.
The metamodel API is very closely related to the standard reflection API in Java. The major difference is that the complier cannot verify its correctness. For example, the following code would pass the compilation test:
Class myClass = Class.forName("com.demo.Test");Field myField = myClass.getField("myName");
The compiler assumes that the property named myName
is defined in the com.demo.Test
package, but it throws a run time exception if the class does not define a property called myName
.
The metamodel API forces the compiler to check whether the appropriate values are assigned to a persistent property of the Entity class. For instance, consider the age
property of the Employee
class, which is an integer
variable. The compiler throws an error if that property is assigned with a String
value. The implementers are not required to support the non-canonical feature because they are not portable across different vendors. Metamodel classes that are written by the programmers are generally called non-canonical metamodel classes. The persistence provider initializes the properties of the metamodel class when the EntityManagerFactory
is created.
Working with Criteria Queries
To better understand criteria queries, consider a Dept
entity that has a collection of Employee
instances. The metamodel classes for Employee
and the Dept
entity are as shown below:
//All Necessary Imports@StaticMetamodel(Dept.class)public class Dept_ { public static volatile SingularAttribute<Dept, Integer> id; public static volatile ListAttribute<Dept, Employee> employeeCollection; public static volatile SingularAttribute<Dept, String> name;}//All Necessary Imports@StaticMetamodel(Employee.class)public class Employee_ { public static volatile SingularAttribute<Employee, Integer> id; public static volatile SingularAttribute<Employee, Integer> age; public static volatile SingularAttribute<Employee, String> name; public static volatile SingularAttribute<Employee, Dept> deptId;}
The following code snippet shows a criteria query, which is used to obtain all the employee instances whose age is greater than 24.
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);Root<Employee> employee = criteriaQuery.from(Employee.class);Predicate condition = criteriaBuilder.gt(employee.get(Employee_.age), 24);criteriaQuery.where(condition);TypedQuery<Employee> typedQuery = em.createQuery(criteriaQuery);List<Employee> result = typedQuery.getResultList();Corresponding SQL: SELECT * FROM employee WHERE age > 24
Building a CriteriaQuery Instance
A CriteriaQuery
object is required for working with criteria queries on entity types or embeddable types. A CriteriaQuery
object is obtained by invoking the CriteriaBuilder
, createQuery
or CriteriaBuilder.createTupleQuery
methods, where the CriteriaBuilder
object acts as the factory for CriteriaQuery
objects. A CriteriaBuilder
factory object is obtained by either invoking the EntityManager.getCriteriaBuilder
method or the EntityManagerFactory.getCriteriaBuilder
instance. Generally, a CriteriaQuery
object is a typed object that specifies the type of outcome to be obtained from executing the criteria query. A CriteriaQuery
object for the Employee
entity is created in the following way:
CriteriaBuilder criteriaBuilder = emf.getCriteriaBuilder();CriteriaQuery<Employee> criteriaQuery = cb.createQuery(Employee.class);
QueryRoot
AbstractQuery
is the superclass for the CriteriaQuery
interface. It provides methods for obtaining the root of the query. The query root of a criteria query defines the entity type, which can be further navigated to obtain the desired results. It is analogous to the FROM
clause of a SQL query.
A Root
instance is also typed and defines the type that can appear in the FROM
clause of the query. A query root instance can be obtained using the AbstractQuery.from
method by passing an EntityType
argument. A criteria query can also have multiple query roots. The Root
object for an Employee
entity type can be created using the following syntax.
Root<Employee> employee = criteriaQuery.from(Employee.class);
Filtering the Queries
Filtering conditions are applied to the SQL statements in the FROM
clause. In criteria queries, filtering conditions are applied to a CriteriaQuery
object through a Predicate
or an Expression
instance. These conditions to a criteria query object are applied using the CriteriaQuery .where
method. A CriteriaBuilder
object also acts as a factory for Predicate
instances. A Predicate
instance is created by invoking conditional methods of the CriteriaBuilder
such as equal
, notEqual
, gt
, ge
, lt
, le
, between
and like
. A Predicate
instance can also be created by invoking the isNull
, isNotNull
and in
methods of the Expression
instance. A compound predicate statement can be constructed using the and
, or
and not
methods of the CriteriaBuilder
.
The following code snippet shows a Predicate
instance that checks for the employee instances whose ages are greater than 24.
Predicate condition = criteriaBuilder.gt(employee.get(Employee_.age), 24);criteriaQuery.where(condition);
The age
property is accessed using the Employee_
metamodel class at compile time using Employee_.age
, which is called as a path expression. The compiler throws an error if the age
property is compared with a String
literal, which was not possible in JPQL.
Page 1 of 3