开发踩坑记:MyBatis selectKey、空 SQL

开发踩坑记: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 自动维护;

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

相关推荐
JosieBook1 小时前
【数据库】MySQL 实战精通系列 · 第6篇:InnoDB 存储引擎、日志与崩溃恢复
数据库·mysql
自强的小白2 小时前
核心功能(Service接口)
java·mybatis
杨云龙UP2 小时前
TDengine 3.4.2.8 Community 三节点三副本生产集群部署实战(DNode/MNode/taosAdapter/Explorer)
大数据·linux·运维·数据库·tdengine·时序库
梦帮科技2 小时前
领域认知知识库图谱注入:从双式记账图网络到高质量问答对自动化合成流水线
运维·网络·数据库·人工智能·矩阵·架构·自动化
Long long ago.2 小时前
vastbase数据库运行sql宕机重启解决
数据库·sql
SelectDB技术团队3 小时前
一条日志两套引擎的账:把 Elasticsearch 检索与分析合并到同一份数据的落地写法
大数据·数据库·elasticsearch·搜索引擎·全文检索·日志·apache doris
数据库百宝箱3 小时前
rum&gin索引对比
java·数据库·gin
wefg14 小时前
【Redis】初识 Redis
数据库·redis·缓存
hacker_LeeFei6 小时前
Windows 下配置 Redis 8.10.2 开机自启:一次踩坑全记录
数据库·windows·redis