🔥后端必看|MyBatis Mapper.xml 10个高频踩坑总结(真实踩坑经验分享🔍)

0 阅读3分钟

后端必看|MyBatis Mapper.xml 10个高频踩坑总结(附实战避坑指南)

大家好,我是一名后端开发,在日常项目中跟 MyBatis 打交道最多,而 mapper.xml 作为 SQL 编写的核心载体,看似简单,却藏着无数“隐形坑”——新手踩雷翻车,老手偶尔也会栽跟头。

结合近3年项目实战经验,我整理了 10个最常见、最致命mapper.xml 踩坑点,每个坑都包含「问题现象+错误代码+正确写法+避坑技巧」,全程实战向,看完直接避开 80% 的 MyBatis 报错,新手也能快速上手!

话不多说,直接上干货,建议收藏备用,遇到问题直接对照排查~

一、高频踩坑点(按报错频率排序,重点优先看)

坑1:XML 特殊字符未转义,项目启动直接报错

问题现象:项目启动失败,控制台抛出 XML 解析错误,常见提示:XML 解析错误The content of elements must consist of well-formed character data or markup

根本原因:XML 语法中,<>、&、'" 是特殊字符,直接写在 SQL 中会被 XML 解析器误判为标签语法,导致解析失败。

错误写法(新手最常犯):

<!-- 错误:直接使用 < 符号,XML 解析会报错 -->
<select id="selectYoungUser" resultType="com.demo.User">
    select * from user where age < 18
</select>

正确写法(两种方案,按需选择):

方案1:使用转义字符(推荐简单场景,如单个条件判断)

原字符转义后字符使用场景
<小于判断(age < 18)
大于判断(create_time > '2026-01-01')
逻辑与(name & age 联合查询)
<select id="selectYoungUser" resultType="com.demo.User">
    select * from user where age &lt; 18
</select>

方案2:用 包裹(推荐复杂条件,如多条件组合)

<select id="selectYoungUser" resultType="com.demo.User">
    select * from user 
    where <![CDATA[ age < 18 AND create_time > '2026-01-01' AND status = 1 ]]>
</select>

避坑技巧:只要 SQL 中出现特殊字符,直接用 CDATA 包裹,省心又不易出错。

坑2:#{ } 和 ${ } 混用,要么报错要么有注入风险

这是 MyBatis 新手最容易混淆的点,也是面试高频考点,记住:99% 的场景用 #{ },只有1% 的场景用 ${ }

问题现象

  • 场景1:传入字符串参数,报错 Unknown column 'xxx' in 'where clause'
  • 场景2:存在 SQL 注入风险(如传入恶意参数,篡改 SQL 逻辑)

核心区别(必记)

  • #{ }:预编译处理,自动给参数加引号,防 SQL 注入,推荐所有普通参数使用
  • ${ }:字符串直接拼接,不加引号,有注入风险,仅用于动态表名、动态字段名

错误写法

<!-- 错误:用 ${ } 传递普通字符串参数,会导致无引号报错 -->
<select id="selectByUsername" resultType="com.demo.User">
    select * from user where username = ${username}
</select>

正确写法

<!-- 普通参数:用 #{ }(自动加引号,防注入) -->
<select id="selectByUsername" resultType="com.demo.User">
    select * from user where username = #{username}
</select>

<!-- 动态表名/字段名:必须用 ${ }(无其他替代方案) -->
<select id="selectByTable" resultType="com.demo.User">
    -- 比如动态切换用户表、历史表
    select * from ${tableName} where id = #{id}
</select>

避坑技巧:写 SQL 时,先问自己“这是普通参数还是动态表/字段”,普通参数直接用 #{ },不用犹豫。

坑3:if 标签判断空值,忽略 null 或判断错误

动态 SQL 中 if 标签是高频使用场景,但判断空值的写法很容易出错,尤其是字符串和数字类型的区别。

问题现象:前端传空字符串、参数为 null 时,SQL 拼接错误,或查询/更新逻辑异常(比如本该过滤的条件没过滤,不该过滤的被过滤)。

错误写法1:只判断空字符串,没判断 null

<!-- 错误:当 username 为 null 时,该条件会被执行,导致 SQL 拼接错误 -->
<if test="username != ''">
    and username = #{username}
</if>

错误写法2:数字类型判断空字符串

<!-- 错误:Integer/Long 类型判断 != '',当值为 0 时会被误判为“空”,导致条件失效 -->
<if test="age != null and age != ''">
    and age = #{age}
</if>

正确写法

<!-- 1. 字符串类型:同时判断 null + 空字符串(标准写法) -->
<if test="username != null and username != ''">
    and username = #{username}
</if>

<!-- 2. 数字类型(Integer/Long/Byte):只判断 null,不要判断空字符串 -->
<if test="age != null">
    and age = #{age}
</if>

