ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

开发踩坑记:MyBatis selectKey、空 SQL

开发踩坑记:MyBatis selectKey、空 SQL 开发踩坑记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示例代码selectKeykeyPropertycountresultTypeintorderBEFOREselect count(*) from company_sign_type_relation where sign_id #{signId}/selectKey原因selectKey会把查询结果放到keyProperty指定的属性中。这里指定了keyPropertycount但传入的 PO 对象中没有count字段或者没有对应的 setter 方法于是 MyBatis 反射注入失败。解决在 PO 对象中添加count属性并提供 getter / setterprivateIntegercount;publicIntegergetCount(){returncount;}publicvoidsetCount(Integercount){this.countcount;}selectKey 属性说明keyProperty对应 PO 对象中的字段名。orderBEFORE/AFTER表示selectKey中的 SQL 在主 SQL 执行之前还是之后执行。resultTypekeyProperty对应字段的类型。二、MyBatis 动态 SQL 报错Query was empty另一个常见错误是MySQLSyntaxErrorException: Query was empty示例insertidsaveBatchByUniqueKeyselectKeykeyPropertycountresultTypeintorderBEFOREselect count(*) from company_sign_type_relation where sign_id #{signId}/selectKeyiftestcount0insert 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 中增加兜底分支例如insertidsaveBatchByUniqueKeyselectKeykeyPropertycountresultTypeintorderBEFOREselect count(*) from company_sign_type_relation where sign_id #{signId}/selectKeyiftestcount0insert into company_sign_type_relation ( company_type_id, company_id, sign_id ) values ( #{companyTypeId}, #{companyId}, #{signId} )/ififtestcount 0select NOW();/if/insert这种方式可以避免空 SQL但实际开发中更推荐把业务判断放到 Service 层或者直接使用数据库的原子 upsert。三、saveOrUpdate 的 MyBatis 实现与并发风险类似下面这种“先 count再 insert 或 update”的写法很常见insertidsaveOrUpdateByEquipPassTeamselectKeykeyPropertycountresultTypeintorderBEFOREselect count(*) from iot_attendance_record_info where device_no #{deviceNo} and pass_time #{passTime}/selectKeyiftestcount0insert 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} )/ififtestcount 0update iot_attendance_record_infosetdevice_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}/setwhere device_no #{deviceNo} and pass_time #{passTime}/if/insert潜在问题这种“先查后写”的方式不是原子操作。在高并发场景下两个请求可能同时查到count 0然后都执行 insert导致重复数据。更稳妥的方案给业务唯一键加数据库唯一索引然后使用INSERTINTO...ONDUPLICATEKEYUPDATE...或者使用其他数据库层面的原子 upsert 语法。这样可以避免并发下的重复插入问题。四、MySQL 时间字段自动维护建表时经常使用created_timedatetimeDEFAULTCURRENT_TIMESTAMPCOMMENT创建时间,updated_timedatetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT修改时间这样可以实现插入时自动填充created_time更新时自动刷新updated_time。不过要注意如果业务代码里显式传入了这两个字段可能会覆盖数据库的默认行为。需要根据实际场景决定是否由数据库维护时间。总结本文整理了几个开发中容易踩坑的点MyBatisselectKey的keyProperty必须有对应 setter动态 SQL 条件不成立时可能生成空 SQL导致Query was empty先 count 再 insert/update 存在并发重复插入风险推荐唯一索引 原子 upsertMySQL 时间字段可以用DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP自动维护愿你我都能在各自的领域里不断成长勇敢追求梦想同时也保持对世界的好奇与善意!
返回列表