☰
开发踩坑记:MyBatis selectKey、空 SQL
2026/9/30 8:06:16 网站建设 项目流程

开发踩坑记:MyBatis selectKey、空 SQL

最近整理了一些开发过程中遇到的典型问题,涉及 MyBatis 动态 SQL、selectKey使用、MySQL 时间字段。本文把这些零散笔记整理成一篇博客,方便后续查阅,也希望能帮到遇到类似问题的同学。


一、MyBatis selectKey 报错:No setter found for the keyProperty ‘count’

在使用 MyBatis 的<selectKey>时,可能会遇到这样的错误:

No setter found for the keyProperty 'count'

示例代码:

<selectKeykeyProperty="count"resultType="int"order="BEFORE">select count(*) from company_sign_type_relation where sign_id = #{signId}</selectKey>

原因

<selectKey>会把查询结果放到keyProperty指定的属性中。这里指定了:

keyProperty="count"

但传入的 PO 对象中没有count字段,或者没有对应的 setter 方法,于是 MyBatis 反射注入失败。

解决

在 PO 对象中添加count属性,并提供 getter / setter:

privateIntegercount;publicIntegergetCount(){returncount;}publicvoidsetCount(Integercount){this.count=count;}

selectKey 属性说明

  • keyProperty:对应 PO 对象中的字段名。
  • order:BEFORE/AFTER,表示<selectKey>中的 SQL 在主 SQL 执行之前还是之后执行。
  • resultType:keyProperty对应字段的类型。

二、MyBatis 动态 SQL 报错:Query was empty

另一个常见错误是:

MySQLSyntaxErrorException: Query was empty

示例:

<insertid="saveBatchByUniqueKey"><selectKeykeyProperty="count"resultType="int"order="BEFORE">select count(*) from company_sign_type_relation where sign_id = #{signId}</selectKey><iftest="count==0">insert into company_sign_type_relation ( company_type_id, company_id, sign_id ) values ( #{companyTypeId}, #{companyId}, #{signId} )</if></insert>

原因

当count == 0不成立时,<if>里面的 SQL 不会拼接。最终 MyBatis 没有生成任何可执行 SQL,于是向数据库发送了空语句,导致Query was empty。

解决方式

方式一:把条件判断放到 Service 层

在 Service 层先查询是否存在,再决定是否调用 insert。这样 SQL 层只负责单一职责,逻辑更清晰。

方式二:在 SQL 中增加兜底分支

例如:

<insertid="saveBatchByUniqueKey"><selectKeykeyProperty="count"resultType="int"order="BEFORE">select count(*) from company_sign_type_relation where sign_id = #{signId}</selectKey><iftest="count==0">insert into company_sign_type_relation ( company_type_id, company_id, sign_id ) values ( #{companyTypeId}, #{companyId}, #{signId} )</if><iftest="count > 0">select NOW();</if></insert>

这种方式可以避免空 SQL,但实际开发中更推荐把业务判断放到 Service 层,或者直接使用数据库的原子 upsert。


三、saveOrUpdate 的 MyBatis 实现与并发风险

类似下面这种“先 count,再 insert 或 update”的写法很常见:

<insertid="saveOrUpdateByEquipPassTeam"><selectKeykeyProperty="count"resultType="int"order="BEFORE">select count(*) from iot_attendance_record_info where device_no = #{deviceNo} and pass_time = #{passTime}</selectKey><iftest="count==0">insert into iot_attendance_record_info ( device_no, brand_id, direction, pass_time, pass_date, person_id, person_primary_id, person_name, id_number, phone_number, group_leader_phone, is_group_leader, sex, age, sign_id, sign_name, company_id, company_name, company_type_id, company_type_name, type_work_id, type_work_name, team_id, team_name, group_leader_id, group_leader_name, boss_id, boss_name, boss_phone, create_time, update_time, is_deleted, person_oss_url, lj_person_id ) values ( #{deviceNo}, #{brandId}, #{direction}, #{passTime}, #{passDate}, #{personId}, #{personPrimaryId}, #{personName}, #{idNumber}, #{phoneNumber}, #{groupLeaderPhone}, #{isGroupLeader}, #{sex}, #{age}, #{signId}, #{signName}, #{companyId}, #{companyName}, #{companyTypeId}, #{companyTypeName}, #{typeWorkId}, #{typeWorkName}, #{teamId}, #{teamName}, #{groupLeaderId}, #{groupLeaderName}, #{bossId}, #{bossName}, #{bossPhone}, #{createTime}, #{updateTime}, #{isDeleted}, #{personOssUrl}, #{ljPersonId} )</if><iftest="count > 0">update iot_attendance_record_info<set>device_no = #{deviceNo}, brand_id = #{brandId}, direction = #{direction}, pass_time = #{passTime}, pass_date = #{passDate}, person_id = #{personId}, person_primary_id = #{personPrimaryId}, person_name = #{personName}, id_number = #{idNumber}, phone_number = #{phoneNumber}, group_leader_phone = #{groupLeaderPhone}, is_group_leader = #{isGroupLeader}, sex = #{sex}, age = #{age}, sign_id = #{signId}, sign_name = #{signName}, company_id = #{companyId}, company_name = #{companyName}, company_type_id = #{companyTypeId}, company_type_name = #{companyTypeName}, type_work_id = #{typeWorkId}, type_work_name = #{typeWorkName}, team_id = #{teamId}, team_name = #{teamName}, group_leader_id = #{groupLeaderId}, group_leader_name = #{groupLeaderName}, boss_id = #{bossId}, boss_name = #{bossName}, boss_phone = #{bossPhone}, create_time = #{createTime}, update_time = #{updateTime}, is_deleted = #{isDeleted}, person_oss_url = #{personOssUrl}, lj_person_id = #{ljPersonId}</set>where device_no = #{deviceNo} and pass_time = #{passTime}</if></insert>

潜在问题

这种“先查后写”的方式不是原子操作。在高并发场景下,两个请求可能同时查到count == 0,然后都执行 insert,导致重复数据。

更稳妥的方案

给业务唯一键加数据库唯一索引,然后使用:

INSERTINTO...ONDUPLICATEKEYUPDATE...

或者使用其他数据库层面的原子 upsert 语法。这样可以避免并发下的重复插入问题。


四、MySQL 时间字段自动维护

建表时经常使用:

`created_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`updated_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'修改时间'

这样可以实现:

  • 插入时自动填充created_time;
  • 更新时自动刷新updated_time。

不过要注意,如果业务代码里显式传入了这两个字段,可能会覆盖数据库的默认行为。需要根据实际场景决定是否由数据库维护时间。


总结

本文整理了几个开发中容易踩坑的点:

  • MyBatis<selectKey>的keyProperty必须有对应 setter;
  • 动态 SQL 条件不成立时可能生成空 SQL,导致Query was empty;
  • 先 count 再 insert/update 存在并发重复插入风险,推荐唯一索引 + 原子 upsert;
  • MySQL 时间字段可以用DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP自动维护;

愿你我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询