Skip to content

MyBatis 高级特性

  涵盖 TypeHandler、枚举处理、批量操作、存储过程调用等 MyBatis 高级用法,帮助你应对复杂业务场景。

一、TypeHandler(类型处理器)

作用:实现 Java 类型与 JDBC 类型之间的相互转换

内置 TypeHandler:
  IntegerTypeHandler    → int / Integer ↔ INTEGER
  StringTypeHandler     → String ↔ VARCHAR
  DateTypeHandler       → Date ↔ TIMESTAMP
  BigDecimalTypeHandler → BigDecimal ↔ DECIMAL
  BooleanTypeHandler    → boolean ↔ BOOLEAN
  ...

1.1 自定义 TypeHandler

场景: 将数据库中的逗号分隔字符串("1,2,3")转换为 Java 的 List<Long>

java
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class ListTypeHandler extends BaseTypeHandler<List<Long>> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                                     List<Long> parameter, JdbcType jdbcType) throws SQLException {
        // Java → 数据库:List<Long> → "1,2,3"
        String value = parameter.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        ps.setString(i, value);
    }

    @Override
    public List<Long> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 数据库 → Java:String → List<Long>
        String value = rs.getString(columnName);
        return parseToList(value);
    }

    @Override
    public List<Long> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return parseToList(value);
    }

    @Override
    public List<Long> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return parseToList(value);
    }

    private List<Long> parseToList(String value) {
        if (value == null || value.isEmpty()) {
            return new ArrayList<>();
        }
        return Arrays.stream(value.split(","))
                .map(Long::parseLong)
                .collect(Collectors.toList());
    }
}

1.2 注册 TypeHandler

xml
<!-- 方式一:全局注册 -->
<typeHandlers>
    <typeHandler handler="com.example.handler.ListTypeHandler"/>
    <!-- 扫描包 -->
    <package name="com.example.handler"/>
</typeHandlers>
xml
<!-- 方式二:在 resultMap 中指定 -->
<resultMap id="userMap" type="User">
    <id property="id" column="id"/>
    <result property="roleIds" column="role_ids"
            typeHandler="com.example.handler.ListTypeHandler"/>
</resultMap>
java
// 方式三:在字段上标注(MyBatis Plus 支持)
@TableField(typeHandler = ListTypeHandler.class)
private List<Long> roleIds;

二、枚举处理

2.1 默认处理(EnumTypeHandler)

java
public enum Gender {
    MALE, FEMALE
}
java
// 实体类
public class User {
    private Gender gender;  // 数据库中存 "MALE" / "FEMALE"
}
xml
<!-- 默认使用 EnumTypeHandler,存枚举的 name() -->
<insert id="insert">
    INSERT INTO user (gender) VALUES (#{gender})
</insert>

2.2 自定义枚举处理(EnumOrdinalTypeHandler)

java
// 数据库中存 0/1
public class User {
    @EnumValue  // MyBatis Plus 注解
    private Gender gender;
}
xml
<!-- 方式一:全局配置 -->
<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
                 javaType="com.example.enums.Gender"/>
</typeHandlers>
java
// 方式二:自定义枚举 TypeHandler
public class GenderTypeHandler extends BaseTypeHandler<Gender> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                                     Gender parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, parameter.getCode());  // 存 code 值
    }

    @Override
    public Gender getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int code = rs.getInt(columnName);
        return Gender.fromCode(code);  // 根据 code 获取枚举
    }

    @Override
    public Gender getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int code = rs.getInt(columnIndex);
        return Gender.fromCode(code);
    }

    @Override
    public Gender getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int code = cs.getInt(columnIndex);
        return Gender.fromCode(code);
    }
}

// 枚举定义
public enum Gender {
    MALE(1, "男"),
    FEMALE(2, "女");

    private final int code;
    private final String desc;

    Gender(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() { return code; }

    public static Gender fromCode(int code) {
        for (Gender gender : values()) {
            if (gender.code == code) return gender;
        }
        throw new IllegalArgumentException("Unknown code: " + code);
    }
}

三、批量操作

3.1 批量插入

xml
<insert id="insertBatch">
    INSERT INTO user (name, age, email) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age}, #{user.email})
    </foreach>
</insert>
java
// 使用 ExecutorType.BATCH 提升性能
@Autowired
private SqlSessionTemplate sqlSessionTemplate;

public void batchInsert(List<User> users) {
    // 方式一:使用 BATCH 执行器
    SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory()
            .openSession(ExecutorType.BATCH, false);
    try {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        for (User user : users) {
            mapper.insert(user);
        }
        sqlSession.commit();
    } catch (Exception e) {
        sqlSession.rollback();
        throw e;
    } finally {
        sqlSession.close();
    }
}

// 方式二:MyBatis Plus 的 saveBatch
// userService.saveBatch(users);

3.2 批量更新

