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,及动态数据源切换功能。
为了方便的自动读写分离,使用了Zebra(https://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 aUNIQUE
index orPRIMARY KEY
, anUPDATE
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 theCLIENT_FOUND_ROWS
flag to themysql_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 andINSERT ... ON DUPLICATE KEY UPDATE
inserts or updates a row, theLAST_INSERT_ID()
function returns theAUTO_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)