Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Criteria API

A JPQL query is a string. That is convenient while the query is fixed, but a search form with optional filters turns string building into a mess of if and +. The Criteria API builds the same query from Java objects: conditions are created by method calls and added only when needed.

A search with optional filters

Create the class src/main/java/com/example/blog/PostSearch.java:

// src/main/java/com/example/blog/PostSearch.java
package com.example.blog;
 
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
 
public class PostSearch {
 
    public static List<Post> find(EntityManager em, String text, Integer minViews) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Post> query = cb.createQuery(Post.class);
        Root<Post> post = query.from(Post.class);
 
        List<Predicate> conditions = new ArrayList<>();
        if (text != null) {
            conditions.add(cb.like(cb.lower(post.get("title")), "%" + text.toLowerCase() + "%"));
        }
        if (minViews != null) {
            conditions.add(cb.ge(post.get("views"), minViews));
        }
 
        query.select(post)
                .where(conditions.toArray(Predicate[]::new))
                .orderBy(cb.desc(post.get("views")));
 
        return em.createQuery(query).getResultList();
    }
}
ObjectRole
CriteriaBuilderFactory of conditions and functions: like, ge, lower, desc
CriteriaQuery<Post>The query itself, like select ... from ... where ...
Root<Post>The entity in from; post.get("title") is the field
PredicateOne condition; several in where are joined with and

Calling it

// src/main/java/com/example/blog/Main.java
Seed.run(emf);
emf.runInTransaction(em -> {
    IO.println("All: " + PostSearch.find(em, null, null).stream().map(Post::getTitle).toList());
    IO.println("'java': " + PostSearch.find(em, "java", null).stream().map(Post::getTitle).toList());
    IO.println("'hibernate' and 50+ views: " + PostSearch.find(em, "hibernate", 50).stream().map(Post::getTitle).toList());
});
Hibernate: select p1_0.id,p1_0.author_id,p1_0.title,p1_0.views from Post p1_0 order by p1_0.views desc
All: [Hello, Hibernate, Records in Java 25, Lazy loading]
Hibernate: select p1_0.id,p1_0.author_id,p1_0.title,p1_0.views from Post p1_0 where lower(p1_0.title) like ? escape '' order by p1_0.views desc
'java': [Records in Java 25]
Hibernate: select p1_0.id,p1_0.author_id,p1_0.title,p1_0.views from Post p1_0 where lower(p1_0.title) like ? escape '' and p1_0.views>=? order by p1_0.views desc
'hibernate' and 50+ views: [Hello, Hibernate]

One method produced three different SQL queries: without where, with one condition and with two. The search text went into a parameter, so it is safe.

The static metamodel

Field names as strings — "title", "views" — are checked only at run time. The hibernate-processor annotation processor generates a class Post_ with a constant for every field during compilation. Add it to pom.xml:

<!-- pom.xml, inside <build><plugins> -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.16.0</version>
  <configuration>
    <annotationProcessorPaths>
      <path>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-processor</artifactId>
        <version>7.4.12.Final</version>
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

After mvn compile the classes Post_, Author_ and others appear in target/generated-sources/annotations. Now the search is written like this:

// src/main/java/com/example/blog/PostSearch.java
conditions.add(cb.like(cb.lower(post.get(Post_.title)), "%" + text.toLowerCase() + "%"));
conditions.add(cb.ge(post.get(Post_.views), minViews));
query.orderBy(cb.desc(post.get(Post_.views)));

A typo in Post_.titel is now a compilation error, and renaming a field updates the metamodel. The result of the program is the same.

Common mistake

With string names a typo is found only when that branch of code runs. Calling the search with a text when the code says post.get("titel"):

Exception in thread "main" org.hibernate.query.sqm.PathElementException: Could not resolve attribute 'titel' of 'com.example.blog.Post'

The call without a text works fine, so the error can survive testing. Use the metamodel for queries that live in the code for a long time.

Thanks for reading!

Donate to the site — we work hard for you.