xml
<update id="updateBatch">
    <foreach collection="list" item="user" separator=";">
        UPDATE user
        SET name = #{user.name}, age = #{user.age}
        WHERE id = #{user.id}
    </foreach>
</update>

注意: 批量更新需要 JDBC 连接 URL 添加 allowMultiQueries=true

3.3 批量删除

xml
<delete id="deleteByIds">
    DELETE FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

四、存储过程调用

sql
-- 存储过程定义
DELIMITER //
CREATE PROCEDURE get_user_by_id(IN userId BIGINT, OUT userName VARCHAR(50))
BEGIN
    SELECT name INTO userName FROM user WHERE id = userId;
END //
DELIMITER ;

4.1 XML 方式

xml
<select id="callProcedure" statementType="CALLABLE" parameterType="map">
    {call get_user_by_id(
        #{userId, mode=IN, jdbcType=BIGINT},
        #{userName, mode=OUT, jdbcType=VARCHAR}
    )}
</select>
java
// Mapper 接口
void callProcedure(Map<String, Object> params);

// 调用
Map<String, Object> params = new HashMap<>();
params.put("userId", 1L);
userMapper.callProcedure(params);
System.out.println(params.get("userName"));  // 获取 OUT 参数

4.2 注解方式

java
@Select("{call get_user_by_id(#{userId, mode=IN, jdbcType=BIGINT}, " +
        "#{userName, mode=OUT, jdbcType=VARCHAR})}")
@Options(statementType = StatementType.CALLABLE)
void callProcedure(Map<String, Object> params);

五、RowBounds 分页

java
// MyBatis 内置的逻辑分页(不推荐,性能差)
RowBounds rowBounds = new RowBounds(0, 10);  // 偏移量,每页数量
List<User> users = sqlSession.selectList("com.example.mapper.UserMapper.selectAll",
                                         null, rowBounds);

注意: RowBounds 是逻辑分页,先查出全部数据再截取,性能极差。生产环境请使用 PageHelper 或 SQL 分页。

六、注解方式开发

java
@Mapper
public interface UserMapper {

    // 查询
    @Select("SELECT * FROM user WHERE id = #{id}")
    User selectById(Long id);

    @Select("SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%')")
    List<User> selectByName(String name);

    // 新增(返回自增主键)
    @Insert("INSERT INTO user (name, age, email) VALUES (#{name}, #{age}, #{email})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);

    // 修改
    @Update("UPDATE user SET name=#{name}, age=#{age}, email=#{email} WHERE id=#{id}")
    int update(User user);

    // 删除
    @Delete("DELETE FROM user WHERE id = #{id}")
    int deleteById(Long id);

    // 动态 SQL(使用 script 标签)
    @Select("<script>" +
            "SELECT * FROM user" +
            "<where>" +
            "<if test='name != null'>AND name = #{name}</if>" +
            "<if test='age != null'>AND age = #{age}</if>" +
            "</where>" +
            "</script>")
    List<User> selectByCondition(User user);

    // 一对一关联
    @Select("SELECT * FROM user WHERE id = #{id}")
    @Results({
        @Result(property = "id", column = "id"),
        @Result(property = "name", column = "name"),
        @Result(property = "orders", column = "id",
                many = @Many(select = "com.example.mapper.OrderMapper.selectByUserId"))
    })
    User selectWithOrders(Long id);
}

七、SQL 注入器(MyBatis Plus)

  MyBatis Plus 提供了 SQL 注入器,可以自定义全局方法。

java
// 自定义 SQL 注入器
@Component
public class MySqlInjector extends DefaultSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(configuration, mapperClass);
        methodList.add(new DeleteAllMethod());
        return methodList;
    }
}

// 自定义方法
public class DeleteAllMethod extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass,
                                                  TableInfo tableInfo) {
        String sql = "DELETE FROM " + tableInfo.getTableName();
        String method = "deleteAll";
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        return this.addDeleteMappedStatement(mapperClass, method, sqlSource);
    }
}

八、速记总结

高级特性速记:

  TypeHandler     → 类型转换(Java ↔ JDBC)
  枚举处理         → 自定义枚举 TypeHandler
  批量操作         → foreach + BATCH 执行器
  存储过程         → statementType="CALLABLE"
  注解开发         → @Select/@Insert/@Update/@Delete
  分页            → PageHelper(推荐)> RowBounds

速记口诀:
  类型转换 TypeHandler,枚举处理自定义,
  批量操作用 foreach,存储过程 CALLABLE,
  注解开发简单用,复杂 SQL 还是 XML 好。

九、面试要点

问题答案要点
TypeHandler 的作用?Java 类型和 JDBC 类型的双向转换
枚举怎么处理?自定义 EnumTypeHandler 或 EnumOrdinalTypeHandler
批量操作如何提升性能?使用 ExecutorType.BATCH
注解和 XML 怎么选?简单 SQL 用注解,复杂 SQL 用 XML