MyBatis 报 No setter found for the keyProperty ‘id’:useGeneratedKeys 要回填主键,DTO 里却没有 id 字段
关键词:MyBatis、useGeneratedKeys、keyProperty、No setter found for the keyProperty、Jdbc3KeyGenerator、@Data
一、问题现场
<insertid="insertDetail"parameterType="com.xxx.dto.DetailDto"useGeneratedKeys="true"keyProperty="id">INSERT INTO detail (prod_id, prod_name, update_time) VALUES (#{prodId}, #{prodName}, #{updateTime})</insert>@DatapublicclassDetailDto{privateStringprodId;privateStringprodName;privateDateupdateTime;// id 是数据库自增的,所以这里没有 id 字段}一执行 insert 就报:
Cause: org.apache.ibatis.executor.ExecutorException: No setter found for the keyProperty 'id' in 'com.xxx.dto.DetailDto'.二、原因
useGeneratedKeys干的事情是:INSERT 执行完之后,把数据库生成的主键回填到参数对象的id属性上。
而DetailDto里既没有id字段,也没有setId方法,回填无处落脚,于是抛异常。
@Data只给"类里声明过的字段"生成 getter/setter,不会凭空造字段,加@Setter也是同样的报错。
为什么 SQL 都执行成功了,还抛异常?
因为回填发生在ps.execute()之后:
// PreparedStatementHandlerpublicintupdate(Statementstatement)throwsSQLException{PreparedStatementps=(PreparedStatement)statement;ps.execute();introws=ps.getUpdateCount();ObjectparameterObject=boundSql.getParameterObject();KeyGeneratorkeyGenerator=mappedStatement.getKeyGenerator();keyGenerator.processAfter(executor,mappedStatement,ps,parameterObject);// 异常从这一行抛出returnrows;}▲ 图 1:主键回填链路 —— 回填发生在
ps.execute()之后,所以 SQL 已经落库*
- 外层没有事务(autocommit):这一行数据已经落库了,但接口给你报错 —— 脏数据的来源之一;
- 外层有 Spring 事务:异常会触发回滚,表里干净,但你会因为"库里明明没数据,却报了个 setter 的错"而更加困惑。
三、源码:MyBatis 怎么判断"有没有 setter"
KeyAssigner.assign()里调metaParam.hasSetter("id"),最终落到Reflector:
publicbooleanhasSetter(StringpropertyName){returnsetMethods.containsKey(propertyName);}而setMethods里除了setXxx方法,Reflector.addFields()还会把字段本身注册进去(沿父类链扫描):
for(Fieldfield:clazz.getDeclaredFields()){if(!setMethods.containsKey(field.getName())){intmodifiers=field.getModifiers();if(!Modifier.isFinal(modifiers)||!Modifier.isStatic(modifiers)){addSetField(field);// 字段本身被当成"可写属性"}}}if(clazz.getSuperclass()!=null){addFields(clazz.getSuperclass());}所以hasSetter为 true 的条件,满足任一即可:
| 条件 | 例子 |
|---|---|
有setId方法(含父类继承) | public void setId(Long id) |
有id字段,且非final static | private Long id;即使没有任何 setter |
由此可以反推出这个报错的唯一含义:
这个类和它所有父类里,既没有
setId方法,也没有id字段。
不用再去怀疑 Lombok 配置、IDEA 注解处理器或 jar 版本 —— 就是字段不存在。
▲ 图 2:hasSetter的两条判定路径,满足任一即为 true
四、怎么改
先判断一件事:插入之后你还会不会用到这个新 id?
不需要 → 删掉两个属性
<insertid="insertDetail"parameterType="com.xxx.dto.DetailDto">INSERT INTO detail (prod_id, prod_name, update_time) VALUES (#{prodId}, #{prodName}, #{updateTime})</insert>SQL 和 DTO 一行都不用动,报错立刻消失。代价是以后拿不到自增主键,需要时得再查一次库。
需要 → 给 DTO 加字段
@DatapublicclassDetailDto{privateLongid;// 只是接住回填的主键值privateStringprodId;privateStringprodName;privateDateupdateTime;}这里要澄清一个常见误解:加id字段不会把 id 插进数据库。
SQL 内容只由 XML 里有没有#{id}占位符决定,加字段前后 SQL 完全一样:
INSERTINTOdetail(prod_id,prod_name,update_time)VALUES(?,?,?)keyProperty管的是"读回来",不是"写进去";自增依然由数据库负责。
如果觉得"更新用"的 DTO 不该长出一个id字段,那就拆成DetailInsertDto(带 id,用于插入)和DetailUpdateDto(不带,用于按 prodId 更新)—— 一个 DTO 承担两种职责,才是这个问题的根子。
▲ 图 3:修复决策树 —— 按"插入后是否需要这个新 id"分两条路
五、三行代码验证
MetaClassmc=MetaClass.forClass(DetailDto.class);System.out.println(mc.hasSetter("id"));// falseSystem.out.println(Arrays.toString(mc.getSetterNames()));// [prodId, prodName, updateTime]getSetterNames()会列出 MyBatis 眼里的全部可写属性,一眼就能看出id不在里面,比翻 Lombok 配置快得多。
六、排查清单
遇到这个报错,四步走:
- 确认报错阶段:
No setter found for the keyProperty属于"结果回填",说明 SQL 已经执行过了,问题在参数对象上。 - 列出类的可写属性:
Arrays.toString(MetaClass.forClass(XxxDto.class).getSetterNames()),看目标属性在不在列表里。 - 交叉验证字节码:
javap -p -classpath target/classes com.xxx.XxxDto | grep setId,同时确认编译产物是不是最新的。 - 确认自己到底需不需要这个 id:不需要就删掉
useGeneratedKeys+keyProperty;需要就补字段。两者都不影响 INSERT SQL 的写法。
▲ 图 4:四步排查清单
七、几个顺手的提醒
useGeneratedKeys只该配在 INSERT 上,在update/delete上看到就直接删。- 多参数(或有任一参数带
@Param)时,keyProperty必须带参数名前缀,如keyProperty="dto.id"。 - 老版本不报这个错:3.4.x 及以前只有一句
if (hasSetter(...))判断,不满足就静默跳过 —— insert 成功但id一直是null;升级到 3.5.x 才变成显式异常。所以"同样的代码同事跑着没事",八成是版本不同。 keyProperty是 MyBatis 少数"配了却不做启动校验"的属性,只有真正执行到那条 insert 才会暴露,单测记得覆盖插入分支。
▲ 图 5:同一份代码,MyBatis 3.4.x 静默失效 vs 3.5.x 显式抛异常
参考
- MyBatis 源码
org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator(KeyAssigner.assign中抛出该异常) - MyBatis 源码
org.apache.ibatis.executor.statement.PreparedStatementHandler - MyBatis 源码
org.apache.ibatis.reflection.Reflector(addFields/addSetField/hasSetter) - MyBatis 源码
org.apache.ibatis.reflection.MetaClass、org.apache.ibatis.reflection.invoker.SetFieldInvoker