MyBatis的灵活性是一把双刃剑。本文将详细剖析我在使用MyBatis过程中遇到的几个典型深坑,包括参数传递的奥秘、动态SQL的性能陷阱、复杂结果映射的诡异行为,以及分页插件的原理误区。每个坑都配有详细的代码对比和原理分析。
坑1:#{}和${}的混淆与SQL注入
#{}: 是参数占位符。MyBatis会将其替换为?,然后使用PreparedStatement的set方法安全地设置参数。可以防止SQL注入。${}: 是字符串拼接。MyBatis会直接将${}内的内容替换为对应的参数值。存在SQL注入风险。
错误示例(排序场景):
<!-- 错误:使用 ${} 接收用户输入的排序字段,极不安全! -->
<select id="selectUsers" resultType="User">
SELECT * FROM user
ORDER BY ${sortField} ${sortOrder}
</select>如果前端传入 sortField=id; DROP TABLE user--,会导致灾难性后果。
正确做法: 对于表名、列名等非值参数,必须使用${},但需要进行白名单校验。
<!-- 在Java代码中进行白名单校验 -->
<select id="selectUsers" resultType="User">
SELECT * FROM user
ORDER BY ${sortField} ${sortOrder}
</select>// Service层代码
public List<User> selectUsers(String sortField, String sortOrder) {
// 白名单校验,防止SQL注入
List<String> allowedFields = Arrays.asList("id", "username", "create_time");
if (!allowedFields.contains(sortField)) {
sortField = "id"; // 默认字段
}
if (!"asc".equalsIgnoreCase(sortOrder) && !"desc".equalsIgnoreCase(sortOrder)) {
sortOrder = "asc";
}
return userMapper.selectUsers(sortField, sortOrder);
}坑2:动态SQL<if>标签与WHERE关键字
错误示例(多余的AND):
<select id="findUser" parameterType="UserQuery" resultType="User">
SELECT * FROM user
WHERE
<if test="username != null and username != ''">
username = #{username}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</select>当username为空而email不为空时,生成的SQL会是:SELECT * FROM user WHERE AND email = ?,语法错误!
解决方案1:使用<where>标签
<select id="findUser" parameterType="UserQuery" resultType="User">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username = #{username}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</where>
</select><where>标签会智能地处理:
- 只有当标签内有内容时,才会插入
WHERE关键字。 - 如果标签内内容以
AND或OR开头,它会将其去除。
解决方案2:使用1=1技巧
<select id="findUser" parameterType="UserQuery" resultType="User">
SELECT * FROM user
WHERE 1=1
<if test="username != null and username != ''">
AND username = #{username}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</select>坑3:一对多查询(<collection>)的N+1查询问题
场景: 查询部门及其下的所有员工。
V1.0 错误示范(触发N+1查询):
<!-- 1. 先查询所有部门 -->
<select id="selectAllDepartments" resultMap="DepartmentMap">
SELECT id, name FROM department
</select>
<resultMap id="DepartmentMap" type="Department">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 2. 对每个部门,执行一次查询员工的SQL -->
<collection property="employees" ofType="Employee" column="id" select="selectEmployeesByDeptId"/>
</resultMap>
<select id="selectEmployeesByDeptId" resultType="Employee">
SELECT id, name FROM employee WHERE dept_id = #{id}
</select>如果查询出N个部门,就会产生1(查询部门) + N(查询每个部门的员工)条SQL,性能极差。
V1.1 正确做法:使用连接查询<resultMap>
<select id="selectAllDepartmentsWithEmployees" resultMap="DepartmentWithEmployeesMap">
SELECT
d.id as dept_id,
d.name as dept_name,
e.id as emp_id,
e.name as emp_name
FROM department d
LEFT JOIN employee e ON d.id = e.dept_id
ORDER BY d.id
</select>
<resultMap id="DepartmentWithEmployeesMap" type="Department">
<id property="id" column="dept_id"/>
<result property="name" column="dept_name"/>
<!-- 使用结果集直接映射,而非嵌套查询 -->
<collection property="employees" ofType="Employee">
<id property="id" column="emp_id"/>
<result property="name" column="emp_name"/>
</collection>
</resultMap>原理: 通过一条SQL连接查询出所有数据,MyBatis的结果集映射器会根据<id>标签进行分组,将同一个部门下的所有员工数据组装到一个集合中。虽然SQL结果集可能有多条重复的部门信息,但内存中只有一个Department对象。用一次复杂的SQL查询代替多次简单的查询,这是解决N+1问题的关键。
坑4:PageHelper分页插件的“内存分页”陷阱
错误用法:
// 在查询之后才设置分页参数? 完全错误!
List<User> users = userMapper.selectAllUsers(); // 1. 先执行查询,查出了所有数据
PageHelper.startPage(1, 10); // 2. 再startPage,为时已晚!
// 此时PageHelper拦截到的是空,分页信息并未生效正确用法: PageHelper.startPage必须紧跟在MyBatis的查询方法之前调用。
// 在Service层
public PageInfo<User> getUsersByPage(int pageNum, int pageSize) {
// 紧跟在查询方法前调用,顺序至关重要!
PageHelper.startPage(pageNum, pageSize);
// 这个查询会被PageHelper自动拦截并改写SQL,加上LIMIT
List<User> users = userMapper.selectAllUsers();
// 用PageInfo包装结果,可以获取总条数等分页信息
return new PageInfo<>(users);
}原理: PageHelper利用MyBatis的插件(Interceptor)机制,在SQL执行前进行拦截。startPage方法将分页参数(页码,大小)存入ThreadLocal。当执行查询的Mapper方法时,插件会从ThreadLocal中获取参数,动态改写原始SQL(例如,SELECT * FROM user被改写为 SELECT * FROM user LIMIT 0, 10)。
总结:
MyBatis的强大在于其灵活性,但这也要求开发者对其底层原理有更深入的了解。两年里,我通过不断踩坑和总结,明白了“怎么用”和“为什么这么用” 同样重要。理解#{}和${}的区别、动态SQL的生成逻辑、结果映射的组装过程以及插件的工作原理,才能写出高效、安全的数据库访问代码。