news 2026/10/15 17:03:13

mybatis-keyproperty-error_illustrated

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
mybatis-keyproperty-error_illustrated

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 staticprivate 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 配置快得多。


六、排查清单

遇到这个报错,四步走:

  1. 确认报错阶段:No setter found for the keyProperty属于"结果回填",说明 SQL 已经执行过了,问题在参数对象上。
  2. 列出类的可写属性:Arrays.toString(MetaClass.forClass(XxxDto.class).getSetterNames()),看目标属性在不在列表里。
  3. 交叉验证字节码:javap -p -classpath target/classes com.xxx.XxxDto | grep setId,同时确认编译产物是不是最新的。
  4. 确认自己到底需不需要这个 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
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 17:02:00

DBC文件转C代码:车载ECU中CAN协议解析的工程化实践

简介&#xff1a;本资源是一款面向汽车电子与嵌入式开发工程师的DBC文件自动化处理工具&#xff0c;专为CAN总线通信中频繁出现的DBC解析需求设计。手动将DBC协议定义转换为C语言结构体、编解码函数易出错且耗时&#xff0c;该工具通过Python脚本实现一键生成标准化C代码&#…

作者头像 李华
网站建设 2026/10/15 17:01:46

ROS架构详解与Ubuntu 20.04环境搭建实操指南

从零折腾ROS&#xff0c;第一道门槛不是代码&#xff0c;而是搞懂它到底是个什么东西。不少朋友一开始就把ROS当成一个"装上就能用的操作系统"&#xff0c;结果装完才发现&#xff0c;它更像是一个跑在Linux上的通信框架和工具集。我在刚开始接触ROS的时候&#xff0…

作者头像 李华
网站建设 2026/10/15 16:59:47

Skill架构:结构化能力接口的五种形态与四大设计准则

1. 什么是Skill&#xff1f;——从工具链到能力单元的范式迁移“Skill的形态有哪些&#xff1f;什么样的Skill叫做好Skill&#xff1f;”——这个问题乍看像在问一个新造词&#xff0c;其实它正切中当下技术演进最真实的脉搏。过去三年&#xff0c;我参与过17个不同行业的自动化…

作者头像 李华
网站建设 2026/10/15 16:59:43

基于python + vue企业人事管理系统(源码+数据库+文档)

企业人事管理系统 目录 基于python vue企业人事管理系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于python vue企业人事管理系统 一、前言 博主介绍&#xff…

作者头像 李华
网站建设 2026/10/15 16:59:30

树莓派双线寻迹:OpenCV视觉定位+L298N差速转向实战

简介&#xff1a;本资源是一份基于树莓派3B四轮驱动小车的双线寻迹项目设计报告&#xff0c;面向嵌入式开发初学者、机器人爱好者及高校自动化/人工智能方向实践者&#xff0c;聚焦视觉导航基础能力训练&#xff0c;解决单板计算机环境下OpenCV图像处理与电机运动协同控制的实际…

作者头像 李华
网站建设 2026/10/15 16:59:26

STM32F407驱动DS18B20单总线温度传感器:时序与避坑指南

简介&#xff1a;一份基于STM32F407与DS18B20温度传感器的完整嵌入式实践工程&#xff0c;面向物联网、智能家居及温度监测场景的开发者&#xff0c;重点演示单总线通信协议、GPIO开漏模式配置以及TFT图形界面&#xff08;GUI&#xff09;显示温度数据。包内共248个文件&#x…

作者头像 李华