在 Java 持久层框架的世界里,MyBatis 以其灵活、高效的特性备受开发者青睐。今天,让我们一同深入探究 MyBatis 中几个至关重要的类,揭开它们的神秘面纱,领略 MyBatis 的强大魅力。

一、MappedStatement:SQL 语句的映射使者

(一)功能概述

MappedStatement 在 MyBatis 框架中扮演着关键角色,它犹如一座桥梁,将 XML 文件中的 SQL 语句节点(如 <select><update><insert> 标签)与 Java 代码紧密相连。在 MyBatis 框架初始化阶段,会对 XML 配置文件进行深度扫描和解析,将其中的 SQL 语句节点逐一转化为一个个 MappedStatement 对象,从而构建起 SQL 语句与代码逻辑之间的映射关系。

MyBatis.jpg

(二)实例解析

以一个简单的 XML Mapper 文件为例:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mybatis.UserDao">
    <cache type="org.mybatis.caches.ehcache.LoggingEhcache"/>
    <resultMap id="userResultMap" type="UserBean">
        <id property="userId" column="user_id"/>
        <result property="userName" column="user_name"/>
        <result property="userPassword" column="user_password"/>
        <result property="createDate" column="create_date"/>
    </resultMap>
    <select id="find" parameterType="UserBean" resultMap="userResultMap">
        select * from user
        <where>
            <if test="userName!=null and userName!=''">
                and user_name = #{userName}
            </if>
            <if test="userPassword!=null and userPassword!=''">
                and user_password = #{userPassword}
            </if>
            <if test="createDate!=null">
                and create_date = #{createDate}
            </if>
        </where>
    </select>
    <select id="find2" parameterType="UserBean" resultMap="userResultMap">
        select * from user
        <where>
            <if test="userName!=null and userName!=''">
                and user_name = #{userName}
            </if>
            <if test="userPassword!=null and userPassword!=''">
                and user_password = #{userPassword}
            </if>
            <if test="createDate!=null">
                and create_date = #{createDate}
            </if>
        </where>
    </select>
</mapper>

MyBatis 解析该文件后,会注册两个 MappedStatement 对象,分别对应 idfindfind2<select> 节点。在 MyBatis 框架中,为了确保每个 MappedStatement 对象的唯一性,其标识采用 Mapper 文件的 namespace 加上节点本身的 id 值。例如,上述两个 MappedStatement 对象在 MyBatis 框架中的唯一标识分别是 mybatis.UserDao.findmybatis.UserDao.find2


(三)源码探秘

查看 MappedStatement 对象的源码,我们可以看到其中包含了众多属性,这些属性与 XML 元素的属性存在着紧密的对应关系。其中,比较关键的属性包括:

  • ParameterMap 对象:用于表示查询参数,它明确了输入参数的类型和映射关系,确保 SQL 语句在执行时能够正确获取参数值。
  • ResultMap 列表(resultMaps):负责定义 SQL 查询结果与 Java 对象之间的映射规则,使得 MyBatis 能够将从数据库中获取的数据准确无误地封装成 JavaBean 对象,方便在 Java 代码中进行处理。
  • SqlSource 对象:这是最为重要的属性之一,它承担着执行动态 SQL 计算和获取的重任。通过这个对象,MappedStatement 能够根据用户提供的查询参数对象,动态生成要执行的 SQL 语句,充分展现了 MyBatis 的灵活性。

(四)工作流程

MappedStatement 对象的工作流程清晰而高效。当用户发起查询请求时,它首先接收用户传递的查询参数对象,然后借助 SqlSource 对象,根据参数对象的具体值动态计算出实际要执行的 SQL 语句。接着,将计算好的 SQL 语句发送到数据库执行,获取查询结果。最后,利用 ResultMap 列表将查询结果封装为 JavaBean 对象,并返回给用户。这个过程完美体现了 MyBatis 的核心价值:“根据用户提供的查询参数对象,动态执行 SQL 语句,并将结果封装为 Java 对象”。

(五)类图展示

