MyBatis 的 Mapper XML 如何拆分为 Java 代码:从简单到复杂的最佳实践

213 阅读6分钟

MyBatis 的 Mapper XML 如何拆分为 Java 代码:从朴素到复杂的最佳实践

在使用 MyBatis 开发插件时,你可能会遇到这样的代码:

StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String originalSql = boundSql.getSql();

这段代码从 StatementHandler 中提取了 BoundSql,进而拿到了 XML 中的 SQL 语句。这引发了一个有趣的问题:既然 XML 的 SQL 可以被 BoundSql 表示,那 XML 中的其他组分(如参数、结果映射)是否也能映射到 Java 代码中的某种对象?如果可以,我们能否用 Java 完全替代 XML?今天,我们从这个朴素的发现出发,逐步分析 XML 的组分,推导问题,最终逼近现代最佳方案。

朴素的第一步:XML 是可以拆解的吗?

假设我们有这样一个 XML:

<mapper namespace="com.example.UserMapper">
    <resultMap id="userMap" type="com.example.User">
        <id property="id" column="id"/>
        <result property="name" column="user_name"/>
    </resultMap>
    <select id="getUserById" parameterType="int" resultMap="userMap">
        SELECT id, user_name FROM users WHERE id = #{id}
    </select>
</mapper>

对应的接口是:

public interface UserMapper {
    User getUserById(int id);
}

从插件中提取 BoundSql 的经验来看,SQL 已经能被 Java 对象(BoundSql)表示。朴素的想法是:XML 的其他部分(参数、结果映射)应该也能拆解成 Java 代码,甚至完全抛弃 XML,用纯 Java 实现。我们先尝试把 SQL 翻译为注解:

@Select("SELECT id, user_name FROM users WHERE id = #{id}")
User getUserById(int id);

这很简单,但参数和结果映射怎么办?让我们从这个朴素 demo 开始,拆解 XML 的每个组分,看看它们对应什么 Java 对象,同时推导不利因素。

XML 的组分拆分:从朴素翻译到问题暴露

MyBatis 的 XML 包含 SQL、参数映射、结果映射等组分,我们逐一分析它们能拆分为 Java 代码中的什么。

1. SQL 语句:拆分为 BoundSql
  • XML 中的样子
    <select id="getUserById" resultMap="userMap">
        SELECT id, user_name FROM users WHERE id = #{id}
    </select>
    
  • 朴素翻译
    @Select("SELECT id, user_name FROM users WHERE id = #{id}")
    User getUserById(int id);
    
  • 对应 Java 对象
    在 MyBatis 内部,SQL 被解析为 BoundSql 对象。通过 StatementHandler.getBoundSql(),我们拿到的是运行时生成的 SQL(可能是 SELECT id, user_name FROM users WHERE id = ?),包含参数绑定信息。
  • 不利因素
    • 可读性:如果 SQL 很长(比如多表连接),注解会显得臃肿。
    • 动态性:XML 支持 <if> 等动态标签,注解无法直接实现。
