Mybatis<foreach>标签执行批量update事件

535 阅读4分钟

Mybatis-plus存在的问题

文章原地址(含其他未同步文章,更新最前线)

问题背景

项目中遇到需要批量更新操作时,使用mapper.xml的标签进行多语句执行操作

问题描述

在项目中正常使用mybatis-plus时,遇到了批量更新的语句,在xml中的sql语句如下

<update id="addStudy">
        <foreach collection="list" item="item" index="index" separator=";">
            update study set num = num - #{item.num} where id = #{item.id}
        </foreach>
</update>

这时候数据库url连接那里是启用了allowMultiQueries=true 按理说更新语句应该没问题,我们执行后控制台所打印的sql语句也是正常的 image.png 语句看起来没问题,复制过去navicat执行也没问题,当我以为万事大吉的时候,数据库数据抗议了,他出现问题了 image.png image.png 同样的减2,后两条却成倍减少,不信邪再执行一遍 image.png ok差距更大了,这实在摸不着头脑,向同事请教,反复排查,最后只能寄托在主管身上

解决办法

首先,未得到结果前,我尝试换了新一种执行办法,不用多语句更新操作是可行的

第一种解决办法

public String updateInventoryDecreaseBatch(Map<String, Object> map) {
        List<InventoryFormDetail> newInvGoodsList = (List<InventoryFormDetail>) map.get("newInvGoodsList");

        StringBuilder sb = new StringBuilder();
        sb.append("UPDATE study SET num = CASE id ");
        for (InventoryFormDetail item : newInvGoodsList) {
            sb.append("WHEN ").append(item.id()).append(" THEN COALESCE(num, 0) - ").append(item.num()).append(" ");
        }
        sb.append("END WHERE id IN (");
        for (InventoryFormDetail item : newInvGoodsList) {
            sb.append(item.id()).append(",");
        }
        sb.deleteCharAt(sb.length() - 1); // 删除最后一个逗号

        return sb.toString();
   }

这里的操作其实就是执行了单语句的批量update操作

UPDATE study SET num = CASE id 
WHEN 1 THEN COALESCE(num, 0) - 2
WHEN 2 THEN COALESCE(num, 0) - 2 
WHEN 3 THEN COALESCE(num, 0) - 2 
END 
WHERE id IN (1,2,3)

第二种解决办法

后续主管提供了druid的执行日志,并在日志中指出了问题所在 image.png 问题就出现在框中,sql在执行前,执行了一遍explain,但是由于是多语句,被分号提前结束掉,后续的两条便不是explain而是直接执行,也就导致最后在执行时,其实后两条已经是第二遍了。 但是我在个人项目中,尝试复现这个问题时,复现不出来,同时我也打了一份我的日志 image.png 我并没有所谓的explain,起初都以为是我druid版本不一致,后续我将版本调成一致也没问题,也是没有explain

算了,不罗嗦太多了,直接进入主题,经过一系列查找,最终定位到该图 image.png 问题在于mybatis的拦截器SqlExplainInterceptor,这个拦截器在后续版本被优化了,不会再出现类似问题,但是如果版本比较老,该拦截器有一定漏洞,会出现以上问题

解决办法很简单,升级mybatis-plus、重写拦截器、或注释掉该拦截器,建议是注释掉该拦截器,该拦截器是不推荐再生产环境使用的,在开发需要排查时用一用就好

以下附带前后新旧两种SqlExplainInterceptor的源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
@Intercepts({@Signature(
    type = Executor.class,
    method = "update",
    args = {MappedStatement.class, Object.class}
)})
public class SqlExplainInterceptor implements Interceptor {
    private static final Log logger = LogFactory.getLog(SqlExplainInterceptor.class);
    private final String minMySQLVersion = "5.6.3";
    private boolean stopProceed = false;

