MyBatis的“明明写了SQL却不执行”——#{}和${}的区别、返回null的坑、分页插件失效

5 阅读3分钟

9年Java开发,MyBatis用了6年。有些坑你看日志明明SQL执行了,但结果就是不对。今天聊三个让我加班到凌晨的MyBatis陷阱: #{}和${}的区别查询返回null的诡异场景分页插件莫名其妙失效


一、#{}=预编译,${}=字符串拼接——选错了就是SQL注入

场景1:用${}传参数,线上被注入了

xml

<!-- ❌ 错误:用${}传用户输入 -->
<select id="getUserByName" resultType="User">
    SELECT * FROM user WHERE name = '${name}'
</select>

攻击示例:

text

name = "张三' OR '1'='1"

最终SQL变成:

sql

SELECT * FROM user WHERE name = '张三' OR '1'='1'  -- 返回所有用户!

场景2:用#{}预编译,安全

xml

<!-- ✅ 正确:用#{}传参数 -->
<select id="getUserByName" resultType="User">
    SELECT * FROM user WHERE name = #{name}
</select>

生成的SQL是预编译的:

sql

SELECT * FROM user WHERE name = ?

参数被安全转义,SQL注入无效。

什么时候用${}?

只有这3种场景才用${}:

xml

<!-- 1. 动态表名/列名 -->
<select id="getUserByDynamicColumn" resultType="User">
    SELECT * FROM user ORDER BY ${columnName}
</select>

<!-- 2. 动态group by/having -->
<select id="groupByUser" resultType="User">
    SELECT ${groupColumn}, COUNT(*) FROM user GROUP BY ${groupColumn}
</select>

<!-- 3. 数据库函数/关键字 -->
<select id="getUserByTime" resultType="User">
    SELECT * FROM user WHERE ${dateFunction}(create_time) = #{date}
</select>

规则:用户输入用#{},代码控制用${}。


二、查询返回null的3个诡异场景

场景1:返回null还是空集合?

java

// DAO方法
User selectById(Long id);  // 查不到返回null
List<User> selectList();    // 查不到返回空集合(不是null)

踩坑现场:

java

User user = userMapper.selectById(999L);
if (user != null) {  // 必须判空,否则NPE
    System.out.println(user.getName());
}

List<User> list = userMapper.selectList();
for (User u : list) {  // 不需要判null,直接遍历
    // 但如果是null就会NPE
}

解决方案:

java

// 方案1:统一返回Optional
Optional<User> selectById(Long id);

// 方案2:判空兜底
User user = userMapper.selectById(id);
user = user == null ? new User() : user;

场景2:字段名映射不上,返回null

xml

<!-- 实体类字段:userName -->
<!-- 数据库字段:user_name -->
<select id="selectUser" resultType="User">
    SELECT user_name FROM user  <!-- ❌ 映射不上,userName=null -->
</select>

解决方案:

xml

<!-- 方案1:使用resultMap -->
<resultMap id="UserMap" type="User">
    <result column="user_name" property="userName"/>
</resultMap>

<!-- 方案2:SQL起别名 -->
<select id="selectUser" resultType="User">
    SELECT user_name as userName FROM user
</select>

<!-- 方案3:开启驼峰映射(application.yml) -->
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>

场景3:MyBatis返回null,但数据库有值

现象:  日志打印的SQL在数据库执行有结果,但Java拿到的是null。

排查清单:

java

// 1. 检查resultType是否正确
<select id="selectUser" resultType="User">  // User类存在吗?

// 2. 检查是否用了错误的数据类型
// ❌ 数据库bigint,Java用Integer,超过范围返回null
private Long id;  // ✅ 用Long

// 3. 检查getter/setter
// Lombok @Data 或 手写getter/setter

// 4. 检查TypeHandler
// 自定义类型需要注册TypeHandler

三、分页插件失效的4个场景

场景1:PageHelper.startPage后跟了多个查询

java

// ❌ 错误:startPage只对第一个查询生效
PageHelper.startPage(1, 10);
List<User> userList = userMapper.selectList();     // 分页生效
List<Order> orderList = orderMapper.selectList();  // 分页也生效?不对!

