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,但朴素方案暴露了问题:
- 可读性下降:长 SQL 和复杂映射让注解或代码臃肿。
- 灵活性不足:动态 SQL 和嵌套参数难以实现。
- 复用性差:结果映射和 SQL 片段无法复用。
- 安全性隐患:手写 SQL 逻辑可能不安全。
这些不利因子表明,朴素地用注解或手写代码替代 XML 是不够的。我们需要更现代的优化方向。
优化方向:逼近主流复杂方案
基于上述 demo 和不利因子,现代主流方案通过工具和框架消解这些问题。以下是优化方向,与当今最佳实践一致:
1. 使用 MyBatis-Plus 的代码生成器
- 优化点:自动化生成 SQL 和映射。
- 实现:MyBatis-Plus 提供代码生成器,生成 Mapper 接口和实体类,内置通用方法(如
selectById)。 - 对应组分:
- SQL:生成
BoundSql的通用逻辑。 - 参数映射:通过实体注解(如
@TableField)替代parameterType。 - 结果映射:自动推导,无需显式
@Results。
- SQL:生成
- 优势:消除了长 SQL 和复杂映射的可读性问题,复用性由框架保证。
2. 引入 Fluent MyBatis 或 MyBatis Dynamic SQL
- 优化点:用流式 API 替代动态 SQL。
- 实现:
SelectDSL<User> dsl = selectFrom(User.class) .where(User::getName, isEqualToWhenPresent(name)) .build(); - 对应组分:动态 SQL 被拆分为
SqlBuilder或SqlNode,生成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 提供了新视角!