开发踩坑记: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自动维护;
愿你我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!