1. MyBatis参数处理机制深度解析
MyBatis作为Java生态中最受欢迎的ORM框架之一,其参数处理机制在实际开发中扮演着关键角色。不同于Hibernate的全自动映射,MyBatis提供了更灵活的参数控制方式,这也是许多开发者选择它的重要原因。
1.1 基础参数绑定原理
当我们在Mapper接口中定义方法时,MyBatis会根据方法参数生成对应的参数映射。最简单的形式是使用单个基本类型参数:
java复制@Select("SELECT * FROM users WHERE id = #{id}")
User findById(int id);
这里的#{id}就是参数占位符,MyBatis会将其替换为预编译语句中的?,并通过PreparedStatement安全地设置参数值。这种处理方式有效防止了SQL注入,是MyBatis参数处理的第一道安全防线。
1.2 复杂参数类型处理
实际开发中我们经常需要处理更复杂的参数场景。当方法有多个参数时,MyBatis默认会按照顺序将它们存储在map中,key分别为"param1"、"param2"等:
java复制@Select("SELECT * FROM users WHERE name = #{param1} AND age = #{param2}")
List<User> findByNameAndAge(String name, int age);
更优雅的做法是使用@Param注解明确指定参数名称:
java复制@Select("SELECT * FROM users WHERE name = #{name} AND age = #{age}")
List<User> findByNameAndAge(@Param("name") String name, @Param("age") int age);
对于对象参数,MyBatis会自动解析其属性:
java复制public class UserQuery {
private String name;
private Integer minAge;
private Integer maxAge;
// getters & setters
}
@Select("SELECT * FROM users WHERE name = #{name} AND age BETWEEN #{minAge} AND #{maxAge}")
List<User> findByQuery(UserQuery query);
1.3 特殊参数类型处理技巧
在处理集合类型参数时,MyBatis提供了强大的支持。比如批量查询:
java复制@Select("<script>" +
"SELECT * FROM users WHERE id IN " +
"<foreach item='id' collection='ids' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
"</script>")
List<User> findByIds(@Param("ids") List<Integer> ids);
这里使用了MyBatis的动态SQL功能,<foreach>标签会遍历集合生成IN语句。注意collection属性必须与@Param指定的名称一致。
对于Map类型的参数,可以直接通过key访问值:
java复制@Insert("INSERT INTO users(name, age) VALUES(#{user.name}, #{user.age})")
int insertUser(@Param("user") Map<String, Object> userMap);
1.4 参数类型处理器(TypeHandler)
MyBatis使用TypeHandler来处理Java类型与JDBC类型之间的转换。框架内置了大量常用类型的处理器,如StringTypeHandler、IntegerTypeHandler等。当遇到特殊类型时,我们可以自定义TypeHandler:
java复制@MappedTypes(PhoneNumber.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class PhoneNumberTypeHandler extends BaseTypeHandler<PhoneNumber> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
PhoneNumber parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.getValue());
}
@Override
public PhoneNumber getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return new PhoneNumber(rs.getString(columnName));
}
// 其他重载方法...
}
注册自定义TypeHandler后,MyBatis就能自动处理PhoneNumber类型与数据库VARCHAR字段的转换。
提示:当处理枚举类型时,MyBatis默认使用枚举的name()值。如果需要使用ordinal()或其他属性,可以实现自定义TypeHandler或使用MyBatis提供的EnumTypeHandler/EnumOrdinalTypeHandler。
2. 查询结果映射的艺术
查询结果映射是MyBatis最强大的特性之一,它提供了从结果集到Java对象的灵活转换方式。合理使用结果映射可以显著减少重复代码,提高开发效率。
2.1 自动映射基础
MyBatis默认会尝试自动映射查询结果到返回类型的属性上,只要列名与属性名匹配(不区分大小写):
java复制public class User {
private Long id;
private String name;
private Integer age;
// getters & setters
}
@Select("SELECT id, name, age FROM users WHERE id = #{id}")
User findUserById(Long id);
这种简单的映射在大多数情况下都能很好地工作,但遇到复杂场景时就需要更精细的控制。
2.2 使用@Results注解定义映射
当列名与属性名不一致,或者需要复杂映射时,可以使用@Results注解:
java复制@Results({
@Result(property = "id", column = "user_id"),
@Result(property = "name", column = "user_name"),
@Result(property = "age", column = "user_age")
})
@Select("SELECT user_id, user_name, user_age FROM users WHERE user_id = #{id}")
User findUserByIdWithCustomMapping(Long id);
@Results注解可以被复用,通过@ResultMap引用:
java复制@ResultMap("userResultMap")
@Select("SELECT user_id, user_name, user_age FROM users")
List<User> findAllUsers();
2.3 XML映射文件中的结果映射
对于更复杂的映射关系,XML配置提供了更强大的表达能力。以下是一个典型的结果映射配置:
xml复制<resultMap id="userResultMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="age" column="user_age"/>
<result property="createTime" column="create_time"
typeHandler="org.apache.ibatis.type.LocalDateTimeTypeHandler"/>
</resultMap>
<select id="findUserById" resultMap="userResultMap">
SELECT * FROM users WHERE user_id = #{id}
</select>
2.4 高级映射:关联和集合
MyBatis支持复杂对象关系的映射,包括一对一、一对多等关联关系。
一对一关联示例:
xml复制<resultMap id="orderResultMap" type="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
</association>
</resultMap>
<select id="findOrderWithUser" resultMap="orderResultMap">
SELECT o.order_id, o.order_no, u.user_id, u.user_name
FROM orders o JOIN users u ON o.user_id = u.user_id
WHERE o.order_id = #{id}
</select>
一对多关联示例:
xml复制<resultMap id="userWithOrdersResultMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
</collection>
</resultMap>
<select id="findUserWithOrders" resultMap="userWithOrdersResultMap">
SELECT u.user_id, u.user_name, o.order_id, o.order_no
FROM users u LEFT JOIN orders o ON u.user_id = o.user_id
WHERE u.user_id = #{id}
</select>
2.5 嵌套查询与延迟加载
对于大型对象图,MyBatis支持通过嵌套查询实现关联映射,并可以配置延迟加载:
xml复制<resultMap id="userWithOrdersResultMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<collection property="orders" ofType="Order"
select="findOrdersByUserId" column="user_id"
fetchType="lazy"/>
</resultMap>
<select id="findUserWithOrders" resultMap="userWithOrdersResultMap">
SELECT user_id, user_name FROM users WHERE user_id = #{id}
</select>
<select id="findOrdersByUserId" resultType="Order">
SELECT order_id as id, order_no as orderNo
FROM orders WHERE user_id = #{userId}
</select>
要启用延迟加载,需要在配置文件中设置:
xml复制<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
3. 动态SQL与参数处理的结合应用
MyBatis的动态SQL功能与参数处理机制结合使用,可以构建出非常灵活的查询逻辑。
3.1 条件查询实现
xml复制<select id="findUsersByCondition" 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>
ORDER BY id
</select>
<where>标签会智能处理WHERE子句,只有当内部条件成立时才会添加WHERE关键字,并自动去除开头多余的AND或OR。
3.2 批量操作实现
批量插入示例:
java复制@Insert("<script>" +
"INSERT INTO users (name, age) VALUES " +
"<foreach collection='users' item='user' separator=','>" +
"(#{user.name}, #{user.age})" +
"</foreach>" +
"</script>")
int batchInsert(@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>
注意:批量更新需要确保数据库连接允许执行多条语句(在JDBC URL中添加
allowMultiQueries=true参数)。
3.3 参数类型判断与动态处理
当参数可能是多种类型时(如既可能是数组又可能是字符串),可以使用OGNL表达式进行判断:
xml复制<select id="findByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<choose>
<when test="ids instanceof java.util.List">
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</when>
<when test="ids instanceof java.lang.String">
<foreach collection="ids.split(',')" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</when>
<otherwise>
(0)
</otherwise>
</choose>
</select>
4. 实战中的常见问题与解决方案
4.1 参数类型转换问题
在处理数值类型时,常见的坑是数据库无符号类型与Java类型的映射。例如MySQL的UNSIGNED INT最大值为4294967295,超过了Java Integer的最大值2147483647。解决方案:
- 使用Long类型接收
- 在JDBC URL中添加
useSSL=false&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&tinyInt1isBit=false&yearIsDateType=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useAffectedRows=true&allowPublicKeyRetrieval=true
对于MyBatis-Plus用户,可以通过注解指定JDBC类型:
java复制@TableField(value = "unsigned_field", jdbcType = JdbcType.BIGINT)
private Long unsignedField;
4.2 结果映射不生效的排查
当结果映射不生效时,可以按照以下步骤排查:
- 检查
resultType或resultMap是否正确指定 - 确认列名与属性名是否匹配(注意数据库下划线命名与Java驼峰命名的自动转换)
- 检查是否有TypeHandler处理该类型
- 查看MyBatis日志(确保
log-impl配置正确)
启用完整日志的配置:
xml复制<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
4.3 批量操作性能优化
大批量数据操作时,可以考虑以下优化方案:
- 使用BatchExecutor:
java复制SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user);
}
session.commit();
} finally {
session.close();
}
- 使用MyBatis-Plus的
saveBatch方法 - 对于超大批量(万级以上),考虑使用LOAD DATA INFILE或分批次处理
4.4 多租户实现方案
在Spring Boot + MyBatis-Plus多租户场景下,可以通过以下方式实现:
- 添加租户处理器:
java复制public class MyTenantHandler implements TenantLineHandler {
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public Expression getTenantId() {
return new StringValue(TenantContext.getCurrentTenant());
}
@Override
public boolean ignoreTable(String tableName) {
return !Arrays.asList("users", "orders").contains(tableName);
}
}
- 配置拦截器:
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MyTenantHandler()));
return interceptor;
}
4.5 插件开发与拦截器使用
MyBatis的插件机制可以拦截四大对象:Executor、ParameterHandler、ResultSetHandler和StatementHandler。开发自定义插件的步骤:
- 实现Interceptor接口:
java复制@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 前置处理
Object result = invocation.proceed();
// 后置处理
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取配置
}
}
- 注册插件:
xml复制<plugins>
<plugin interceptor="com.example.MyPlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
或者通过Java配置:
java复制@Bean
public MyPlugin myPlugin() {
MyPlugin plugin = new MyPlugin();
plugin.setProperties(new Properties());
return plugin;
}
