com.feingto.cloud.data.jpa.specification.DynamicSpecifications Maven / Gradle / Ivy
package com.feingto.cloud.data.jpa.specification;
import com.feingto.cloud.data.jpa.specification.bean.Condition;
import com.feingto.cloud.data.jpa.specification.bean.Rule;
import com.feingto.cloud.data.jpa.specification.bean.RuleGroup;
import com.feingto.cloud.kit.CastUtils;
import com.feingto.cloud.kit.DateKit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.EnumUtils;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.Assert;
import javax.persistence.criteria.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
/**
* 通用表达式查询封装, 支持属性表达式嵌套, 联合运算查询.
*
* @author longfei
*/
@Slf4j
public class DynamicSpecifications {
/**
* IN 查询参数最大个数
* oracle in 里面参数个数最大1000个
* mysql 对sql语句的长度有限制(max_allowed_packet)
* 取舍后定义为600个参数
*/
private final static int MAX_IN_COUNT = 600;
/**
* 组装查询规则
*
* @param condition 查询参数封装 OperatorRule, Rule,Search
* @return Specification
*/
public static Specification byCondition(final Condition condition) {
return byCondition(condition, JoinType.LEFT);
}
/**
* 组装查询规则
*
* @param condition 查询条件表达式
* @param joinType JoinType.INNER(Default), JoinType.LEFT, JoinType.RIGHT
* @return Specification
*/
public static Specification byCondition(final Condition condition, final JoinType joinType) {
Assert.notNull(condition, "Parameter condition can't be null");
return (root, query, builder) -> {
List predicatesUnion = new ArrayList<>();
condition.getConditions().forEach(cond -> makeRuleGroup(predicatesUnion, cond, root, builder, joinType));
makeRuleGroup(predicatesUnion, condition, root, builder, joinType);
if (CollectionUtils.isNotEmpty(predicatesUnion)) {
if (predicatesUnion.size() == 1) {
applyGroupByAndDistinct(root, query, condition);
return predicatesUnion.get(0);
} else if (predicatesUnion.size() > 1) {
RuleGroup.UnionOperator union = condition.getOp() != null ? condition.getOp() : RuleGroup.UnionOperator.AND;
Predicate predicate = null;
switch (union) {
case AND:
predicate = builder.and(predicatesUnion.toArray(new Predicate[0]));
break;
case OR:
predicate = builder.or(predicatesUnion.toArray(new Predicate[0]));
break;
default:
break;
}
if (predicate != null) {
applyGroupByAndDistinct(root, query, condition);
return predicate;
}
}
}
applyGroupByAndDistinct(root, query, condition);
return builder.conjunction();
};
}
/**
* 组装断言规则组
*
* @param predicatesUnion 交集/并集断言集合
* @param condition 查询条件表达式
* @param root Root
* @param builder CriteriaBuilder
* @param joinType sql连接类型
*/
private static void makeRuleGroup(List predicatesUnion, Condition condition,
Root root, CriteriaBuilder builder, JoinType joinType) {
List subPredicates = new ArrayList<>();
for (RuleGroup group : condition.getGroups()) {
if (CollectionUtils.isEmpty(group.getRules())) {
continue;
}
List groupPredicates = new ArrayList<>();
group.getRules()
.forEach(rule -> toPredicates(groupPredicates, rule, makePath(rule, root, joinType), builder));
if (groupPredicates.size() > 0) {
RuleGroup.UnionOperator union = group.getOp() != null ? group.getOp() : RuleGroup.UnionOperator.AND;
addUnionPredicates(subPredicates, groupPredicates, union, builder);
}
}
condition.getRules()
.forEach(rule -> toPredicates(subPredicates, rule, makePath(rule, root, joinType), builder));
if (subPredicates.size() > 0) {
RuleGroup.UnionOperator union = condition.getOp() != null ? condition.getOp() : RuleGroup.UnionOperator.AND;
addUnionPredicates(predicatesUnion, subPredicates, union, builder);
}
}
/**
* 组装 Path
*
* @param rule 查询规则
* @param root Root
* @param joinType sql连接类型
*/
private static Path> makePath(Rule rule, Root root, JoinType joinType) {
Join, ?> join = null;
String[] names = rule.getProperty().split("\\.");
if (names.length > 1) {
join = applyJoin(names, root, joinType);
}
return join != null ? join.get(names[names.length - 1]) : root.get(names[0]);
}
/**
* 添加AND/OR操作符断言
*
* @param predicatesUnion 交集/并集断言集合
* @param predicates 断言集合
* @param union AND/OR
* @param builder CriteriaBuilder
*/
private static void addUnionPredicates(List predicatesUnion, List predicates, RuleGroup.UnionOperator union, CriteriaBuilder builder) {
switch (union) {
case AND:
predicatesUnion.add(builder.and(predicates.toArray(new Predicate[0])));
break;
case OR:
predicatesUnion.add(builder.or(predicates.toArray(new Predicate[0])));
break;
default:
break;
}
}
/**
* 嵌套的路径转换
* 例如对象 Group 有 user 属性名,表达式为 "user.name",转换为 Group.user.name 属性
*/
private static Join, ?> applyJoin(String[] names, Root root, JoinType joinType) {
Join, ?> join = root.join(names[0], joinType);
for (int i = 1; i < names.length - 1; i++) {
join = join.join(names[i], joinType);
}
return join;
}
/**
* 组装断言集合
*
* @param predicates 原始断言集合
* @param operatorRule 运算规则对象 Rule.OperatorRule
* @param expression Path
* @param builder CriteriaBuilder
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static void toPredicates(List predicates, Rule operatorRule, Path expression, CriteriaBuilder builder) {
Class> clazz = expression.getJavaType();
Object obj = operatorRule.getValue();
if (Date.class.isAssignableFrom(clazz) && obj != null && !obj.getClass().equals(clazz)) {
obj = convert2Date(CastUtils.cast(obj));
} else if (Enum.class.isAssignableFrom(clazz) && obj != null && !obj.getClass().equals(clazz)) {
if (List.class.isAssignableFrom(obj.getClass())) {
obj = CastUtils.>cast(obj).stream()
.map(v -> convert2Enum(CastUtils.cast(clazz), CastUtils.cast(v)));
} else {
obj = convert2Enum(CastUtils.cast(clazz), CastUtils.cast(obj));
}
}
switch (operatorRule.getOp()) {
case EQ:
predicates.add(builder.equal(expression, obj));
break;
case NEQ:
predicates.add(builder.notEqual(expression, obj));
break;
case SLIKE:
predicates.add(builder.like(expression, obj + "%"));
break;
case ELIKE:
predicates.add(builder.like(expression, "%" + obj));
break;
case LIKE:
predicates.add(builder.like(expression, "%" + obj + "%"));
break;
case GT:
predicates.add(builder.greaterThan(expression, (Comparable) obj));
break;
case LT:
predicates.add(builder.lessThan(expression, (Comparable) obj));
break;
case GTE:
predicates.add(builder.greaterThanOrEqualTo(expression, (Comparable) obj));
break;
case LTE:
predicates.add(builder.lessThanOrEqualTo(expression, (Comparable) obj));
break;
case IN:
case NIN:
addInPredicates(predicates, operatorRule.getOp(), obj, expression, builder);
break;
case ISEMPTY:
predicates.add(builder.isEmpty(CastUtils.cast(expression)));
break;
case ISNOTEMPTY:
predicates.add(builder.isNotEmpty(CastUtils.cast(expression)));
break;
case ISNULL:
predicates.add(builder.isNull(expression));
break;
case ISNOTNULL:
predicates.add(builder.isNotNull(expression));
break;
case ISBOOLEAN:
if (obj instanceof Boolean && (Boolean) obj || "true".equals(obj)) {
predicates.add(builder.isTrue(CastUtils.cast(expression)));
} else {
predicates.add(builder.isFalse(CastUtils.cast(expression)));
}
break;
case ISTRUE:
predicates.add(builder.isTrue(CastUtils.cast(expression)));
break;
case ISFALSE:
predicates.add(builder.isFalse(CastUtils.cast(expression)));
break;
default:
break;
}
}
/**
* 添加 IN 断言
* in >>> (in or in)
*
* @param predicates 原始断言集合
* @param operator IN/NIN
* @param obj 属性值
* @param expression Path
* @param builder CriteriaBuilder
*/
private static void addInPredicates(List predicates, Rule.Operator operator, Object obj, Path> expression, CriteriaBuilder builder) {
Assert.state(Rule.Operator.IN.equals(operator) || Rule.Operator.NIN.equals(operator),
"仅支持 IN、NIN 操作符");
Optional.of(obj)
.filter(v -> v instanceof List)
.map(List.class::cast)
.filter(list -> list.size() > MAX_IN_COUNT)
.map(list -> {
List subPredicates = new ArrayList<>();
int size = list.size();
for (int i = 0; i < size; i += MAX_IN_COUNT) {
final Predicate inPredicate = expression.in(list.subList(i,
Math.min(i + MAX_IN_COUNT, size)));
subPredicates.add(Optional.of(operator)
.filter(Rule.Operator.IN::equals)
.map(op -> builder.and(inPredicate))
.orElse(builder.not(inPredicate)));
}
addUnionPredicates(predicates, subPredicates, Optional.of(operator)
.filter(Rule.Operator.IN::equals)
.map(op -> RuleGroup.UnionOperator.OR)
.orElse(RuleGroup.UnionOperator.AND), builder);
return true;
})
.orElseGet(() -> {
predicates.add(Optional.of(operator)
.filter(Rule.Operator.IN::equals)
.map(op -> builder.and(expression.in(obj)))
.orElse(builder.not(expression.in(obj))));
return true;
});
}
/**
* 分组、去重(group by and distinct)
*/
private static void applyGroupByAndDistinct(Root root, CriteriaQuery> query, Condition condition) {
List> expGroupBys = new ArrayList<>();
condition.getGroupByNames().forEach(name -> {
Path> expGroupBy = null;
String[] names = name.split("\\.");
for (int i = 0; i < names.length; i++) {
expGroupBy = (i == 0 ? root : expGroupBy).get(names[i]);
}
expGroupBys.add(expGroupBy);
});
if (expGroupBys.size() > 0) {
query.groupBy(expGroupBys);
}
query.distinct(condition.isDistinct());
}
private static Date convert2Date(String dateString) {
SimpleDateFormat sFormat = new SimpleDateFormat(DateKit.DATE_TIME);
try {
return sFormat.parse(dateString);
} catch (ParseException e) {
try {
return sFormat.parse(DateKit.DATE);
} catch (ParseException e1) {
try {
return sFormat.parse(DateKit.TIME);
} catch (ParseException e2) {
log.error(String.format("parser %s to Date fail", dateString), e2);
}
}
}
return null;
}
private static > E convert2Enum(Class enumClass, String enumString) {
return EnumUtils.getEnum(enumClass, enumString);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy