开发踩坑记:MyBatis selectKey、空 SQL
最近整理了一些开发过程中遇到的典型问题,涉及 MyBatis 动态 SQL、
selectKey使用、MySQL 时间字段。本文把这些零散笔记整理成一篇博客,方便后续查阅,也希望能帮到遇到类似问题的同学。
一、MyBatis selectKey 报错:No setter found for the keyProperty 'count'
在使用 MyBatis 的 <selectKey> 时,可能会遇到这样的错误:
text
No setter found for the keyProperty 'count'
示例代码:
xml
<selectKey keyProperty="count" resultType="int" order="BEFORE">
select count(*) from company_sign_type_relation where sign_id = #{signId}
</selectKey>
原因
<selectKey> 会把查询结果放到 keyProperty 指定的属性中。这里指定了:
xml
keyProperty="count"
但传入的 PO 对象中没有 count 字段,或者没有对应的 setter 方法,于是 MyBatis 反射注入失败。
解决
在 PO 对象中添加 count 属性,并提供 getter / setter:
java
private Integer count;
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
selectKey 属性说明
keyProperty:对应 PO 对象中的字段名。order:BEFORE/AFTER,表示<selectKey>中的 SQL 在主 SQL 执行之前还是之后执行。resultType:keyProperty对应字段的类型。
二、MyBatis 动态 SQL 报错:Query was empty
另一个常见错误是:
text
MySQLSyntaxErrorException: Query was empty
示例:
xml
<insert id="saveBatchByUniqueKey">
<selectKey keyProperty="count" resultType="int" order="BEFORE">
select count(*) from company_sign_type_relation where sign_id = #{signId}
</selectKey>
<if test="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 中增加兜底分支
例如:
xml
<insert id="saveBatchByUniqueKey">
<selectKey keyProperty="count" resultType="int" order="BEFORE">
select count(*) from company_sign_type_relation where sign_id = #{signId}
</selectKey>
<if test="count==0">
insert into company_sign_type_relation (
company_type_id,
company_id,
sign_id
)
values (
#{companyTypeId},
#{companyId},
#{signId}
)
</if>
<if test="count > 0">
select NOW();
</if>
</insert>
这种方式可以避免空 SQL,但实际开发中更推荐把业务判断放到 Service 层,或者直接使用数据库的原子 upsert。
三、saveOrUpdate 的 MyBatis 实现与并发风险
类似下面这种"先 count,再 insert 或 update"的写法很常见:
xml
<insert id="saveOrUpdateByEquipPassTeam">
<selectKey keyProperty="count" resultType="int" order="BEFORE">
select count(*) from iot_attendance_record_info
where device_no = #{deviceNo} and pass_time = #{passTime}
</selectKey>
<if test="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>
<if test="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,导致重复数据。
更稳妥的方案
给业务唯一键加数据库唯一索引,然后使用:
sql
INSERT INTO ... ON DUPLICATE KEY UPDATE ...
或者使用其他数据库层面的原子 upsert 语法。这样可以避免并发下的重复插入问题。
四、MySQL 时间字段自动维护
建表时经常使用:
sql
`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间'
这样可以实现:
- 插入时自动填充
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自动维护;
愿你我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!