Error selecting key or setting result to parameter object. Cause: java.sql.SQLSyntaxErrorException

## 通用mapper insertSelective方法报语法错误

报错信息如下

Caused by: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '?' at line 1
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.StatementImpl.executeInternal(StatementImpl.java:764)
	at com.mysql.cj.jdbc.StatementImpl.execute(StatementImpl.java:648)
	at com.alibaba.druid.filter.FilterChainImpl.statement_execute(FilterChainImpl.java:2958)
	at com.alibaba.druid.filter.FilterAdapter.statement_execute(FilterAdapter.java:2473)
	at com.alibaba.druid.filter.FilterEventAdapter.statement_execute(FilterEventAdapter.java:188)
	at com.alibaba.druid.filter.FilterChainImpl.statement_execute(FilterChainImpl.java:2956)
	at com.alibaba.druid.proxy.jdbc.StatementProxyImpl.execute(StatementProxyImpl.java:147)
	at com.alibaba.druid.pool.DruidPooledStatement.execute(DruidPooledStatement.java:619)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.ibatis.logging.jdbc.StatementLogger.invoke(StatementLogger.java:57)
	at com.sun.proxy.$Proxy405.execute(Unknown Source)
	at org.apache.ibatis.executor.statement.SimpleStatementHandler.query(SimpleStatementHandler.java:73)
	at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
	at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)
	at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
	at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
	at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49)
	at com.didichuxing.erp.log.interceptor.LogMybatisInterceptor.intercept(LogMybatisInterceptor.java:62)
	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
	at com.sun.proxy.$Proxy394.query(Unknown Source)
	at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:136)
	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
	at com.sun.proxy.$Proxy394.query(Unknown Source)
	at tk.mybatis.mapper.mapperhelper.SelectKeyGenerator.processGeneratedKeys(SelectKeyGenerator.java:77)
	at tk.mybatis.mapper.mapperhelper.SelectKeyGenerator.processAfter(SelectKeyGenerator.java:63)
	at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:50)
	at org.apache.ibatis.executor.statement.RoutingStatementHandler.update(RoutingStatementHandler.java:74)
	at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:50)
	at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)
	at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49)
	at com.didichuxing.erp.log.interceptor.LogMybatisInterceptor.intercept(LogMybatisInterceptor.java:62)
	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
	at com.sun.proxy.$Proxy394.update(Unknown Source)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)
	at com.sun.proxy.$Proxy394.update(Unknown Source)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:185)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
	... 123 more

核心报错如下

Error selecting key or setting result to parameter object. Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '?' at line 1

报错的大概意思是

为参数对象选择键或设置结果时出错。原因:java.sql.SQLSyntaxErrorException:您的 SQL 语法有错误;检查与您的 MySQL 服务器版本相对应的手册,以了解在“?”附近使用的正确语法在第 1 行

这个问题在我们线上项目偶发,大概三四天出现一次,可以说是非常折磨人。这个报错非常明确,但是找遍全网,也没找到相关的问题,这里记录一下我排查这个问题的思路,以及解决办法。

进入正文

  • 分析日志
    在这里插入图片描述
    日志打印了三条执行sql的记录,sql看不出来问题,不知道为什么会报错。

  • 分析源码
    找到 mybatis Interceptor 这个类的实现 PageInterceptor
    Interceptor 讲解去这里看

上源码

public Object intercept(Invocation invocation) throws Throwable {
        Object var22;
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement)args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds)args[2];
            ResultHandler resultHandler = (ResultHandler)args[3];
            Executor executor = (Executor)invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            if (args.length == 4) {
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
            //二级缓存有数据会走到这里
                cacheKey = (CacheKey)args[4];
                boundSql = (BoundSql)args[5];
            }

            List resultList;
            //判断是否需要分页
            if (this.dialect.skip(ms, parameter, rowBounds)) {
            	//不需要分页走这里
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else {
                String msId = ms.getId();
                Configuration configuration = ms.getConfiguration();
                Map<String, Object> additionalParameters = (Map)this.additionalParametersField.get(boundSql);
                //判断是否需要count
                if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
                    //需要count的上去了会拼上countSuffix 
                    String countMsId = msId + this.countSuffix;
                    MappedStatement countMs = this.getExistedMappedStatement(configuration, countMsId);
                    Long count;
                    if (countMs != null) {
                        count = this.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
                    } else {
                        countMs = (MappedStatement)this.msCountMap.get(countMsId);
                        if (countMs == null) {
                            countMs = MSUtils.newCountMappedStatement(ms, countMsId);
                            this.msCountMap.put(countMsId, countMs);
                        }
						//执行count的核心方法
                        count = this.executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
                    }

                    if (!this.dialect.afterCount(count, parameter, rowBounds)) {
                        Object var24 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
                        return var24;
                    }
                }

                if (!this.dialect.beforePage(ms, parameter, rowBounds)) {
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                } else {
                    parameter = this.dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
                    //这个方法在处理sql
                    String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
                    BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
                    Iterator var17 = additionalParameters.keySet().iterator();

                    while(true) {
                        if (!var17.hasNext()) {
                            resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
                            break;
                        }

                        String key = (String)var17.next();
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                }
            }
			//执行分页的核心方法
            var22 = this.dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            this.dialect.afterAll();
        }

        return var22;
    }

