引言
MyBatis作为Java领域最流行的持久层框架之一,其参数传递机制是开发者必须掌握的核心技能。参数传递的正确与否直接影响SQL的执行效率、代码的可维护性和系统的安全性。本文将从基础概念讲起,逐步深入到高级技巧,并结合实际案例分析常见错误,帮助开发者全面提升MyBatis参数传递的掌握程度。
一、MyBatis参数传递基础
1.1 参数传递的基本概念
MyBatis的参数传递是指将Java方法中的参数值映射到SQL语句中的占位符(#{})或拼接符(${})的过程。MyBatis提供了多种参数传递方式,以适应不同的业务场景。
1.2 基本参数传递方式
1.2.1 单个参数传递
当Mapper接口方法只有一个参数时,MyBatis会自动将该参数传递给SQL语句。
// Mapper接口
public interface UserMapper {
User getUserById(Integer id);
}
// XML映射文件
<select id="getUserById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
说明:#{id}中的id可以是任意名称,因为只有一个参数,MyBatis会自动匹配。但为了代码可读性,建议使用与参数名一致的名称。
1.2.2 多个参数传递
当方法有多个参数时,MyBatis默认使用param1、param2等作为参数名,也可以使用@Param注解指定参数名。
方式一:默认参数名
// Mapper接口
public interface UserMapper {
User getUserByNameAndAge(String name, Integer age);
}
// XML映射文件
<select id="getUserByNameAndAge" resultType="User">
SELECT * FROM users WHERE name = #{param1} AND age = #{param2}
</select>
方式二:使用@Param注解
// Mapper接口
public interface UserMapper {
User getUserByNameAndAge(@Param("name") String name, @Param("age") Integer age);
}
// XML映射文件
<select id="getUserByNameAndAge" resultType="User">
SELECT * FROM users WHERE name = #{name} AND age = #{age}
</select>
推荐使用@Param注解,因为它提高了代码的可读性和可维护性。
1.3 参数类型推断
MyBatis能够根据参数类型自动推断如何处理参数。例如:
- 基本类型:直接作为值传递
- 对象类型:可以访问其属性
- 集合类型:可以遍历处理
// 对象参数示例
public interface UserMapper {
void insertUser(User user);
}
<select id="insertUser" parameterType="User">
INSERT INTO users (name, age, email)
VALUES (#{name}, #{age}, #{email})
</select>
// 集合参数示例
public interface UserMapper {
List<User> getUsersByIds(List<Integer> ids);
}
<select id="getUsersByIds" resultType="User">
SELECT * FROM users WHERE id IN
<foreach collection="list" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
二、MyBatis参数传递高级技巧
2.1 动态SQL与参数传递
MyBatis的动态SQL功能(如<if>、<choose>、<foreach>等)与参数传递紧密结合,可以构建灵活的查询。
2.1.1 使用<if>标签进行条件判断
// Mapper接口
public interface UserMapper {
List<User> searchUsers(@Param("name") String name,
@Param("age") Integer age,
@Param("email") String email);
}
// XML映射文件
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null and email != ''">
AND email LIKE CONCAT('%', #{email}, '%')
</if>
</where>
</select>
说明:<where>标签会自动处理第一个AND或OR,避免SQL语法错误。
2.1.2 使用<foreach>标签处理集合
// 批量更新示例
public interface UserMapper {
int batchUpdateStatus(@Param("ids") List<Integer> ids,
@Param("status") Integer status);
}
<update id="batchUpdateStatus">
UPDATE users SET status = #{status}
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</update>
2.1.3 使用<choose>标签实现多条件分支
// Mapper接口
public interface UserMapper {
List<User> getUsersByCondition(@Param("type") String type,
@Param("value") Object value);
}
<select id="getUsersByCondition" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="type == 'name'">
AND name = #{value}
</when>
<when test="type == 'age'">
AND age = #{value}
</when>
<when test="type == 'email'">
AND email = #{value}
</when>
<otherwise>
AND 1=1
</otherwise>
</choose>
</where>
</select>
2.2 复杂对象参数传递
2.2.1 传递Map参数
// Mapper接口
public interface UserMapper {
List<User> searchUsersByMap(Map<String, Object> params);
}
// XML映射文件
<select id="searchUsersByMap" resultType="User">
SELECT * FROM users
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
</where>
</select>
// 调用示例
Map<String, Object> params = new HashMap<>();
params.put("name", "张");
params.put("minAge", 20);
params.put("maxAge", 30);
List<User> users = userMapper.searchUsersByMap(params);
2.2.2 传递嵌套对象参数
// 实体类
public class User {
private Integer id;
private String name;
private Integer age;
private Department department; // 嵌套对象
// getters and setters
}
public class Department {
private Integer id;
private String name;
// getters and setters
}
// Mapper接口
public interface UserMapper {
List<User> getUsersByDepartment(@Param("dept") Department dept);
}
// XML映射文件
<select id="getUsersByDepartment" resultType="User">
SELECT u.* FROM users u
JOIN departments d ON u.department_id = d.id
<where>
<if test="dept.id != null">
AND d.id = #{dept.id}
</if>
<if test="dept.name != null and dept.name != ''">
AND d.name LIKE CONCAT('%', #{dept.name}, '%')
</if>
</where>
</select>
2.3 参数类型处理器(TypeHandler)
MyBatis提供了TypeHandler接口,用于处理Java类型与数据库类型之间的转换。
2.3.1 自定义TypeHandler示例
// 自定义TypeHandler处理枚举类型
public class EnumTypeHandler<E extends Enum<E>> implements TypeHandler<E> {
private final Class<E> type;
private final Map<String, E> enumMap = new HashMap<>();
public EnumTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
for (E e : type.getEnumConstants()) {
enumMap.put(e.name(), e);
}
}
@Override
public void setParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
ps.setNull(i, Types.VARCHAR);
} else {
ps.setString(i, parameter.name());
}
}
@Override
public E getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return value == null ? null : enumMap.get(value);
}
@Override
public E getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return value == null ? null : enumMap.get(value);
}
@Override
public E getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return value == null ? null : enumMap.get(value);
}
}
// 使用自定义TypeHandler
public enum UserStatus {
ACTIVE, INACTIVE, PENDING
}
// 实体类
public class User {
private Integer id;
private String name;
private UserStatus status;
// getters and setters
}
// Mapper接口
public interface UserMapper {
void insertUser(User user);
}
// XML映射文件
<insert id="insertUser" parameterType="User">
INSERT INTO users (name, status)
VALUES (#{name}, #{status, typeHandler=com.example.EnumTypeHandler})
</insert>
// 或者在全局配置中注册TypeHandler
<configuration>
<typeHandlers>
<typeHandler handler="com.example.EnumTypeHandler"
javaType="com.example.UserStatus"/>
</typeHandlers>
</configuration>
2.4 批量操作与参数传递
2.4.1 批量插入
// Mapper接口
public interface UserMapper {
int batchInsert(@Param("users") List<User> users);
}
// XML映射文件
<insert id="batchInsert">
INSERT INTO users (name, age, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
// 调用示例
List<User> users = new ArrayList<>();
for (int i = 0; i < 100; i++) {
User user = new User();
user.setName("User" + i);
user.setAge(20 + i % 30);
user.setEmail("user" + i + "@example.com");
users.add(user);
}
userMapper.batchInsert(users);
注意:批量插入时,如果数据量过大(如超过1000条),建议分批次执行,避免SQL语句过长导致性能问题。
2.4.2 批量更新
// Mapper接口
public interface UserMapper {
int batchUpdate(@Param("users") List<User> users);
}
// XML映射文件
<update id="batchUpdate">
<foreach collection="users" item="user" separator=";">
UPDATE users SET name = #{user.name}, age = #{user.age}
WHERE id = #{user.id}
</foreach>
</update>
2.5 存储过程参数传递
MyBatis支持调用存储过程,并可以处理输入输出参数。
// Mapper接口
public interface UserMapper {
void callProcedure(@Param("inParam") String inParam,
@Param("outParam") Integer outParam);
}
// XML映射文件
<select id="callProcedure" statementType="CALLABLE">
{call my_procedure(#{inParam, mode=IN}, #{outParam, mode=OUT, jdbcType=INTEGER})}
</select>
// 调用示例
Map<String, Object> params = new HashMap<>();
params.put("inParam", "test");
userMapper.callProcedure(params);
Integer result = (Integer) params.get("outParam");
三、常见错误及避免方法
3.1 参数名不匹配错误
错误示例:
// Mapper接口
public interface UserMapper {
User getUserById(Integer id);
}
// XML映射文件(错误)
<select id="getUserById" resultType="User">
SELECT * FROM users WHERE id = #{userId} <!-- 参数名不匹配 -->
</select>
错误原因:当只有一个参数时,MyBatis默认使用参数名(如id),但XML中使用了#{userId},导致参数无法正确传递。
解决方法:
- 保持参数名一致
- 使用
@Param注解明确指定参数名 - 使用
#{param1}(不推荐,可读性差)
3.2 SQL注入风险
错误示例:
// Mapper接口
public interface UserMapper {
List<User> searchUsers(String name);
}
// XML映射文件(错误)
<select id="searchUsers" resultType="User">
SELECT * FROM users WHERE name = '${name}' <!-- 使用${}导致SQL注入 -->
</select>
错误原因:${}是字符串拼接,不会进行预编译,直接拼接SQL语句,存在SQL注入风险。
解决方法:
- 始终使用
#{}进行参数传递 - 如果必须使用
${}(如动态表名、列名),要对参数进行严格的白名单校验
// 安全的动态表名示例
<select id="getByTableName" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
// 调用前必须校验
public List<User> getByTableName(String tableName, Integer id) {
// 白名单校验
if (!isValidTableName(tableName)) {
throw new IllegalArgumentException("Invalid table name");
}
return userMapper.getByTableName(tableName, id);
}
private boolean isValidTableName(String tableName) {
// 只允许字母、数字和下划线
return tableName.matches("^[a-zA-Z0-9_]+$");
}
3.3 空值处理不当
错误示例:
// Mapper接口
public interface UserMapper {
List<User> searchUsers(String name, Integer age);
}
// XML映射文件
<select id="searchUsers" resultType="User">
SELECT * FROM users WHERE name = #{name} AND age = #{age}
</select>
问题:当name或age为null时,SQL语句会变成WHERE name = null AND age = null,这通常不是期望的结果。
解决方法:使用动态SQL处理空值
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
3.4 集合参数遍历错误
错误示例:
// Mapper接口
public interface UserMapper {
List<User> getUsersByIds(List<Integer> ids);
}
// XML映射文件(错误)
<select id="getUsersByIds" resultType="User">
SELECT * FROM users WHERE id IN (#{ids}) <!-- 不能直接传递集合 -->
</select>
错误原因:不能直接将集合传递给#{},必须使用<foreach>标签遍历。
正确做法:
<select id="getUsersByIds" resultType="User">
SELECT * FROM users WHERE id IN
<foreach collection="list" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
3.5 参数类型转换错误
错误示例:
// Mapper接口
public interface UserMapper {
void insertUser(String name, String age);
}
// XML映射文件
<insert id="insertUser">
INSERT INTO users (name, age) VALUES (#{name}, #{age})
</insert>
问题:age字段在数据库中是整数类型,但传入的是字符串类型,可能导致类型转换错误。
解决方法:
- 确保参数类型与数据库字段类型匹配
- 使用
@Param注解明确指定参数类型 - 在实体类中使用正确的数据类型
// 正确的Mapper接口
public interface UserMapper {
void insertUser(@Param("name") String name, @Param("age") Integer age);
}
3.6 动态SQL逻辑错误
错误示例:
// Mapper接口
public interface UserMapper {
List<User> searchUsers(@Param("name") String name,
@Param("age") Integer age);
}
// XML映射文件(错误)
<select id="searchUsers" resultType="User">
SELECT * FROM users WHERE
<if test="name != null">
name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</select>
问题:当只有age参数时,SQL会变成WHERE AND age = #{age},导致语法错误。
解决方法:使用<where>标签或手动处理第一个条件
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
3.7 批量操作性能问题
问题:批量插入大量数据时,如果一次性插入所有数据,可能导致内存溢出或SQL语句过长。
解决方案:分批次处理
// 分批次批量插入
public void batchInsertUsers(List<User> users) {
int batchSize = 100; // 每批100条
for (int i = 0; i < users.size(); i += batchSize) {
int end = Math.min(i + batchSize, users.size());
List<User> batch = users.subList(i, end);
userMapper.batchInsert(batch);
}
}
// 或者使用MyBatis的批量执行器
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
for (User user : users) {
userMapper.insertUser(user);
}
sqlSession.commit();
} finally {
sqlSession.close();
}
四、性能优化技巧
4.1 使用合适的参数类型
- 基本类型 vs 包装类型:基本类型(如
int)比包装类型(如Integer)性能稍好,但包装类型可以处理null值 - 集合类型选择:对于大量数据,考虑使用
Stream或分页处理
4.2 减少参数传递开销
// 不推荐:传递多个独立参数
public interface UserMapper {
List<User> searchUsers(String name, Integer age, String email,
String phone, String address);
}
// 推荐:使用DTO封装参数
public class UserSearchDTO {
private String name;
private Integer age;
private String email;
private String phone;
private String address;
// getters and setters
}
public interface UserMapper {
List<User> searchUsers(UserSearchDTO dto);
}
4.3 使用缓存减少参数传递
// Mapper接口
public interface UserMapper {
@Cacheable(value = "users", key = "#id")
User getUserById(Integer id);
}
// XML映射文件
<select id="getUserById" resultType="User" useCache="true">
SELECT * FROM users WHERE id = #{id}
</select>
4.4 批量操作优化
// 使用MyBatis的批量执行器
public void batchInsertOptimized(List<User> users) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
int batchSize = 100;
for (int i = 0; i < users.size(); i += batchSize) {
int end = Math.min(i + batchSize, users.size());
for (int j = i; j < end; j++) {
userMapper.insertUser(users.get(j));
}
sqlSession.flushStatements(); // 定期刷新
}
sqlSession.commit();
} finally {
sqlSession.close();
}
}
五、最佳实践总结
5.1 参数传递原则
- 明确性原则:使用
@Param注解明确参数名,避免依赖默认命名 - 安全性原则:始终使用
#{}进行参数传递,避免SQL注入 - 可读性原则:使用有意义的参数名,避免使用
param1、param2 - 类型安全原则:确保参数类型与数据库字段类型匹配
5.2 代码组织建议
- 使用DTO封装复杂参数:当参数超过3个时,考虑使用DTO封装
- 统一参数校验:在Service层进行参数校验,而不是在Mapper层
- 合理使用动态SQL:避免过度复杂的动态SQL,保持SQL可读性
- 注释清晰:对复杂的参数传递逻辑添加注释
5.3 性能优化建议
- 批量操作分批次:大数据量操作时,分批次执行
- 避免不必要的参数传递:只传递必要的参数
- 使用缓存:对频繁查询的数据使用缓存
- 监控SQL执行:使用MyBatis的插件监控SQL执行性能
六、进阶技巧:注解方式参数传递
除了XML映射文件,MyBatis还支持使用注解方式定义SQL,这种方式在Spring Boot项目中尤为常见。
6.1 基本注解使用
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User getUserById(Integer id);
@Insert("INSERT INTO users (name, age, email) VALUES (#{name}, #{age}, #{email})")
int insertUser(User user);
@Update("UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}")
int updateUser(User user);
@Delete("DELETE FROM users WHERE id = #{id}")
int deleteUser(Integer id);
}
6.2 动态SQL注解
public interface UserMapper {
@SelectProvider(type = UserSqlProvider.class, method = "searchUsers")
List<User> searchUsers(@Param("name") String name,
@Param("age") Integer age);
}
public class UserSqlProvider {
public String searchUsers(@Param("name") String name,
@Param("age") Integer age) {
return new SQL() {{
SELECT("*");
FROM("users");
if (name != null && !name.isEmpty()) {
WHERE("name LIKE CONCAT('%', #{name}, '%')");
}
if (age != null) {
WHERE("age = #{age}");
}
}}.toString();
}
}
6.3 注解方式的参数传递技巧
public interface UserMapper {
// 批量插入
@Insert({
"<script>",
"INSERT INTO users (name, age, email) VALUES ",
"<foreach collection='users' item='user' separator=','>",
"(#{user.name}, #{user.age}, #{user.email})",
"</foreach>",
"</script>"
})
int batchInsert(@Param("users") List<User> users);
// 动态表名
@Select("SELECT * FROM ${tableName} WHERE id = #{id}")
List<User> getByTableName(@Param("tableName") String tableName,
@Param("id") Integer id);
}
七、实战案例:复杂查询参数传递
7.1 多表关联查询参数传递
// Mapper接口
public interface OrderMapper {
List<OrderDTO> searchOrders(@Param("orderNo") String orderNo,
@Param("customerName") String customerName,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("status") Integer status);
}
// XML映射文件
<select id="searchOrders" resultType="OrderDTO">
SELECT
o.id, o.order_no, o.total_amount, o.status,
c.name as customer_name, c.phone as customer_phone
FROM orders o
JOIN customers c ON o.customer_id = c.id
<where>
<if test="orderNo != null and orderNo != ''">
AND o.order_no LIKE CONCAT('%', #{orderNo}, '%')
</if>
<if test="customerName != null and customerName != ''">
AND c.name LIKE CONCAT('%', #{customerName}, '%')
</if>
<if test="startDate != null">
AND o.create_time >= #{startDate}
</if>
<if test="endDate != null">
AND o.create_time <= #{endDate}
</if>
<if test="status != null">
AND o.status = #{status}
</if>
</where>
ORDER BY o.create_time DESC
</select>
7.2 树形结构查询参数传递
// Mapper接口
public interface DepartmentMapper {
List<Department> getDepartmentTree(@Param("parentId") Integer parentId);
}
// XML映射文件
<select id="getDepartmentTree" resultType="Department">
WITH RECURSIVE dept_tree AS (
SELECT id, name, parent_id, 0 as level
FROM departments
WHERE parent_id = #{parentId}
UNION ALL
SELECT d.id, d.name, d.parent_id, dt.level + 1
FROM departments d
JOIN dept_tree dt ON d.parent_id = dt.id
)
SELECT * FROM dept_tree ORDER BY level, id
</select>
八、调试与问题排查
8.1 开启MyBatis日志
在application.properties中配置:
# 显示SQL语句和参数
logging.level.com.example.mapper=DEBUG
# 显示更详细的MyBatis日志
logging.level.org.mybatis=DEBUG
8.2 使用MyBatis插件
// 自定义插件拦截参数传递
@Intercepts({
@Signature(type = StatementHandler.class, method = "parameterize", args = Statement.class)
})
public class ParameterInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
System.out.println("SQL: " + boundSql.getSql());
System.out.println("Parameters: " + boundSql.getParameterObject());
return invocation.proceed();
}
}
8.3 常见问题排查清单
- 参数未传递:检查
@Param注解是否正确使用 - 类型不匹配:检查参数类型与数据库字段类型
- 空值处理:检查动态SQL是否正确处理null值
- SQL注入:检查是否使用
${}拼接参数 - 性能问题:检查是否使用了合适的批量操作方式
九、总结
MyBatis的参数传递机制虽然灵活,但也需要开发者深入理解其工作原理。通过本文的详细解析,您应该能够:
- 掌握基础参数传递:单个参数、多个参数、对象参数的传递方式
- 运用高级技巧:动态SQL、批量操作、存储过程等复杂场景
- 避免常见错误:参数名不匹配、SQL注入、空值处理等问题
- 提升开发效率:通过最佳实践和性能优化技巧
记住,良好的参数传递习惯不仅能提高代码质量,还能显著提升系统性能和安全性。在实际开发中,建议结合具体业务场景,灵活运用这些技巧,不断优化和改进代码。
最后,建议定期回顾和重构MyBatis相关代码,确保参数传递方式符合最新的最佳实践,这样才能在快速变化的技术环境中保持代码的健壮性和可维护性。
