All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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