There are only 1 target objects. You either specified a wrong ‘keyProperty‘ or encountered a driver

Posted Dreamer who

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了There are only 1 target objects. You either specified a wrong ‘keyProperty‘ or encountered a driver相关的知识,希望对你有一定的参考价值。

1、由于标题限制,详细异常堆栈如下

 

org.apache.ibatis.executor.ExecutorException: Too many keys are generated. There are only 1 target objects. You either specified a wrong 'keyProperty' or encountered a driver bug like #1523.
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.assignKeysToParam(Jdbc3KeyGenerator.java:117)
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.assignKeys(Jdbc3KeyGenerator.java:100)
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.processBatch(Jdbc3KeyGenerator.java:81)
    at org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.processAfter(Jdbc3KeyGenerator.java:67)
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:51)
    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 jdk.internal.reflect.GeneratedMethodAccessor418.invoke
    at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:566)
    at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49)

 

2、场景描述

 

改造之前项目仅仅使用了mybatis+druid,及动态数据源切换功能。

为了方便的自动读写分离,使用了Zebrahttps://github.com/Meituan-Dianping/Zebra)开源的读写分离组件(对读写分离代码单独抽出,不使用配置初始化数据源,显示的传入主从数据源:

 

public class GroupDataSource extends AbstractDataSource 

    protected final DataSource readDataSource;

    protected final DataSource writeDataSource;

    private final ReadWriteStrategy readWriteStrategy;

    public GroupDataSource(DataSource readDataSource, DataSource writeDataSource) 
        this.readDataSource = readDataSource;
        this.writeDataSource = writeDataSource;
        this.readWriteStrategy = new LocalContextReadWriteStrategy();
    

    public GroupDataSource(DataSource readDataSource, DataSource writeDataSource, ReadWriteStrategy readWriteStrategy) 
        this.readDataSource = readDataSource;
        this.writeDataSource = writeDataSource;
        this.readWriteStrategy = readWriteStrategy;
    

    @Override
    public Connection getConnection() throws SQLException 
        return getConnection(null, null);
    

    @Override
    public Connection getConnection(final String username, final String password) throws SQLException 
        return getConnectionInternal(username, password);
    

    private GroupConnection getConnectionInternal(String username, String password) throws SQLException 

        return new GroupConnection(readDataSource, writeDataSource, readWriteStrategy);
    

里面的读写数据源的配置其实是项目中已经初始化好的druid数据源。

 

碰到问题的sql语句:

 


