在现代的企业级应用中,动态查询是一个非常常见的需求。Spring Data JPA 提供了一套强大的工具集,包括 Specification
、CriteriaBuilder
和 Predicate
,可以帮助我们构建复杂的动态查询。本文将详细介绍这些工具的使用,并通过一个实际示例展示如何在 Spring Data JPA 中实现动态查询。
一、相关概念和类
在开始编写代码之前,我们需要理解几个关键概念和类:
- Specification:
Specification
是 Spring Data JPA 提供的一个接口,用于构建 JPA Criteria 查询。它通常与CriteriaBuilder
和Predicate
一起使用。- 定义:
public interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder); }
Specification
接口中的toPredicate
方法用于将查询条件转换为 JPA 的Predicate
对象。
- CriteriaBuilder:
CriteriaBuilder
是 JPA 提供的一个接口,用于构建查询的各个部分,如条件(Predicate
)、排序(Order
)等。- 常用方法:
equal(Expression<?> x, Object y)
:构建等于条件like(Expression<String> x, String pattern)
:构建模糊查询条件greaterThan(Expression<? extends Comparable> x, Comparable y)
:构建大于条件lessThan(Expression<? extends Comparable> x, Comparable y)
:构建小于条件and(Predicate... restrictions)
:构建 AND 组合条件or(Predicate... restrictions)
:构建 OR 组合条件
- Predicate:
Predicate
是 JPA Criteria 查询中的一个条件表达式,用于构建复杂的查询条件。- 定义:
public interface Predicate extends Expression<Boolean> { }
二、示例:图书查询系统
为了更好地理解这些概念,我们将通过一个简单的图书查询系统的例子来演示如何使用这些工具进行动态查询。
1. 定义实体类 Book
首先,我们定义一个简单的 Book
实体类,它包含书名、作者和出版日期等字段。
import javax.persistence.*;
import java.util.Date;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private Date publishDate;
// Getters and setters omitted for brevity
}
2. 定义查询条件类 BookQueryCriteria
接下来,我们定义一个 BookQueryCriteria
类,用于封装用户的查询条件。这些条件将会在动态查询中使用。
import cn.jichuang.annotation.Query;
import lombok.Data;
import java.util.Date;
@Data
public class BookQueryCriteria {
@Query(type = Query.Type.INNER_LIKE)
private String title;
@Query(type = Query.Type.INNER_LIKE)
private String author;
@Query(type = Query.Type.GREATER_THAN, propName = "publishDate")
private Date publishDateFrom;
@Query(type = Query.Type.LESS_THAN, propName = "publishDate")
private Date publishDateTo;
}
3. 定义 BookRepository
接口
我们定义一个 BookRepository
接口,它继承自 JpaRepository
和 JpaSpecificationExecutor
。JpaSpecificationExecutor
接口提供了动态查询的能力。
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> {
}
4. 实现动态查询工具类 QueryHelp
我们实现一个 QueryHelp
工具类,用于根据查询条件动态构建 Predicate
对象。
package cn.jichuang.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import cn.jichuang.annotation.DataPermission;
import cn.jichuang.annotation.Query;
import javax.persistence.criteria.*;
import java.lang.reflect.Field;
import java.util.*;
@Slf4j
@SuppressWarnings({"unchecked","all"})
public class QueryHelp {
public static <R, Q> Predicate getPredicate(Root<R> root, Q query, CriteriaBuilder cb) {
List<Predicate> list = new ArrayList<>();
if(query == null){
return cb.and(list.toArray(new Predicate[0]));
}
try {
List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
for (Field field : fields) {
boolean accessible = field.isAccessible();
field.setAccessible(true);
Query q = field.getAnnotation(Query.class);
if (q != null) {
String propName = q.propName();
String joinName = q.joinName();
String blurry = q.blurry();
String attributeName = isBlank(propName) ? field.getName() : propName;
Class<?> fieldType = field.getType();
Object val = field.get(query);
if (ObjectUtil.isNull(val) || "".equals(val)) {
continue;
}
Join join = null;
if (ObjectUtil.isNotEmpty(blurry)) {
String[] blurrys = blurry.split(",");
List<Predicate> orPredicate = new ArrayList<>();
for (String s : blurrys) {
orPredicate.add(cb.like(root.get(s).as(String.class), "%" val.toString() "%"));
}
Predicate[] p = new Predicate[orPredicate.size()];
list.add(cb.or(orPredicate.toArray(p)));
continue;
}
if (ObjectUtil.isNotEmpty(joinName)) {
String[] joinNames = joinName.split(">");
for (String name : joinNames) {
switch (q.join()) {
case LEFT:
if(ObjectUtil.isNotNull(join) && ObjectUtil.isNotNull(val)){
join = join.join(name, JoinType.LEFT);
} else {
join = root.join(name, JoinType.LEFT);
}
break;
case RIGHT:
if(ObjectUtil.isNotNull(join) && ObjectUtil.isNotNull(val)){
join = join.join(name, JoinType.RIGHT);
} else {
join = root.join(name, JoinType.RIGHT);
}
break;
case INNER:
if(ObjectUtil.isNotNull(join) && ObjectUtil.isNotNull(val)){
join = join.join(name, JoinType.INNER);
} else {
join = root.join(name, JoinType.INNER);
}
break;
default: break;
}
}
}
switch (q.type()) {
case EQUAL:
list.add(cb.equal(getExpression(attributeName,join,root).as((Class<? extends Comparable>) fieldType),val));
break;
case GREATER_THAN:
list.add(cb.greaterThanOrEqualTo(getExpression(attributeName,join,root).as((Class<? extends Comparable>) fieldType), (Comparable) val));
break;
case LESS_THAN:
list.add(cb.lessThanOrEqualTo(getExpression(attributeName,join,root).as((Class<? extends Comparable>) fieldType), (Comparable) val));
break;
case LESS_THAN_NQ:
list.add(cb.lessThan(getExpression(attributeName,join,root).as((Class<? extends Comparable>) fieldType), (Comparable) val));
break;
case INNER_LIKE:
list.add(cb.like(getExpression(attributeName,join,root).as(String.class), "%" val.toString() "%"));
break;
case LEFT_LIKE:
list.add(cb.like(getExpression(attributeName,join,root).as(String.class), "%" val.toString()));
break;
case RIGHT_LIKE:
list.add(cb.like(getExpression(attributeName,join,root).as(String.class), val.toString() "%"));
break;
case IN:
if (CollUtil.isNotEmpty((Collection<Object>)val)) {
list.add(getExpression(attributeName,join,root).in((Collection<Object>) val));
}
break;
case NOT_IN:
if (CollUtil.isNotEmpty((Collection<Object>)val)) {
list.add(getExpression(attributeName,join,root).in((Collection<Object>) val).not());
}
break;
case NOT_EQUAL:
list.add(cb.notEqual(getExpression(attributeName,join,root), val));
break;
case NOT_NULL:
list.add(cb.isNotNull(getExpression(attributeName,join,root)));
break;
case IS_NULL:
list.add(cb.isNull(getExpression(attributeName,join,root)));
break;
case BETWEEN:
List<Object> between = new ArrayList<>((List<Object>)val);
list.add(cb.between(getExpression(attributeName, join, root).as((Class<? extends Comparable>) between.get(0).getClass()),
(Comparable) between.get(0), (Comparable) between.get(1)));
break;
default: break;
}
}
field.setAccessible(accessible);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
int size = list.size();
return cb.and(list.toArray(new Predicate[size]));
}
@SuppressWarnings("unchecked")
private static <T, R> Expression<T> getExpression(String attributeName, Join join, Root<R> root) {
if (ObjectUtil.isNotEmpty(join)) {
return join.get(attributeName);
} else {
return root.get(attributeName);
}
}
private static boolean isBlank(final CharSequence cs) {
int strLen;
if (cs == null || (strLen = cs.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i ) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}
return true;
}
private static List<Field> getAllFields(Class<?> clazz, List<Field> fields) {
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
if (clazz.getSuperclass() != null) {
getAllFields(clazz.getSuperclass(), fields);
}
return fields;
}
}
5. 在服务中使用 Specification 进行查询
我们可以在服务层中使用 Specification
来执行动态查询。例如,我们可以在 BookService
中添加一个方法,根据查询条件动态查询图书。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class BookService {
@Autowired
private BookRepository bookRepository;
public List<Book> findAllBooks(BookQueryCriteria criteria) {
Specification<Book> specification = (root, query, cb) -> QueryHelp.getPredicate(root, criteria, cb);
return bookRepository.findAll(specification);
}
}
三、结语
通过以上步骤,我们实现了一个简单的图书查询系统,能够根据用户提供的查询条件动态构建 JPA 查询。这种方式不仅提高了代码的灵活性和可维护性,还增强了系统的扩展性。Specification
、CriteriaBuilder
和 Predicate
是 JPA 提供的强大工具,熟练掌握它们的使用可以极大地提升我们的开发效率。
我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!