JPA criteria queries

Querying database with JPQL queries is quite simple. But a main drawback of this solution is its static character. To create dynamic queries, a more adapted solution exists, JPA Criteria.

In one of previous articles, we discovered the static metamodel in JPA. It can be used to generate a dynamic JPA queries, based on JPA's Criteria API. The first chapter of this article will explain the Criteria API. The next one will cover more technical part by describing important interfaces of the API. At the last chapter we'll see how to use Criteria API to construct dynamical queries.

What is JPA Criteria API ?

We can simplify the definition by telling that Criteria API is a programmatic way of preparing JPA database queries. Instead of concatenating Strings, we can use Java's objects and create queries in another manner. This another way has some advantages compared with text queries:

But they are not only advantages of using Criteria queries. The drawbacks exist too. Firstly, they can't be pushed in parse cache. Secondly, in the most of cases, Criteria queries are also verbose. Imagine the case of a simple SELECT query which we can write in one String expression. To do the same, we will need several lines in Criteria API.

Components of JPA Criteria API

JPA Criteria queries look like object graph where each part represents more specific part of the query. For example, the root will represent the table used in FROM clause while the other nodes will represent associated tables. A keyword linked with Criteria JPA's components appeared here. It's root, and more precisely, javax.persistence.criteria.Root implementations. As we can read in the JavaDoc, Root references always an entity and points to FROM part of query.

Another important component of Criteria API is javax.persistence.criteria.CriteriaBuilder. The implementations of this interface are used to generated the queries, add conditions (as SQL's WHERE, AND, ==) or orderings (SQL's ASC or DESC for ORDER BY clauses) to queries.

Interfaces used to define queries are javax.persistence.criteria.AbstractQuery and javax.persistence.criteria.CriteriaQuery, where the first one is a superinterface for the second. By implementing them, we can create an object that can be used as a parameter for EntityManager's createQuery() method. The implementation of CriteriaQuery has to have a set of root entities. It means that you can't execute a query which doesn't generate a Root object. If you try to do that, following exception should be returned:

java.lang.IllegalStateException: No criteria query roots were specified
  at org.hibernate.ejb.criteria.CriteriaQueryImpl.validate(CriteriaQueryImpl.java:303)
  at org.hibernate.ejb.criteria.CriteriaQueryCompiler.compile(CriteriaQueryCompiler.java:146)
  at org.hibernate.ejb.AbstractEntityManagerImpl.createQuery(AbstractEntityManagerImpl.java:587)
  at com.waitingforcode.jpa.test.CriteriaTest.test(CriteriaTest.java:143)

Code samples with JPA Criteria API

After this introduction we can finally pass to code samples. We'll focus here on basic cases as selecting from a single table. In our sample, we'll extend previously saw Product entity by adding complementary fields: price, added date and color. The sample test code will show how to construct a simple query based on Criteria API and how to create a more complicated dynamic query, based on user choice. Let's start by the first one:

@Test
public void simpleSelect() {
  EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("sampleUnit");
  EntityManager entityManager = emFactory.createEntityManager();
  Product product = new Product();
  product.setName("radish");
  entityManager.persist(product);
  
  // check if was correctly inserted
  assertTrue("Radish wasn't correctly inserted to the database", 
    product.getId() > 0);
  
  // make simple query with Criteria API
  CriteriaBuilder builder = entityManager.getCriteriaBuilder();
  CriteriaQuery<Product> criteriaQuery = builder.createQuery(Product.class);
  Root<Product> criteriaRadish = criteriaQuery.from(Product.class);
  criteriaQuery.where(builder.equal(criteriaRadish.get(Product_.name), "radish"));
  TypedQuery<Product> typedQuery = entityManager.createQuery(criteriaQuery);
  Product radishDb = typedQuery.getSingleResult();
  assertTrue("Pea should be found in the database but it wasn't", radishDb != null);
  assertTrue("Retreived product should be a 'radish' but was '"+radishDb.getName()+"'", 
    radishDb.getName().equals("radish"));
}

This case is quite simple. At the begin, we prepare the ground by deleting all products called "radish" and by inserting only one with this name. After we check if the product was added into database. Next part concerns our Criteria problematic. Depart point for the Criteria queries is CriteriaBuilder which creates a CriteriaQuery. After that, CriteriaQuery's instance is used to generate the root element of the graph. In our case, the root is the single present element. After we call an where clause by using Product's static metamodel. Thanks to it, we avoid to type errors in the query. The last step consists on create TypedQuery instance by invoking createQuery method and getting the product through standard getSingleResult method.

The previous example was quite normal. Now we'll try to implement a more dynamic query, based on user choices. But before writing test case, we need to upgrade Product's entity and its static model by adding price, added date and color fields:

