文章目录

概要

` jpa生成的sql语句in在oracle数据库超过1000后会报错,现在用一个简单办法来解决这个问题

代码

hibernate拦截器

import cn.hutool.core.util.StrUtil;
import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.util.JdbcUtils;
import lombok.CustomLog;
import lombok.Data;
import org.hibernate.EmptyInterceptor;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

public class SQLStatementInterceptor extends EmptyInterceptor {

    /**
     * 进行sql语句的预处理时进行转换处理
     * @param sql
     * @return
     */
    @Override
    public String onPrepareStatement(String sql)
    {
        sql = inClause(sql);
        return sql;
    }

    /**
     * 需要拆分sql中IN超过1000的情况
     *
     * @param sql 结构化查询语言
     * @return {@link String }
     * @author pengzhikang
     * @date 2026/03/03 10:09
     */
    public String inClause(String sql) {
        // 简单测试是否包含in子句
        if (StrUtil.isBlank(sql) || !StrUtil.containsIgnoreCase(sql, "in")) {
            return sql;
        }

        // 获取当前数据库类型
        DataSourceProperties properties = SpringContextHolder.getBean(DataSourceProperties.class);
        DbType dbType = JdbcUtils.getDbTypeRaw(properties.getUrl(), properties.getDriverClassName());

        // sql解析
        SQLStatement sqlStatement = SQLUtils.parseSingleStatement(sql, dbType, false);
        // 处理其中的IN子句
        InClauseSplitVisitor visitor = new InClauseSplitVisitor();
        sqlStatement.accept(visitor);

        // 未执行过说明不存在in子句
        if (!visitor.isExecutionFlag()) {
            return sql;
        }
        // 将修改后的AST重新生成SQL字符串
        SQLUtils.FormatOption option = new SQLUtils.FormatOption(false, false);
        return SQLUtils.toSQLString(sqlStatement, dbType, option);
    }

}

druid访问者工具


import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLObject;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOperator;
import com.alibaba.druid.sql.ast.expr.SQLInListExpr;
import com.alibaba.druid.sql.ast.statement.SQLSelectQueryBlock;
import com.alibaba.druid.sql.dialect.mysql.visitor.MySqlASTVisitor;
import com.alibaba.druid.sql.dialect.oracle.visitor.OracleASTVisitor;
import com.alibaba.druid.sql.dialect.postgresql.visitor.PGASTVisitor;
import com.alibaba.druid.sql.dialect.sqlserver.visitor.SQLServerASTVisitor;
import com.alibaba.druid.sql.visitor.SQLASTVisitorAdapter;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 自定义AST访问者:遍历并拆分超长IN子句
 */
public class InClauseSplitVisitor extends SQLASTVisitorAdapter
        implements OracleASTVisitor, MySqlASTVisitor, PGASTVisitor, SQLServerASTVisitor {

    private static final int MAX_IN_SIZE = 1000;

    /** 执行标志 */
    private boolean executionFlag;

    public boolean isExecutionFlag() {
        return executionFlag;
    }

    /**
     * 访问IN表达式节点,核心处理逻辑
     */
    @Override
    public boolean visit(SQLInListExpr inExpr) {
        // 获取IN子句中的值列表(排除子查询场景,仅处理固定值列表)
        List<SQLExpr> targetList = inExpr.getTargetList();
        if (targetList == null || targetList.size() <= MAX_IN_SIZE) {
            // 值数量未超过限制,无需处理
            return true;
        }

        executionFlag = true;

        // 1. 拆分值列表为多个子列表(每个最多1000个值)
        List<List<SQLExpr>> splitLists = Lists.partition(targetList, MAX_IN_SIZE);

        // 2. 为每个子列表创建新的IN表达式
        List<SQLInListExpr> newInExprs = new ArrayList<>();
        for (List<SQLExpr> subList : splitLists) {
            SQLInListExpr newInExpr = new SQLInListExpr();
            // 复制原IN子句的左值(如 id IN (1,2) 中的 id)
            newInExpr.setExpr(inExpr.getExpr().clone());
            // 设置拆分后的子值列表
            newInExpr.setTargetList(subList.stream().map(SQLExpr::clone).collect(Collectors.toList()));
            // 复制原IN子句的NOT标记(如 NOT IN 场景)
            newInExpr.setNot(inExpr.isNot());
            newInExprs.add(newInExpr);
        }

        // 3. 将多个新IN表达式用OR连接,替换原IN表达式
        replaceOriginalInExpr(inExpr, newInExprs);

        // 停止遍历当前IN节点的子节点(已处理完成)
        return false;
    }

    /**
     * 替换原IN表达式为多个OR连接的新IN表达式
     */
    private void replaceOriginalInExpr(SQLInListExpr originalInExpr, List<SQLInListExpr> newInExprs) {
        // 获取原IN表达式的父节点(通常是SQLBinaryOpExpr,如 WHERE 中的 AND/OR 表达式)
        SQLObject parent = originalInExpr.getParent();

        if (parent instanceof SQLBinaryOpExpr) {
            SQLBinaryOpExpr parentBinaryExpr = (SQLBinaryOpExpr) parent;
            // 判断原IN表达式是父节点的左操作数还是右操作数
            if (parentBinaryExpr.getLeft() == originalInExpr) {
                // 构建OR连接的表达式替换左操作数
                parentBinaryExpr.setLeft(buildOrExpr(newInExprs));
            } else if (parentBinaryExpr.getRight() == originalInExpr) {
                // 构建OR连接的表达式替换右操作数
                parentBinaryExpr.setRight(buildOrExpr(newInExprs));
            }
        } else if (parent instanceof SQLSelectQueryBlock) {
            // 特殊场景:IN子句直接作为WHERE条件(无AND/OR父节点)
            SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) parent;
            queryBlock.setWhere(buildOrExpr(newInExprs));
        }
    }

    /**
     * 将多个IN表达式用OR运算符连接成一个表达式
     */
    private SQLExpr buildOrExpr(List<SQLInListExpr> inExprs) {
        if (inExprs.size() == 1) {
            return inExprs.get(0);
        }

        // 初始化OR表达式:第一个IN子句作为左值
        SQLBinaryOpExpr orExpr = new SQLBinaryOpExpr(
                inExprs.get(0),
                SQLBinaryOperator.BooleanOr,
                inExprs.get(1)
        );

        // 依次拼接剩余的IN子句
        for (int i = 2; i < inExprs.size(); i++) {
            orExpr = new SQLBinaryOpExpr(
                    orExpr,
                    SQLBinaryOperator.BooleanOr,
                    inExprs.get(i)
            );
        }

        return orExpr;
    }
}

配置,要替换成你实际的类路径

spring:
  jpa:
    properties:
      hibernate:
        #使拦截器生效
        ejb:
          interceptor: com.thghh.interceptor.SQLStatementInterceptor

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