JPA : JPQL Criteria Queries

In JPA2 there is a query API referred to as "criteria". This is really an API allowing the construction of queries expression by expression, and optionally making it type safe. It provides two ways of specifying a field/property. The first way is using Strings, and the second using a MetaModel. The advantage of the MetaModel is that it means that your queries are refactorable if you rename a field. Each example will be expressed in both ways where appropriate so you can see the difference.

Creating a Criteria query

To use the JPA Criteria API, firstly you need to create a CriteriaQuery object for the candidate in question, and set the candidate, its alias, and the result to be of the candidate type

CriteriaBuilder cb = emf.getCriteriaBuilder();
CriteriaQuery<Person> crit = cb.createQuery(Person.class);
Root<Person> candidateRoot = crit.from(Person.class);
candidateRoot.alias("p");

crit.select(candidateRoot);

So what we have there equates to

SELECT p FROM mydomain.Person p

For a complete list of all methods available on CriteriaBuilder, refer to For a complete list of all methods available on CriteriaQuery, refer to


JPQL equivalent of the Criteria query

If you ever want to know what is the equivalent JPQL string-based query for your Criteria, just print out criteriaQuery.toString(). This is not part of the JPA spec, but something that we feel is very useful so is provided as a DataNucleus vendor extension. So, for example, the criteria query above would result in the following from crit.toString()

SELECT p FROM mydomain.Person p

Criteria API : Result clause

The basic Criteria query above is fine, but you may want to define a result other than the candidate. To do this we need to use the Criteria API.

Path nameField = candidateRoot.get("name");
crit.select(nameField);

which equates to

SELECT p.name

Note that here we accessed a field by its name (as a String). We could easily have accessed it via the Criteria MetaModel too.


Criteria API : From clause joins

The basic Criteria query above is fine, but you may want to define some explicit joins. To do this we need to use the Criteria API.

Metamodel model = emf.getMetamodel();
ManagedType personType = model.type(Person.class);
Attribute addressAttr = personType.getAttribute("address");
Join addressJoin = candidateRoot.join((SingularAttribute)addressAttr);
addressJoin.alias("a");

which equates to

FROM mydomain.Person p JOIN p.address a

Criteria API : Filter

The basic Criteria query above is fine, but in the majority of cases we want to define a filter. To do this we need to use the Criteria API.

String-based:
Predicate nameEquals = cb.equal(candidateRoot.get("name"), "First");
crit.where(nameEquals);

MetaModel-based:
Predicate nameEquals = cb.equal(candidateRoot.get(Person_.name), "First");
crit.where(nameEquals);

You can also invoke methods, so a slight variation on this clause would be

String-based:
Predicate nameUpperEquals = cb.equal(cb.upper(candidateRoot.get("name")), "FIRST");

MetaModel-based:
Predicate nameUpperEquals = cb.equal(cb.upper(candidateRoot.get(Person_.name)), "FIRST");

which equates to

WHERE (UPPER(p.name) = 'FIRST')

Criteria API : Ordering

The basic Criteria query above is fine, but in many cases we want to define ordering. To do this we need to use the Criteria API.

String-based:
Order orderFirstName = cb.desc(candidateRoot.get("name"));
crit.orderBy(orderFirstName);

MetaModel-based:
Order orderFirstName = cb.desc(candidateRoot.get(Person_.name));
crit.orderBy(orderFirstName);

which equates to

ORDER BY p.name DESC

Criteria API : Parameters

Another common thing we would want to do is specify input parameters. To do this we need to use the Criteria API. Let's take an example of a filter with parameters.

String-based:
ParameterExpression param1 = cb.parameter(String.class, "myParam1");
Predicate nameEquals = cb.equal(candidateRoot.get("name"), param1);
crit.where(nameEquals);

MetaModel-based:
ParameterExpression param1 = cb.parameter(String.class, "myParam1");
Predicate nameEquals = cb.equal(candidateRoot.get(Person_.name), param1);
crit.where(nameEquals);

which equates to

WHERE (p.name = :myParam)

Don't forget to set the value of the parameters before executing the query!


Executing a Criteria query

Ok, so we've seen how to generate a Criteria query. So how can we execute it ? This is simple; convert it into a standard JPA query, set any parameter values and execute it.

Query query = em.createQuery(crit);
List<Person> results = query.getResultList();

MetaModel

As we mentioned at the start of this section, there is a MetaModel allowing refactorability. In JPA the MetaModel is a static metamodel of generated classes that mirror the applications persistable classes and have persistable fields marked as public and static so that they can be accessed when generating the queries. In the examples above you saw reference to a class with name with suffix "_". This is a metamodel class. It is defined below.

The JPA2 spec contains the following description of the static metamodel.
For every managed class in the persistence unit, a corresponding metamodel class is produced as follows:

  • For each managed class X in package p, a metamodel class X_ in package p is created.
  • The name of the metamodel class is derived from the name of the managed class by appending "_" to the name of the managed class.
  • The metamodel class X_ must be annotated with the javax.persistence.StaticMetamodel annotation
  • If class X extends another class S, where S is the most derived managed class (i.e., entity or mapped superclass) extended by X, then class X_ must extend class S_, where S_ is the meta-model class created for S.
  • For every persistent non-collection-valued attribute y declared by class X, where the type of y is Y, the metamodel class must contain a declaration as follows:
    public static volatile SingularAttribute<X, Y> y;
  • For every persistent collection-valued attribute z declared by class X, where the element type of z is Z, the metamodel class must contain a declaration as follows:
    • if the collection type of z is java.util.Collection, then
      public static volatile CollectionAttribute<X, Z> z;
    • if the collection type of z is java.util.Set, then
      public static volatile SetAttribute<X, Z> z;
    • if the collection type of z is java.util.List, then
      public static volatile ListAttribute<X, Z> z;
    • if the collection type of z is java.util.Map, then
      public static volatile MapAttribute<X, K, Z> z;
      where K is the type of the key of the map in class X

Let's take an example, for the following class

package org.datanucleus.samples.jpa2.metamodel;

import java.util.*;
import javax.persistence.*;

@Entity
public class Person
{
    @Id
    long id;

    String name;

    @OneToMany
    List<Address> addresses;
}

the static metamodel class will be

package org.datanucleus.samples.jpa2.metamodel;

import javax.persistence.metamodel.*;

@StaticMetamodel(Person.class)
public class Person_ 
{
    public static volatile SingularAttribute<Person, Long> id;
    public static volatile SingularAttribute<Person, String> name;
    public static volatile ListAttribute<Person, Address> addresses;
}

So how do we generate this metamodel definition for our query classes? DataNucleus provides an annotation processor in the jar datanucleus-jpa-query that can be used when compiling your model classes to generate the static metamodel classes. What this does is when the compile is invoked, all classes that have persistence annotations will be passed to the annotation processor and a Java file generated for its metamodel. Then all classes (original + metamodel) are compiled.

To enable this in Maven2 you would need the above jar, plus datanucleus-core and datanucleus-api-jpa (as well as persistence-api.jar) to be in the CLASSPATH at compile

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <source>1.6</source>
        <target>1.6</target>
    </configuration>
</plugin>

To enable this in Eclipse you would need to do the following

  • Go to Java Compiler and make sure the compiler compliance level is 1.6 or above
  • Go to Java Compiler -> Annotation Processing and enable the project specific settings and enable annotation processing
  • Go to Java Compiler -> Annotation Processing -> Factory Path, enable the project specific settings and then add the following jars to the list: datanucleus-jpa-query.jar, datanucleus-api-jpa.jar, datanucleus-core.jar, jpa-api.jar, jdo-api.jar