// Product.java
@Entity
@Table(name="products")
public class Product {

  private Integer id;
  private String name;
  private String color;
  private double price;
  private Date addedDate;
  
  @Id
  @GeneratedValue(strategy = IDENTITY)
  @Column(name="id")
  public Integer getId() {
    return this.id;
  }
  
  @Column(name="name")
  @NotEmpty
  public String getName() {
    return this.name;
  }

  @Column(name="color")
  public String getColor() {
    return this.color;
  }
  
  @Column(name="price")
  public double getPrice() {
    return this.price;
  }
  
  @Column(name="added_date")
  @Temporal(TIMESTAMP)
  public Date getAddedDate() {
    return this.addedDate;
  }
  
  public void setId(Integer id) {
    this.id = id;
  }
  
  public void setName(String name) {
    this.name = name;
  }

  public void setColor(String color) {
    this.color = color;
  }

  public void setPrice(double price) {
    this.price = price;
  }

  public void setAddedDate(Date addedDate) {
    this.addedDate = addedDate;
  }

  @Override
  public String toString() {
    return "Product {"+this.name+"}";
  }
}

// Product_.java
@StaticMetamodel(Product.class)
public class Product_ {
  public static volatile SingularAttribute<Product, Integer> id;
  public static volatile SingularAttribute<Product, String> name;
  public static volatile SingularAttribute<Product, String> color;
  public static volatile SingularAttribute<Product, Double> price;
  public static volatile SingularAttribute<Product, Date> addedDate; 
}

We won't discuss about previous code. It's a simple entity with even simpler static model. Instead, we'll see how to construct a multi-criteria query for them:

@Test
public void dynamicSelect() {
  EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("sampleUnit");
  assertTrue("Entity manager factory can't be null", emFactory != null);
  EntityManager entityManager = emFactory.createEntityManager();
  // add coffee
  Product coffee = new Product();
  coffee.setName("coffee");
  coffee.setAddedDate(new Date());
  coffee.setPrice(40.5d);
  coffee.setColor("black");
  entityManager.persist(coffee);
  // add tea
  Product tea = new Product();
  tea.setName("tea");
  tea.setAddedDate(new Date());
  tea.setPrice(45.5d);
  tea.setColor("brown");
  entityManager.persist(tea);
  // add milk
  Product milk = new Product();
  milk.setName("milk");
  milk.setAddedDate(new Date());
  milk.setPrice(31.5d);
  milk.setColor("white");
  entityManager.persist(milk);
  
  // construct criteria query based on given configuration
  double priceFrom = 30d;
  double priceTo = 50d;
  String color = null;
  String name = null;
  String likeName = "e";
  Date addedDate = null;  
  
  CriteriaBuilder builder = entityManager.getCriteriaBuilder();
  CriteriaQuery<Product> criteriaQuery = builder.createQuery(Product.class);
  Root<Product> criteriaProducts = criteriaQuery.from(Product.class);
  List<Predicate> conditions = new ArrayList<Predicate>();
  if (priceFrom > 0) {
    conditions.add(builder.ge(criteriaProducts.get(Product_.price), priceFrom));
  }
  if (priceTo > 0) {
    conditions.add(builder.le(criteriaProducts.get(Product_.price), priceTo));
  }
  if (color != null && !color.equals("")) {
    conditions.add(builder.equal(criteriaProducts.get(Product_.color), color));
  }
  if (name != null && !name.equals("")) {
    conditions.add(builder.equal(criteriaProducts.get(Product_.name), name));
  }
  else if (likeName != null && !likeName.equals("")) {
    conditions.add(builder.like(criteriaProducts.get(Product_.name), "%"+likeName+"%"));
  }
  if (addedDate != null) {
    conditions.add(builder.equal(criteriaProducts.get(Product_.addedDate), addedDate));
  }
  
  // get results
  criteriaQuery.where(builder.and(conditions.toArray(new Predicate[conditions.size()])));
  TypedQuery<Product> typedQuery = entityManager.createQuery(criteriaQuery);
  List<Product> products = typedQuery.getResultList();
  assertTrue("Any product found in the database", products != null);
  assertTrue("The query should find 2 products but "+products.size() + " were found instead ("+products+")", 
    products.size() == 2);
}

As we can see in this article, JPA's Criteria API is a powerful tool, even if we didn't expose all of its features (jointures, having clauses, ordering, writing operations). We saw that thanks to static metamodels, the Criteria-based queries are type safes and that it's more probable to detect an error on compile time that in the case of handy-written JPQL queries. We also saw that more complex queries, based on alterable conditions, are prettier in Criteria than in normal JPQL queries.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!