Dynamic Query Building Spring Boot With JPA Criteria Queries
In this article, we’ll explore a flexible and reusable framework that allows developers to construct complex queries effortlessly.
Join the DZone community and get the full member experience.
Join For FreeDynamic query building is a critical aspect of modern application development, especially in scenarios where the search criteria are not known at compile time. In this publication, let's deep dive into the world of dynamic query building in Spring Boot applications using JPA criteria queries. We’ll explore a flexible and reusable framework that allows developers to construct complex queries effortlessly.
Explanation of Components
Criteria Interface
- The
Criteria
interface serves as the foundation for our framework. It extendsSpecification<T>
and provides a standardized way to build dynamic queries. - By implementing the
toPredicate
method, theCriteria
interface enables the construction of predicates based on the specified criteria.
package com.core.jpa;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
public class Criteria<T> implements Specification<T> {
private static final long serialVersionUID = 1L;
private transient List<Criterion> criterions = new ArrayList<>();
@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
if (!criterions.isEmpty()) {
List<Predicate> predicates = new ArrayList<>();
for (Criterion c : criterions) {
predicates.add(c.toPredicate(root, query, builder));
}
if (!predicates.isEmpty()) {
return builder.and(predicates.toArray(new Predicate[predicates.size()]));
}
}
return builder.conjunction();
}
public void add(Criterion criterion) {
if (criterion != null) {
criterions.add(criterion);
}
}
}
Criterion Interface
- The
Criterion
interface defines the contract for building individual predicates. It includes thetoPredicate
method, which is implemented by various classes to create specific predicates such as equals, not equals, like, etc.
public interface Criterion {
public enum Operator {
EQ, IGNORECASEEQ, NE, LIKE, GT, LT, GTE, LTE, AND, OR, ISNULL
}
public Predicate toPredicate(Root<?> root, CriteriaQuery<?> query, CriteriaBuilder builder);
}
LogicalExpression
Class
- The
LogicalExpression
class facilitates the combination of multiple criteria using logical operators such as AND and OR. - By implementing the
toPredicate
method, this class allows developers to create complex query conditions by chaining together simple criteria.
public class LogicalExpression implements Criterion {
private Criterion[] criterion;
private Operator operator;
public LogicalExpression(Criterion[] criterions, Operator operator) {
this.criterion = criterions;
this.operator = operator;
}
@Override
public Predicate toPredicate(Root<?> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<>();
for(int i=0;i<this.criterion.length;i++){
predicates.add(this.criterion[i].toPredicate(root, query, builder));
}
if(null != operator && operator.equals(Criterion.Operator.OR)) {
return builder.or(predicates.toArray(new Predicate[predicates.size()]));
}
return null;
}
}
Restrictions Class
- The
Restrictions
class provides a set of static methods for creating instances ofSimpleExpression
andLogicalExpression
. - These methods offer convenient ways to build simple and complex criteria, making it easier for developers to construct dynamic queries.
public class Restrictions {
private Restrictions() {
}
public static SimpleExpression eq(String fieldName, Object value, boolean ignoreNull) {
if (ignoreNull && (ObjectUtils.isEmpty(value)))
return null;
return new SimpleExpression(fieldName, value, Operator.EQ);
}
public static SimpleExpression ne(String fieldName, Object value, boolean ignoreNull) {
if (ignoreNull && (ObjectUtils.isEmpty(value)))
return null;
return new SimpleExpression(fieldName, value, Operator.NE);
}
public static SimpleExpression like(String fieldName, String value, boolean ignoreNull) {
if (ignoreNull && (ObjectUtils.isEmpty(value)))
return null;
return new SimpleExpression(fieldName, value.toUpperCase(), Operator.LIKE);
}
public static SimpleExpression gt(String fieldName, Object value, boolean ignoreNull) {
if (ignoreNull && (ObjectUtils.isEmpty(value)))
return null;
return new SimpleExpression(fieldName, value, Operator.GT);
}
public static SimpleExpression lt(String fieldName, Object value, boolean ignoreNull) {
if (ignoreNull && (ObjectUtils.isEmpty(value)))
return null;
return new SimpleExpression(fieldName, value, Operator.LT);
}
public static SimpleExpression gte(String fieldName, Object value, boolean ignoreNull) {
if (ignoreNull && (ObjectUtils.isEmpty(value)))
return null;
return new SimpleExpression(fieldName, value, Operator.GTE);
}
public static SimpleExpression lte(String fieldName, Object value, boolean ignoreNull) {
if (ignoreNull && (ObjectUtils.isEmpty(value)))
return null;
return new SimpleExpression(fieldName, value, Operator.LTE);
}
public static SimpleExpression isNull(String fieldName, boolean ignoreNull) {
if (ignoreNull)
return null;
return new SimpleExpression(fieldName, null, Operator.ISNULL);
}
public static LogicalExpression and(Criterion... criterions) {
return new LogicalExpression(criterions, Operator.AND);
}
public static LogicalExpression or(Criterion... criterions) {
return new LogicalExpression(criterions, Operator.OR);
}
public static <E> LogicalExpression in(String fieldName, Collection<E> value, boolean ignoreNull) {
if (ignoreNull && CollectionUtils.isEmpty(value))
return null;
SimpleExpression[] ses = new SimpleExpression[value.size()];
int i = 0;
for (Object obj : value) {
if(obj instanceof String) {
ses[i] = new SimpleExpression(fieldName, String.valueOf(obj), Operator.IGNORECASEEQ);
} else {
ses[i] = new SimpleExpression(fieldName, obj, Operator.EQ);
}
i++;
}
return new LogicalExpression(ses, Operator.OR);
}
public static Long convertToLong(Object o) {
String stringToConvert = String.valueOf(o);
if (!"null".equals(stringToConvert)) {
return Long.parseLong(stringToConvert);
} else {
return Long.valueOf(0);
}
}
}
SimpleExpression
Class
- The
SimpleExpression
class represents simple expressions with various operators such as equals, not equals, like, greater than, less than, etc. - By implementing the
toPredicate
method, this class translates simple expressions into JPA criteria predicates, allowing for precise query construction. - The
SimpleExpression
class represents simple expressions with various operators such as equals, not equals, like, greater than, less than, etc. - By implementing the
toPredicate
method, this class translates simple expressions into JPA criteria predicates, allowing for precise query construction.
public class SimpleExpression implements Criterion {
private String fieldName;
private Object value;
private Operator operator;
protected SimpleExpression(String fieldName, Object value, Operator operator) {
this.fieldName = fieldName;
this.value = value;
this.operator = operator;
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public Predicate toPredicate(Root<?> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
Path expression = null;
if (fieldName.contains(".")) {
String[] names = StringUtils.split(fieldName, ".");
if(names!=null && names.length>0) {
expression = root.get(names[0]);
for (int i = 1; i < names.length; i++) {
expression = expression.get(names[i]);
}
}
} else {
expression = root.get(fieldName);
}
switch (operator) {
case EQ:
return builder.equal(expression, value);
case IGNORECASEEQ:
return builder.equal(builder.upper(expression), value.toString().toUpperCase());
case NE:
return builder.notEqual(expression, value);
case LIKE:
return builder.like(builder.upper(expression), value.toString().toUpperCase() + "%");
case LT:
return builder.lessThan(expression, (Comparable) value);
case GT:
return builder.greaterThan(expression, (Comparable) value);
case LTE:
return builder.lessThanOrEqualTo(expression, (Comparable) value);
case GTE:
return builder.greaterThanOrEqualTo(expression, (Comparable) value);
case ISNULL:
return builder.isNull(expression);
default:
return null;
}
}
}
Usage Example
Suppose we have a User
entity and a corresponding UserRepository
interface defined in our Spring Boot application:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
private double salary;
// Getters and setters
}
public interface UserRepository extends JpaRepository<User, Long> {
}
With these entities in place, let’s demonstrate how to use our dynamic query-building framework to retrieve a list of users based on certain search criteria:
Criteria<User> criteria = new Criteria<>();
criteria.add(Restrictions.eq("age", 25, true));
criteria.add(Restrictions.like("name", "John", true));
criteria.add(Restrictions.or(
Restrictions.gt("salary", 50000, true),
Restrictions.isNull("salary", null, false)
));
List<User> users = userRepository.findAll(criteria);
In this example, we construct a dynamic query using the Criteria
interface and various Restrictions
provided by our framework. We specify criteria such as age equals 25, name contains "John", and salary greater than 50000 or null. Finally, we use the UserRepository
to execute the query and retrieve the matching users.
Conclusion
Dynamic query building with JPA criteria queries in Spring Boot applications empowers developers to create sophisticated queries tailored to their specific needs. By leveraging the framework outlined in this publication, developers can streamline the process of constructing dynamic queries and enhance the flexibility and efficiency of their applications.
Additional Resources
Opinions expressed by DZone contributors are their own.
Comments