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();
}
}| Object | Role |
|---|---|
CriteriaBuilder | Factory 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 |
Predicate | One 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.