    public SqlExplainInterceptor() {
    }

    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement)invocation.getArgs()[0];
        if (ms.getSqlCommandType() == SqlCommandType.DELETE || ms.getSqlCommandType() == SqlCommandType.UPDATE) {
            Executor executor = (Executor)invocation.getTarget();
            Configuration configuration = ms.getConfiguration();
            Object parameter = invocation.getArgs()[1];
            BoundSql boundSql = ms.getBoundSql(parameter);
            Connection connection = executor.getTransaction().getConnection();
            String databaseVersion = connection.getMetaData().getDatabaseProductVersion();
            if (GlobalConfigUtils.getDbType(configuration).equals(DBType.MYSQL) && VersionUtils.compare("5.6.3", databaseVersion)) {
                logger.warn("Warn: Your mysql version needs to be greater than '5.6.3' to execute of Sql Explain!");
                return invocation.proceed();
            }

            this.sqlExplain(configuration, ms, boundSql, connection, parameter);
        }

        return invocation.proceed();
    }

    protected void sqlExplain(Configuration configuration, MappedStatement mappedStatement, BoundSql boundSql, Connection connection, Object parameter) {
        StringBuilder explain = new StringBuilder("EXPLAIN ");
        explain.append(boundSql.getSql());
        String sqlExplain = explain.toString();
        StaticSqlSource sqlsource = new StaticSqlSource(configuration, sqlExplain, boundSql.getParameterMappings());
        MappedStatement.Builder builder = new MappedStatement.Builder(configuration, "explain_sql", sqlsource, SqlCommandType.SELECT);
        builder.resultMaps(mappedStatement.getResultMaps()).resultSetType(mappedStatement.getResultSetType()).statementType(mappedStatement.getStatementType());
        MappedStatement queryStatement = builder.build();
        DefaultParameterHandler handler = new DefaultParameterHandler(queryStatement, parameter, boundSql);

        try {
            PreparedStatement stmt = connection.prepareStatement(sqlExplain);
            Throwable var13 = null;

            try {
                handler.setParameters(stmt);
                ResultSet rs = stmt.executeQuery();
                Throwable var15 = null;

                try {
                    while(rs.next()) {
                        if (!"Using where".equals(rs.getString("Extra"))) {
                            if (this.isStopProceed()) {
                                throw new MybatisPlusException("Error: Full table operation is prohibited. SQL: " + boundSql.getSql());
                            }
                            break;
                        }
                    }
                } catch (Throwable var40) {
                    var15 = var40;
                    throw var40;
                } finally {
                    if (rs != null) {
                        if (var15 != null) {
                            try {
                                rs.close();
                            } catch (Throwable var39) {
                                var15.addSuppressed(var39);
                            }
                        } else {
                            rs.close();
                        }
                    }

                }
            } catch (Throwable var42) {
                var13 = var42;
                throw var42;
            } finally {
                if (stmt != null) {
                    if (var13 != null) {
                        try {
                            stmt.close();
                        } catch (Throwable var38) {
                            var13.addSuppressed(var38);
                        }
                    } else {
                        stmt.close();
                    }
                }

            }

        } catch (Exception var44) {
            throw new MybatisPlusException(var44);
        }
    }

    public Object plugin(Object target) {
        return target instanceof Executor ? Plugin.wrap(target, this) : target;
    }

    public void setProperties(Properties prop) {
        String stopProceed = prop.getProperty("stopProceed");
        if (StringUtils.isNotEmpty(stopProceed)) {
            this.stopProceed = Boolean.valueOf(stopProceed);
        }

    }

    public boolean isStopProceed() {
        return this.stopProceed;
    }

    public void setStopProceed(boolean stopProceed) {
        this.stopProceed = stopProceed;
    }
}

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
@Intercepts({@Signature(
    type = Executor.class,
    method = "update",
    args = {MappedStatement.class, Object.class}
)})
public class SqlExplainInterceptor extends AbstractSqlParserHandler implements Interceptor {
    private static final Log logger = LogFactory.getLog(SqlExplainInterceptor.class);
    private Properties properties;

    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement)args[0];
        Object parameter = args[1];
        Configuration configuration = ms.getConfiguration();
        Object target = invocation.getTarget();
        StatementHandler handler = configuration.newStatementHandler((Executor)target, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
        this.sqlParser(SystemMetaObject.forObject(handler));
        return invocation.proceed();
    }

    public Object plugin(Object target) {
        return target instanceof Executor ? Plugin.wrap(target, this) : target;
    }

    public void setProperties(Properties prop) {
        this.properties = prop;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof SqlExplainInterceptor)) {
            return false;
        } else {
            SqlExplainInterceptor other = (SqlExplainInterceptor)o;
            if (!other.canEqual(this)) {
                return false;
            } else if (!super.equals(o)) {
                return false;
            } else {
                Object this$properties = this.getProperties();
                Object other$properties = other.getProperties();
                if (this$properties == null) {
                    if (other$properties != null) {
                        return false;
                    }
                } else if (!this$properties.equals(other$properties)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(Object other) {
        return other instanceof SqlExplainInterceptor;
    }

    public int hashCode() {
        int PRIME = true;
        int result = super.hashCode();
        Object $properties = this.getProperties();
        result = result * 59 + ($properties == null ? 43 : $properties.hashCode());
        return result;
    }

    public SqlExplainInterceptor() {
    }

    public Properties getProperties() {
        return this.properties;
    }

    public String toString() {
        return "SqlExplainInterceptor(properties=" + this.getProperties() + ")";
    }
}

Mybatis-Plus(三)在该文中有介绍一些mybatis-plus的使用,其中就有提到mp的分析插件【在MP中提供了对SQL执行的分析的插件,可用作阻断全表更新、删除的操作,注意:该插件仅适用于开发环境,不适用于生产环境。】

以上文中涉及的图片代码均为后面复现所截