class MappedStatement {
    - resource: String
    - configuration: Configuration
    - id: String
    - fetchSize: Integer
    - timeout: Integer
    - statementType: StatementType
    - resultSetType: ResultSetType
    - sqlSource: SqlSource
    - cache: Cache
    - parameterMap: ParameterMap
    - resultMaps: List<ResultMap>
    - flushCacheRequired: boolean
    - useCache: boolean
    - resultOrdered: boolean
    - sqlCommandType: SqlCommandType
    - keyGenerator: KeyGenerator
    - keyProperties: String[]
    - keyColumns: String[]
    - hasNestedResultMaps: boolean
    - databaseId: String
    - statementLog: Log
    - lang: LanguageDriver
    + MappedStatement()
}

二、SqlSource:动态 SQL 的幕后大师

(一)接口定义

SqlSource 是一个接口类,在 MappedStatement 对象中作为一个关键属性存在。其代码如下:

package org.apache.ibatis.mapping;

public interface SqlSource {
    BoundSql getBoundSql(Object parameterObject);
}

这个接口仅有一个方法 getBoundSql(Object parameterObject),该方法返回一个 BoundSql 对象。BoundSql 对象代表了一次 SQL 语句的实际执行内容,而 SqlSource 对象的核心职责就是根据传入的参数对象,动态计算并生成这个 BoundSql 对象。

(二)常用实现类

SqlSource 最常用的实现类是 DynamicSqlSource,其代码如下:

package org.apache.ibatis.scripting.xmltags;

import java.util.Map;

import org.apache.ibatis.builder.SqlSourceBuilder;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.session.Configuration;

public class DynamicSqlSource implements SqlSource {
    private Configuration configuration;
    private SqlNode rootSqlNode;

    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
    }

    public BoundSql getBoundSql(Object parameterObject) {
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        rootSqlNode.apply(context);
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
            boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
        }
        return boundSql;
    }
}

getBoundSql 方法中,通过创建 DynamicContext 对象,并调用 rootSqlNode.apply(context) 启动了一个基于递归实现的动态计算 SQL 语句的过程。这个过程借助 OGNL 根据传入的参数对象计算表达式,从而生成该次调用过程中实际要执行的 SQL 语句。

(三)动态 SQL 计算过程

  1. 首先,创建 DynamicContext 对象,传入 Configuration 和参数对象。DynamicContext 会对参数对象进行"Map 化”处理,即将传入的 POJO 对象转换为一个类似 Map 的数据结构,以便后续统一使用 Map 接口方法来访问数据。
  2. 接着,调用 rootSqlNode.apply(context),这是动态计算 SQL 语句的核心步骤。rootSqlNode 会根据传入的参数对象,通过 OGNL 计算表达式,逐步构建出实际要执行的 SQL 语句,并将其添加到 DynamicContextsqlBuilder 中。
  3. 然后,创建 SqlSourceBuilder 对象,使用它来解析 DynamicContext 中的 SQL 语句和参数类型,生成一个新的 SqlSource 对象。
  4. 最后,从新生成的 SqlSource 对象中获取 BoundSql 对象,并将 DynamicContext 中的绑定参数设置到 BoundSql 对象中,最终返回 BoundSql 对象,代表了这次动态计算得到的实际 SQL 语句和相关参数。

(四)类图展示

interface SqlSource {
    + getBoundSql(Object parameterObject): BoundSql
}

class DynamicSqlSource {
    - configuration: Configuration
    - rootSqlNode: SqlNode
    + DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode)
    + getBoundSql(Object parameterObject): BoundSql
}

三、DynamicContext:参数处理的核心枢纽

(一)功能介绍

DynamicContext 类在 MyBatis 的参数处理和动态 SQL 计算过程中起着至关重要的作用。它主要负责对传入的参数对象进行处理,将其转换为适合动态 SQL 计算的格式,并提供了一系列方法用于操作和获取 SQL 相关的信息。

(二)源码分析

  1. DynamicContext 的构造函数中,根据传入的参数对象是否为 Map 类型,有两种不同的处理方式来构造 ContextMap 对象。ContextMap 是一个继承自 HashMap 的内部类,其作用是统一参数的访问方式,使得无论是普通的 POJO 对象还是 Map 对象,都可以通过 Map 接口方法来访问数据。
  2. 当传入的参数对象不是 Map 类型时,MyBatis 会使用 MetaObject 对象对其进行封装。在动态计算 SQL 过程中,当需要获取数据时,通过 Map 接口的 get 方法包装 MetaObject 对象的取值过程,从而实现对 POJO 对象属性的访问。
  3. DynamicContext 类中的静态初始块 static { OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor()); } 表明 MyBatis 使用 OGNL 来计算动态 SQL 语句。ContextAccessorDynamicContext 的内部类,实现了 OGNL 中的 PropertyAccessor 接口,为 OGNL 提供了如何使用 ContextMap 参数对象的具体说明,从而完成了整个参数对象的"Map 化”处理。