<!-- 3. 集合类型(List/Set):判断 null + 非空 -->
<if test="ids != null and ids.size() > 0">
    and id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</if>

坑4:where 标签使用不当,出现 “where and/or” 语法错误

动态拼接多条件时,手动写 where 关键字,很容易出现“where 后面直接跟 and/or”的语法错误,尤其是当第一个条件不满足时。

问题现象:执行 SQL 报错,提示 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'and'

错误写法

<!-- 错误:手动写 where,当第一个 if 条件不满足时,会变成 “where and age = ?” -->
<select id="selectUserList" resultType="com.demo.User">
    select * from user
    where
    <if test="username != null and username != ''">
        and username = #{username}
    </if>
    <if test="age != null">
        and age = #{age}
    </if>
</select>

正确写法:使用 MyBatis 自带的 标签,自动去除多余的 and/or,无需手动处理。

<select id="selectUserList" resultType="com.demo.User">
    select * from user
    <where>
        <if test="username != null and username != ''">
            and username = #{username}
        </if>
        <if test="age != null">
            and age = #{age}
        </if>
    </where>
</select>

补充技巧:如果不想用 标签,也可以在 where 后面加 1=1(兜底条件),但不如 简洁,不推荐。

坑5:update 语句没加 标签,出现多余逗号

更新数据时,动态拼接字段,很容易出现“最后一个字段后面多一个逗号”的语法错误,尤其是字段较多时。

问题现象:执行 update 语句报错,提示You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ','

错误写法

<!-- 错误:无 <set> 标签,当最后一个 if 条件满足时,会多出一个逗号 -->
<update id="updateUser">
    update user
    set
    <if test="username != null and username != ''">
        username = #{username},
    </if>
    <if test="age != null">
        age = #{age},
    </if>
    where id = #{id}
</update>

正确写法:使用 标签,自动去除最后一个字段后面的多余逗号,省心又安全。

<update id="updateUser">
    update user
    <set>
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
    </set>
    where id = #{id}
</update>

坑6:查询未配置 resultType/resultMap,数据映射失败

很多新手写完查询 SQL 后,会忘记配置返回值类型,导致查询结果无法封装到实体类中,出现 null 或字段不匹配的问题。

问题现象

  • 场景1:查询结果为 null,控制台无报错,但实体类属性全是默认值
  • 场景2:报错 No constructor found for com.demo.User
  • 场景3:字段名和实体类属性名不一致,部分字段值为 null

核心区别(必记)

  • resultType:简单映射,适用于「数据库字段名和实体类属性名完全一致」的场景
  • resultMap:复杂映射,适用于「字段名和属性名不一致」、「关联查询(一对一/一对多)」的场景

错误写法

<!-- 错误:忘记配置 resultType/resultMap,无法封装查询结果 -->
<select id="selectById" parameterType="Long">
    select * from user where id = #{id}
</select>

正确写法

<!-- 方案1:resultType(字段名和属性名完全一致) -->
<select id="selectById" parameterType="Long" resultType="com.demo.User">
    select * from user where id = #{id}
</select>

<!-- 方案2:resultMap(字段名和属性名不一致,比如数据库是 user_name,实体类是 username) -->
<resultMap id="UserResultMap" type="com.demo.User">
    <id column="id" property="id"/> <!-- 主键映射 -->
    <result column="user_name" property="username"/> <!-- 字段名和属性名不一致映射 -->
    <result column="user_age" property="age"/>
</resultMap>
<select id="selectById" parameterType="Long" resultMap="UserResultMap">
    select id, user_name, user_age from user where id = #{id}
</select>

坑7:模糊查询 like 写法错误,查询无结果或有注入风险

模糊查询是日常开发中高频需求,但 like 的写法很容易出错,要么查询不到数据,要么存在 SQL 注入风险。

错误写法(两种常见错误):

<!-- 错误1:直接拼接 %,语法错误,#{username} 会被当作字符串的一部分 -->
and username like '%#{username}%'

<!-- 错误2:用 ${ } 拼接,存在 SQL 注入风险 -->
and username like '%${username}%'

正确写法(推荐两种,优先选方案1):

