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,产假中持续输出。点个赞,收藏防丢❤️