(三)参数传递和使用过程

  1. 传入的参数对象首先被统一封装为 ContextMap 对象。
  2. OGNL 运行时环境在动态计算 SQL 语句时,按照 ContextAccessor 中描述的 Map 接口方式来访问和读取 ContextMap 对象,获取计算过程中所需的参数。
  3. ContextMap 对象内部可能封装了一个普通的 POJO 对象,也可能是直接传递的 Map 对象,但从外部来看,都是通过 Map 接口来读取数据,实现了对不同类型参数的无差别化处理。

(四)类图展示

class DynamicContext {
    - bindings: ContextMap
    - sqlBuilder: StringBuilder
    - uniqueNumber: int
    + PARAMETER_OBJECT_KEY: String
    + DATABASE_ID_KEY: String
    + DynamicContext(Configuration configuration, Object parameterObject)
    + getBindings(): Map<String, Object>
    + bind(String name, Object value)
    + appendSql(String sql)
    + getSql(): String
    + getUniqueNumber(): int
    static class ContextMap {
        - parameterMetaObject: MetaObject
        + ContextMap(MetaObject parameterMetaObject)
        + get(Object key): Object
    }
    static class ContextAccessor {
        + getProperty(Map context, Object target, Object name): Object
        + setProperty(Map context, Object target, Object name, Object value)
    }
}

(五)示例验证

以下是一个 JUnit 测试方法,用于验证 MyBatis 参数获取过程中对 Map 对象和普通 POJO 对象的无差别化处理:

@Test
public void testSqlSource() throws Exception {
    String resource = "mybatis/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
          .build(inputStream);
    SqlSession session = sqlSessionFactory.openSession();

    try {
        Configuration configuration = session.getConfiguration();
        MappedStatement mappedStatement = configuration
              .getMappedStatement("mybatis.UserDao.find2");
        assertNotNull(mappedStatement);

        UserBean param = new UserBean();
        param.setUserName("admin");
        param.setUserPassword("admin");
        BoundSql boundSql = mappedStatement.getBoundSql(param);
        String sql = boundSql.getSql();

        Map<String, Object> map = new HashMap<>();
        map.put("userName", "admin");
        map.put("userPassword", "admin");
        BoundSql boundSql2 = mappedStatement.getBoundSql(map);
        String sql2 = boundSql2.getSql();

        assertEquals(sql, sql2);

        UserBean bean = session.selectOne("mybatis.UserDao.find2", map);
        assertNotNull(bean);
    } finally {
        session.close();
    }
}

在这个测试中,第一次使用 UserBean 对象获取和计算 SQL 语句,第二次使用 HashMap 对象进行同样的操作,甚至直接使用 HashMap 对象启动了一次 session 对象的查询。测试结果通过,充分说明了 MyBatis 在参数获取过程中,对 Map 对象和普通 POJO 对象的无差别化处理,因为在内部,两者都会被封装,然后通过 Map 接口来访问。

总结

通过对 MyBatis 中这几个重要类的深入剖析,我们清晰地了解了 MyBatis 的核心工作机制。MappedStatement 作为 SQL 语句的映射使者,协调了 SQL 与 Java 代码之间的关系;SqlSource 则是动态 SQL 的幕后大师,根据参数动态生成可执行的 SQL 语句;DynamicContext 作为参数处理的核心枢纽,确保了参数的统一访问和动态 SQL 计算的顺利进行。这些类相互协作,共同构建了 MyBatis 强大而灵活的持久层框架,为开发者提供了高效、便捷的数据库操作体验。

说明:本文基于 MyBatis 3.x 版本进行剖析,核心类结构与机制在后续版本中保持相对稳定,但具体实现细节可能随版本迭代有所调整。