2. 参数映射:拆分为 ParameterMapping
  • XML 中的样子
    <select id="getUserById" parameterType="int">
        SELECT id, user_name FROM users WHERE id = #{id}
    </select>
    
  • 朴素翻译
    注解中无需显式定义 parameterType,MyBatis 通过方法签名(int id)自动推导。
    @Select("SELECT id, user_name FROM users WHERE id = #{id}")
    User getUserById(int id);
    
  • 对应 Java 对象
    BoundSql 内部包含 ParameterMapping 列表,记录了 #{id} 到参数 id 的映射关系。在运行时,ParameterHandler 会根据这个映射将 id 的值(比如 1)绑定到 SQL。
  • 不利因素
    • 隐式性:参数名依赖方法签名,改动(如 int id 改为 int userId)需要同步调整 SQL。
    • 复杂参数:如果参数是对象(如 User),注解难以处理嵌套字段(如 #{user.name})。
3. 结果映射:拆分为 ResultMap
  • XML 中的样子
    <resultMap id="userMap" type="com.example.User">
        <id property="id" column="id"/>
        <result property="name" column="user_name"/>
    </resultMap>
    <select id="getUserById" resultMap="userMap">
        SELECT id, user_name FROM users WHERE id = #{id}
    </select>
    
  • 朴素翻译
    @Results 注解:
    @Select("SELECT id, user_name FROM users WHERE id = #{id}")
    @Results({
        @Result(property = "id", column = "id"),
        @Result(property = "name", column = "user_name")
    })
    User getUserById(int id);
    
  • 对应 Java 对象
    ResultMap 是 MyBatis 内部的结果映射对象,存储了列(如 user_name)到属性(如 name)的映射关系。ResultSetHandler 在处理查询结果时会使用它。
  • 不利因素
    • 冗余性:复杂对象(多字段)会导致 @Results 代码量增加。
    • 复用性差:XML 的 <resultMap> 可以被多个查询复用,注解需要重复定义。
4. 动态 SQL:拆分为逻辑代码
  • XML 中的样子
    <select id="findUsers" resultType="com.example.User">
        SELECT * FROM users
        <where>
            <if test="name != null">
                AND name = #{name}
            </if>
        </where>
    </select>
    
  • 朴素翻译
    注解不支持动态 SQL,最朴素的替代是手写逻辑:
    default List<User> findUsers(String name) {
        String sql = "SELECT * FROM users";
        if (name != null) {
            sql += " WHERE name = #{name}";
        }
        // 伪代码:假设有工具执行
        return executeSql(sql, name);
    }
    
  • 对应 Java 对象
    XML 的动态 SQL 被解析为 DynamicSqlSource,运行时生成 BoundSql
  • 不利因素
    • 复杂性:手写逻辑容易出错,维护困难。
    • 安全性:SQL 拼接可能导致注入风险。

朴素策略的不利因子总结

从插件中提取 BoundSql 启发我们用注解或代码替代 XML,但朴素方案暴露了问题:

  1. 可读性下降:长 SQL 和复杂映射让注解或代码臃肿。
  2. 灵活性不足:动态 SQL 和嵌套参数难以实现。
  3. 复用性差:结果映射和 SQL 片段无法复用。
  4. 安全性隐患:手写 SQL 逻辑可能不安全。

这些不利因子表明,朴素地用注解或手写代码替代 XML 是不够的。我们需要更现代的优化方向。

优化方向:逼近主流复杂方案

基于上述 demo 和不利因子,现代主流方案通过工具和框架消解这些问题。以下是优化方向,与当今最佳实践一致:

1. 使用 MyBatis-Plus 的代码生成器
  • 优化点:自动化生成 SQL 和映射。
  • 实现:MyBatis-Plus 提供代码生成器,生成 Mapper 接口和实体类,内置通用方法(如 selectById)。
  • 对应组分
    • SQL:生成 BoundSql 的通用逻辑。
    • 参数映射:通过实体注解(如 @TableField)替代 parameterType
    • 结果映射:自动推导,无需显式 @Results
  • 优势:消除了长 SQL 和复杂映射的可读性问题,复用性由框架保证。
2. 引入 Fluent MyBatis 或 MyBatis Dynamic SQL
  • 优化点:用流式 API 替代动态 SQL。
  • 实现
    SelectDSL<User> dsl = selectFrom(User.class)
        .where(User::getName, isEqualToWhenPresent(name))
        .build();
    
  • 对应组分:动态 SQL 被拆分为 SqlBuilderSqlNode,生成 BoundSql
  • 优势:灵活性提升,安全性由框架保障,避免拼接风险。
3. 注解 + SQL Provider
  • 优化点:分离复杂 SQL 和映射。
  • 实现
    @SelectProvider(type = UserSqlProvider.class, method = "getUserSql")
    User getUserById(int id);
    
    class UserSqlProvider {
        public String getUserSql(int id) {
            return "SELECT id, user_name FROM users WHERE id = #{id}";
        }
    }
    
  • 对应组分@SelectProvider 动态生成 BoundSql,结果映射可用单独类定义。
  • 优势:可读性提升,复用性通过 Java 类实现。
4. 转向全注解 ORM(如 JPA)
  • 优化点:完全用 Java 替代 XML。
  • 实现:使用 JPA,通过注解定义实体和查询:
    @Entity
    public class User {
        @Id
        private int id;
        @Column(name = "user_name")
        private String name;
    }
    
  • 对应组分:SQL 和映射被拆分为实体注解。
  • 优势:代码一致性高,但失去 MyBatis 的 SQL 灵活性。

最终方案:现代最佳实践

综合优化,现代主流方案是:

  • 简单场景:用注解(如 @Select)+ MyBatis-Plus。
  • 复杂场景:用 Fluent MyBatis 或 @SelectProvider
  • 大型项目:结合代码生成器,减少手工劳动。

这些方案消除了不利因子:

  • 可读性:长 SQL 外置或用流式 API。
  • 灵活性:动态 SQL 通过框架支持。
  • 复用性:代码生成和提供者模式。
  • 安全性:框架内置防护。

回答开头的疑问

“XML 中除了 SQL 以外,参数和结果是不是也能用 Java 代码中某种对象表达?”答案是肯定的:

  • SQL -> BoundSql:插件中直接获取。
  • 参数映射 -> ParameterMapping:存储在 BoundSql 中。
  • 结果映射 -> ResultMap:由 MappedStatement 持有,ResultSetHandler 使用。

这些对象在运行时由 MyBatis 动态生成,完全可以用 Java 代码替代 XML 定义。

结语

从插件中发现 BoundSql,到朴素地拆解 XML,再到暴露问题并优化,我们逐步逼近了现代方案。XML 的组分确实可以映射为 Java 对象,而主流实践通过工具和框架,让开发更高效、安全。希望这篇博客解答了你的疑问,也为你理解 MyBatis 提供了新视角!