引言

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默认使用param1param2等作为参数名,也可以使用@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>标签会自动处理第一个ANDOR,避免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},导致参数无法正确传递。

解决方法

  1. 保持参数名一致
  2. 使用@Param注解明确指定参数名
  3. 使用#{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注入风险。

解决方法

  1. 始终使用#{}进行参数传递
  2. 如果必须使用${}(如动态表名、列名),要对参数进行严格的白名单校验
// 安全的动态表名示例
<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>

问题:当nameage为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字段在数据库中是整数类型,但传入的是字符串类型,可能导致类型转换错误。

解决方法

  1. 确保参数类型与数据库字段类型匹配
  2. 使用@Param注解明确指定参数类型
  3. 在实体类中使用正确的数据类型
// 正确的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 参数传递原则

  1. 明确性原则:使用@Param注解明确参数名,避免依赖默认命名
  2. 安全性原则:始终使用#{}进行参数传递,避免SQL注入
  3. 可读性原则:使用有意义的参数名,避免使用param1param2
  4. 类型安全原则:确保参数类型与数据库字段类型匹配

5.2 代码组织建议

  1. 使用DTO封装复杂参数:当参数超过3个时,考虑使用DTO封装
  2. 统一参数校验:在Service层进行参数校验,而不是在Mapper层
  3. 合理使用动态SQL:避免过度复杂的动态SQL,保持SQL可读性
  4. 注释清晰:对复杂的参数传递逻辑添加注释

5.3 性能优化建议

  1. 批量操作分批次:大数据量操作时,分批次执行
  2. 避免不必要的参数传递:只传递必要的参数
  3. 使用缓存:对频繁查询的数据使用缓存
  4. 监控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 常见问题排查清单

  1. 参数未传递:检查@Param注解是否正确使用
  2. 类型不匹配:检查参数类型与数据库字段类型
  3. 空值处理:检查动态SQL是否正确处理null值
  4. SQL注入:检查是否使用${}拼接参数
  5. 性能问题:检查是否使用了合适的批量操作方式

九、总结

MyBatis的参数传递机制虽然灵活,但也需要开发者深入理解其工作原理。通过本文的详细解析,您应该能够:

  1. 掌握基础参数传递:单个参数、多个参数、对象参数的传递方式
  2. 运用高级技巧:动态SQL、批量操作、存储过程等复杂场景
  3. 避免常见错误:参数名不匹配、SQL注入、空值处理等问题
  4. 提升开发效率:通过最佳实践和性能优化技巧

记住,良好的参数传递习惯不仅能提高代码质量,还能显著提升系统性能和安全性。在实际开发中,建议结合具体业务场景,灵活运用这些技巧,不断优化和改进代码。

最后,建议定期回顾和重构MyBatis相关代码,确保参数传递方式符合最新的最佳实践,这样才能在快速变化的技术环境中保持代码的健壮性和可维护性。