正确用法:

java

PageHelper.startPage(1, 10);
List<User> userList = userMapper.selectList();  // 只分页这个
PageInfo<User> pageInfo = new PageInfo<>(userList);

场景2:startPage后执行了非查询方法

java

// ❌ 错误:startPage后做了其他操作
PageHelper.startPage(1, 10);
userMapper.updateUser(user);  // 分页拦截器会拦截这个update,混乱
List<User> list = userMapper.selectList();  // 分页可能失效

正确:startPage后立即紧跟要分页的查询。


场景3:多线程环境下使用PageHelper

java

// ❌ 错误:多线程共用同一个ThreadLocal
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
    PageHelper.startPage(1, 10);
    userMapper.selectList();  // 可能拿到别的线程的分页参数
});

PageHelper基于ThreadLocal实现,多线程环境要用PageMethod:

java

// ✅ 正确:每个线程独立
PageHelper.startPage(1, 10);  // 主线程
List<User> list = PageHelper.startPage(1, 10)
    .doSelectPage(() -> userMapper.selectList());

场景4:嵌套查询导致分页count不对

xml

<!-- 复杂SQL:group by + join -->
<select id="selectComplexList" resultType="User">
    SELECT u.*, COUNT(o.id) as orderCount
    FROM user u
    LEFT JOIN order o ON u.id = o.user_id
    GROUP BY u.id
</select>

问题:  分页插件的count查询是自动生成的,复杂SQL可能生成错误的count语句。

解决方案:

java

// 方案1:手动写count查询
@SelectProvider(type = UserSqlProvider.class, method = "countSql")
long countByCondition(Map<String, Object> params);

// 方案2:使用PageHelper的count查询参数
PageHelper.startPage(1, 10, true);  // 开启count
PageHelper.startPage(1, 10, false); // 关闭count,自己处理

四、其他高频踩坑点

坑1:参数名写错导致传入null

java

// DAO
User selectByNameAndAge(@Param("name") String name, 
                         @Param("age") Integer age);

xml

<!-- ❌ 错误 -->
<select id="selectByNameAndAge">
    SELECT * FROM user WHERE name = #{userName}  <!-- 传参是name,不是userName -->
</select>

解决方案:  参数名必须和@Param一致,或者直接用#{0},#{1}(不推荐)。


坑2:模糊查询like的写法

xml

<!-- ❌ 错误:这样查不到 -->
<select id="searchByName">
    SELECT * FROM user WHERE name LIKE '%#{name}%'
</select>

<!-- ✅ 正确:用concat或bind -->
<select id="searchByName">
    SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%')
</select>

<!-- 或使用bind -->
<select id="searchByName">
    <bind name="pattern" value="'%' + name + '%'"/>
    SELECT * FROM user WHERE name LIKE #{pattern}
</select>

坑3:批量插入性能问题

xml

<!-- ❌ 错误:循环单条插入 -->
<insert id="insertOne">INSERT INTO user VALUES(...)</insert>
<!-- Java循环调用1000次 -->

<!-- ✅ 正确:批量插入 -->
<insert id="batchInsert">
    INSERT INTO user (name, age) VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.name}, #{item.age})
    </foreach>
</insert>

五、总结速查表

陷阱错误用法正确姿势
SQL注入风险${用户输入}#{}预编译
返回null不判空直接调用if(user!=null) 或返回Optional
字段映射失败数据库user_name,实体userName开启驼峰映射或用resultMap
分页失效startPage后跟多个查询紧跟一个查询
多线程分页多个线程共用一个startPage每个线程独立或用doSelectPage
like查询'%#{name}%'CONCAT('%',#{name},'%')
批量操作循环单条插入foreach批量插入

六、互动一下

你因为#{}和${}搞错过吗?线上出过SQL注入事故吗?

分页插件失效让你加班到几点?

评论区见👇


下期预告:  避坑4——多线程的“我以为线程安全”(SimpleDateFormat、ArrayList、HashMap、双重检查锁)


我是小X,9年Java,产假中持续输出。点个赞,收藏防丢❤️