<insert id="insertOnDuplicateUpdateBatch" keyColumn="id" keyProperty="id" parameterType="xxxx.RecommendHistory" useGeneratedKeys="true">
    insert into xxxx_recommend_history (recommend_id, info_id, recommend_type)
    values
    <foreach collection="list" item="history" separator=",">
      (#history.recommendId,jdbcType=INTEGER, #history.infoId,jdbcType=VARCHAR,
      #history.recommendType,jdbcType=INTEGER)
    </foreach>
    ON duplicate KEY UPDATE
    recommend_id=VALUES(recommend_id)
  </insert>

sql语句的重点是用了useGeneratedKeys="true", 和 ON duplicate KEY UPDATE

 

 

3、问题原因初步描述

 

代码异常抛出处:

 

从上面的代码看出,数据库返回的结果比参数多,从上面的sql语句看,useGeneratedKeys="true"配置,需要从结果中拿到自增的主键值,赋值到我们的java类属性中keyProperty="id",只需要一个值就行,而

数据库返回的结果有多个,debug下返回两个结果。

 

我们看下INSERT ... ON DUPLICATE KEY UPDATE Statement 的特殊性:https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html

If you specify an ON DUPLICATE KEY UPDATE clause and a row to be inserted would cause a duplicate value in a UNIQUE index or PRIMARY KEY, an UPDATE of the old row occurs. 

 

With ON DUPLICATE KEY UPDATE, the affected-rows value per row is 1 if the row is inserted as a new row, 2 if an existing row is updated, and 0 if an existing row is set to its current values. If you specify the CLIENT_FOUND_ROWS flag to the mysql_real_connect() C API function when connecting to mysqld, the affected-rows value is 1 (not 0) if an existing row is set to its current values.

If a table contains an AUTO_INCREMENT column and INSERT ... ON DUPLICATE KEY UPDATE inserts or updates a row, the LAST_INSERT_ID() function returns the AUTO_INCREMENT value.

大致意思是:当执行这个sql语句时,根据唯一索引(UNIQUE index)或唯一主键(PRIMARY KEY)来判断插入的值是否为重复数据,不重复执行插入数据,返回的受影响的行值为1,如果重复就执行更新,返回的受影响的行值为2. 如果更新的数据和原数据源一样,则返回的受影响的行值为0,表示数据库里的值没改变。

如果表的主键是自增列(AUTO_INCREMENT column), LAST_INSERT_ID() 函数返回自增值,mysql驱动也由com.mysql.cj.protocol.Resultset#getUpdateID 获取此值。

 

4、debug,对比引入读写分离之前和之后的执行流程

 

 

debug过程不说了,说下debug的结果点吧。。

 

mybatis执行到最后最终是要执行mysql驱动的一些方法的:org.apache.ibatis.executor.statement.PreparedStatementHandler

 

public class PreparedStatementHandler extends BaseStatementHandler 

  public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) 
    super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
  

  @Override
  public int update(Statement statement) throws SQLException 
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  

  @Override
  public void batch(Statement statement) throws SQLException 
    PreparedStatement ps = (PreparedStatement) statement;
    ps.addBatch();
  

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException 
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  

  @Override
  public <E> Cursor<E> queryCursor(Statement statement) throws SQLException 
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleCursorResultSets(ps);
  

  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException 
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) 
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) 
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
       else 
        return connection.prepareStatement(sql, keyColumnNames);
      
     else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) 
      return connection.prepareStatement(sql);
     else 
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    
  

  @Override
  public void parameterize(Statement statement) throws SQLException 
    parameterHandler.setParameters((PreparedStatement) statement);
  

 

除了批量操作为,执行更新,查询都是用的java.sql.PreparedStatement#execute 此方法,

对比druid和读写分离对这个方法的最终执行是什么样的:

druid:

 

读写分离组件:

 

 

 

确实,最终调用的mysql驱动的执行方法上是有区别的。

我们看看区别描述:

 

 

java.sql.PreparedStatement#execute 的大致功能:全能啊,什么sql语句都可以执行,有些结果返回多行也可以处理,不正是INSERT ... ON DUPLICATE KEY UPDATE Statement 有返回多个结果的例子吗。

 

5、mysql驱动源码是如何做的

 

既然找到了表像原因,我们只能根据源码查看最终原因是什么了(我也想什么都知道根本原因,可时间不允许啊)

com.mysql.cj.jdbc.ClientPreparedStatement#execute

 

 

 

 

 

 

com.mysql.cj.jdbc.ClientPreparedStatement#execute 的执行过程中对这个INSERT ... ON DUPLICATE KEY UPDATE Statement 做了判断,会对结果

 

具体的id处理过程不再复数了,debug之后我也记不住了。

 

6、总结

 

看来mysql的驱动,对sql的执行提供了几个方法,而每个方法的功能可能有局限性,有的是万能的,有的是针对普遍的sql(而非特殊的)实现的。

 

 

 

 

以上是关于There are only 1 target objects. You either specified a wrong ‘keyProperty‘ or encountered a driver的主要内容,如果未能解决你的问题,请参考以下文章

There are only 1 target objects. You either specified a wrong ‘keyProperty‘ or encountered a driver

运行时候报异常could only be replicated to 0 nodes instead of minReplication (=1). There are 2 datanode(s) r

pods 报错There may only be up to 1 unique SWIFT_VERSION per target

我的Android进阶之旅解决编译报错:Invoke-customs are only supported starting with Android O (--min-api 26)

我的Android进阶之旅解决编译报错:Invoke-customs are only supported starting with Android O (--min-api 26)

Kotlin的报错提示:Invoke-customs are only supported starting with Android O (--min-api 26)