代码的核心语句我都加上了备注。

  • debug代码
    我把线上报错日志的入参整到本地,想复现报错,用上了jmeter并发请求,发现不会出现任何问题(崩溃)
    跟踪insertSelective 正确执行过程 如下
    执行先执行 install into 然后执行LAST_INSERT_ID() 获取刚刚插入的id 放入到插入的对象中

错误就是下面这段源码抛出来的,所以这里重点看了一下

    private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
        try {
            if (parameter != null && this.keyStatement != null && this.keyStatement.getKeyProperties() != null) {
                String[] keyProperties = this.keyStatement.getKeyProperties();
                Configuration configuration = ms.getConfiguration();
                MetaObject metaParam = configuration.newMetaObject(parameter);
                if (keyProperties != null) {
                    Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
                    //获取id值的核心方法
                    List<Object> values = keyExecutor.query(this.keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
                    if (values.size() == 0) {
                        throw new ExecutorException("SelectKey returned no data.");
                    }

                    if (values.size() > 1) {
                        throw new ExecutorException("SelectKey returned more than one value.");
                    }

                    MetaObject metaResult = configuration.newMetaObject(values.get(0));
                    //有结果的话下面方法把结果set到id中
                    if (keyProperties.length == 1) {
                        if (metaResult.hasGetter(keyProperties[0])) {
                            this.setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
                        } else {
                            this.setValue(metaParam, keyProperties[0], values.get(0));
                        }
                    } else {
                        this.handleMultipleProperties(keyProperties, metaParam, metaResult);
                    }
                }
            }

        } catch (ExecutorException var10) {
            throw var10;
        } catch (Exception var11) {
        //日志中打印的异常, 抛出的地方
            throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + var11, var11);
        }
    }
   

源码看到这里完全发现不了问题
我仔细对比了一下执行正确和执行错误的日志,发现出错都会出现这么一行日志,我觉得问题出现在这里。

com.legal.draft.dao.mapper.ContractDraftMapper.insertSelective!selectKey_COUNT

debug许久后,我根据日志中的 _COUNT 找到核心的代码块

		//判断是否分页
		if (this.dialect.skip(ms, parameter, rowBounds)) {
				//正常的话会在这个if
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else {
            	//错误的都会走这个
                String msId = ms.getId();
                Configuration configuration = ms.getConfiguration();
                Map<String, Object> additionalParameters = (Map)this.additionalParametersField.get(boundSql);
                if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
                //那个出现错误都会拼上_COUNT ,一行奇怪的日志让我离真相越来越近
                    String countMsId = msId + this.countSuffix;

为什么进else里面呢 一个插入方法为啥会分页,skip(是否分页)的源码如下

public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        if (ms.getId().endsWith("_COUNT")) {
            throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
        } else {
        	//获取page
            Page page = this.pageParams.getPage(parameterObject, rowBounds);
            if (page == null) {
                return true;
            } else {
                if (StringUtil.isEmpty(page.getCountColumn())) {
                    page.setCountColumn(this.pageParams.getCountColumn());
                }

                this.autoDialect.initDelegateDialect(ms);
                return false;
            }
        }
    }

源码很简单,就是判断当前线程有没有page,就是下面这个东西

PageHelper.startPage(pageNum, pageSize);

肯定没有啊,我一个插入接口怎么会去分页,真是离了大普,于是我去查PageHelper插件的原理,发现了这么一句话。

PageHelper.startPage 方法调用后,后面必须有一个Mapper的查询方法,必须被消费掉。否则会由于ThreadLocal的原因,当该线程被其他方法调用时被分页。在文档中非常明确的写了分页插件的使用方法!!另外这不是ThreadLocal引起的问题,是使用不当!

我豁然大悟,于是写了个测试接口。

    /**
     * 变更
     * @param
     * @return
     */
    @PostMapping("modifyDraftTemp")
    @EPAroundLog
    public BaseResponse<ContractVO> modifyDraft() {

        //开启分页
        PageHelper.startPage(1,1);

        //返回结果
        return BaseResponse.ok();
    }

在掉了几次这个测试接口以后,在调用插入insertSelective方法插入,果然问题必复现。

到这里问题已经明了,原因是项目中某个位置调用了PageHelper.startPage,但是没有被消费,因为这个page对象是放在本地线程中,ThreadLocal刚好执行了insertSelective的方法,会走到分页方法中,
处理获取刚刚插入的id时,组装出来的sql长这样。

SELECT LAST_INSERT_ID() LIMIT ?

执行的话就会报上面的sql语法的错误

知道问题原因,就很好解决了。
方法 1 : 找到项目中没有被消费的PageHelper.startPage的地方,优化掉代码。(我看了一下我们项目好几十处,太恐怖)
方法 2 : 在插入语句前加 PageHelper.clearPage() (治标不治本,但是好使,我又懒,我用的2)

        //如果别的线程有未消费的分页,会导致这里插入报错
        PageHelper.clearPage();
        if (contractDraftMapper.insertSelective(contractDraft) != 1) {
            return null;
        }

搞定。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值