<!-- 方案1:concat 函数(MySQL 通用,防注入,最推荐) -->
<if test="username != null and username != ''">
    and username like concat('%', #{username}, '%')
</if>

<!-- 方案2:bind 标签(跨数据库兼容,比如适配 MySQL、Oracle) -->
<if test="username != null and username != ''">
    <bind name="usernameLike" value="'%' + username + '%'"/>
    and username like #{usernameLike}
</if>

坑8:foreach 遍历集合,参数名错误导致报错

批量操作(批量查询、批量删除、批量插入)时,foreach 标签是必用的,但参数名写错是新手高频踩坑点。

问题现象:报错 Parameter 'list' not foundCollection is null,尤其是传入数组、Set 时。

核心坑点(必记):

  • 传入 数组(如 Long[] ids):默认参数名是 array
  • 传入 List(如 List ids):默认参数名是 list
  • 传入 Set(如 Set ids):默认参数名是 collection
  • 最稳妥方案:在 Mapper 接口中用@Param 自定义参数名,避免依赖默认名

错误写法

<!-- 错误:传入的是数组,却用 list 作为 collection 参数名 -->
<select id="selectByIds" resultType="com.demo.User">
    select * from user where id in
    <foreach collection="list" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

正确写法

<!-- 方案1:传入数组,用 array 作为参数名 -->
<select id="selectByIds" resultType="com.demo.User">
    select * from user where id in
    <foreach collection="array" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

<!-- 方案2:自定义参数名(最稳妥,推荐) -->
// Mapper 接口(添加 @Param 注解)
List<User> selectByIds(@Param("ids") Long[] ids);

// mapper.xml(用自定义的 ids 作为参数名)
<select id="selectByIds" resultType="com.demo.User">
    select * from user where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

坑9:SQL 末尾多加分号,导致多语句执行报错

这个坑很隐蔽,很多开发者习惯在 SQL 末尾加封号,但在 mapper.xml 中,分号会被 MyBatis 解析为 SQL 结束符,可能导致多语句执行报错。

问题现象:执行 SQL 报错,提示 Could not execute SQL statement; nested exception is java.sql.SQLSyntaxErrorException

错误写法

<!-- 错误:SQL 末尾多加分号 -->
<select id="selectAll" resultType="com.demo.User">
    select * from user;
</select>

正确写法:删除 SQL 末尾的分号,mapper.xml 中无需加分号。

<select id="selectAll" resultType="com.demo.User">
    select * from user
</select>

坑10:namespace 路径错误,Mapper 接口绑定失败

这是 MyBatis 最经典、最常见的报错,没有之一——很多新手排查半天,最后发现是 namespace 写错了。

问题现象:项目启动报错,提示 Invalid bound statement (not found),翻译过来就是“找不到绑定的 Mapper 方法”。

根本原因:mapper.xml 中的 namespace 属性,必须和 Mapper 接口的 全类名完全一致,大小写、包名、类名,错一个都不行。

错误写法

<!-- 错误:namespace 包名写错(少写了 mapper 包) -->
<mapper namespace="com.demo.UserMapper">

<!-- 错误:类名写错(UserMapper 写成了 UserMapper1) -->
<mapper namespace="com.demo.mapper.UserMapper1">

正确写法

<!-- 正确:namespace 与 Mapper 接口全类名完全一致 -->
<mapper namespace="com.demo.mapper.UserMapper">

避坑技巧:写 namespace 时,直接复制 Mapper 接口的全类名,避免手动输入出错。

二、通用避坑万能技巧(新手必背,直接落地)

掌握以下技巧,能避开 90% 的 mapper.xml 坑,大幅提升开发效率,减少调试时间:

  1. XML 特殊字符一律用 <![CDATA[ ]]> 包裹,不用记转义字符,省心又不易错;
  2. 参数传递优先用 #{ },只有动态表名、动态字段名才用 ${ },从根源避免注入风险;
  3. if 标签判断:字符串判 null + 空串,数字只判 null,集合判 null + 非空
  4. 动态查询用 <where> 标签,动态更新用<set> 标签,自动处理语法问题;
  5. 模糊查询优先用 concat('%', #{参数}, '%'),跨数据库用 <bind> 标签;
  6. foreach 遍历集合,优先用 @Param 自定义参数名,避免依赖默认参数名;
  7. SQL 末尾绝对不要加分号,避免多语句执行报错;
  8. 开启 MyBatis 日志,查看最终执行的 SQL,快速定位问题(关键技巧)。

附:application.yml 开启 MyBatis 日志配置(直接复制可用)

mybatis:
  configuration:
    # 开启日志(控制台打印执行的 SQL,方便调试)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  # 配置 mapper.xml 路径(避免找不到 mapper 文件)
  mapper-locations: classpath:mapper/**/*.xml

三、总结

mapper.xml 的坑,看似五花八门,实则有规律可循——大部分错误都集中在「XML 语法」「参数使用」「动态 SQL 标签」「映射配置」这4个方面。

记住核心原则:XML 语法要规范,参数传递用 #{ },条件拼接用 where/set,特殊字符要转义,日志开启好调试

本文整理的 10 个踩坑点,覆盖了日常开发中 90% 的 MyBatis 报错场景,每个坑都有具体的错误示例和正确写法,新手可以直接对照使用,老手可以用来查漏补缺。

如果觉得有用,欢迎点赞、收藏,也可以在评论区分享你遇到的 mapper.xml 踩坑经历,一起避坑成长~

最后,祝大家写 SQL 零报错,开发效率翻倍!🚀