Chinaunix首页 | 论坛 | 博客
  • 博客访问: 75470
  • 博文数量: 165
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1655
  • 用 户 组: 普通用户
  • 注册时间: 2022-09-26 14:37
文章分类

全部博文(165)

文章存档

2024年(2)

2023年(95)

2022年(68)

我的朋友

分类: 大数据

2022-10-11 13:42:48

导读

本文通过MyBatis一个低版本的bug(3.4.5之前的版本)入手,分析MyBatis的一次完整的查询流程,从配置文件的解析到一个查询的完整执行过程详细解读MyBatis的一次查询流程,通过本文可以详细了解MyBatis的一次查询过程。在平时的代码编写中,发现了MyBatis一个低版本的bug(3.4.5之前的版本),由于现在很多工程中的版本都是低于3.4.5的,因此在这里用一个简单的例子复现问题,并且从源码角度分析MyBatis一次查询的流程,让大家了解MyBatis的查询原理。


1 问题现象

1.1 场景问题复现

如下图所示,在示例Mapper中,下面提供了一个方法queryStudents,从student表中查询出符合查询条件的数据,入参可以为student_name或者student_name的集合,示例中参数只传入的是studentName的List集合

 List studentNames = new LinkedList<>();
 studentNames.add("lct");
 studentNames.add("lct2");
 condition.setStudentNames(studentNames);


 <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap"> select * from student
        <where>
            <if test="studentNames != null and studentNames.size > 0 ">
                AND student_name IN
                <foreach collection="studentNames" item="studentName" open="(" separator="," close=")"> #{studentName, jdbcType=VARCHAR} foreach>
            if>


            <if test="studentName != null and studentName != '' ">
                AND student_name = #{studentName, jdbcType=VARCHAR} if>
        where>
    select>

期望运行的结果是

select * from student WHERE student_name IN ( 'lct' , 'lct2' )

但是实际上运行的结果是

==> Preparing: select * from student WHERE student_name IN ( ? , ? ) AND student_name = ?

==> Parameters: lct(String), lct2(String), lct2(String)

<== Columns: id, student_name, age

<== Row: 2, lct2, 2

<== Total: 1

通过运行结果可以看到,没有给student_name单独赋值,但是经过MyBatis解析以后,单独给student_name赋值了一个值,可以推断出MyBatis在解析SQL并对变量赋值的时候是有问题的,初步猜测是foreach循环中的变量的值带到了foreach外边,导致SQL解析出现异常,下面通过源码进行分析验证

2 MyBatis查询原理

2.1 MyBatis架构

2.1.1 架构图

先简单来看看MyBatis整体上的架构模型,从整体上看MyBatis主要分为四大模块:

接口层:主要作用就是和数据库打交道

数据处理层:数据处理层可以说是MyBatis的核心,它要完成两个功能:

  • 通过传入参数构建动态SQL语句;
  • SQL语句的执行以及封装查询结果集成List

框架支撑层:主要有事务管理、连接池管理、缓存机制和SQL语句的配置方式

引导层:引导层是配置和启动MyBatis 配置信息的方式。MyBatis 提供两种方式来引导MyBatis :基于XML配置文件的方式和基于Java API 的方式

2.1.2 MyBatis四大对象

贯穿MyBatis整个框架的有四大核心对象,ParameterHandler、ResultSetHandler、StatementHandler和Executor,四大对象贯穿了整个框架的执行过程,四大对象的主要作用为:

  • ParameterHandler:设置预编译参数
  • ResultSetHandler:处理SQL的返回结果集
  • StatementHandler:处理sql语句预编译,设置参数等相关工作
  • Executor:MyBatis的执行器,用于执行增删改查操作

2.2 从源码解读MyBatis的一次查询过程

首先给出复现问题的代码以及相应的准备过程

2.2.1 数据准备

CREATE TABLE `student` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `student_name` varchar(255) NULL DEFAULT NULL, `age` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1; -- ---------------------------- -- Records of student -- ---------------------------- INSERT INTO `student` VALUES (1, 'lct', 1); INSERT INTO `student` VALUES (2, 'lct2', 2);


2.2.2 代码准备

1.mapper配置文件

 mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "" > <mapper namespace="mybatis.StudentDao">  <resultMap id="resultMap" type="mybatis.Student"> <id column="id" property="id" jdbcType="BIGINT" /> <result column="student_name" property="studentName" jdbcType="VARCHAR" /> <result column="age" property="age" jdbcType="INTEGER" /> resultMap> <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap"> select * from student <where> <if test="studentNames != null and studentNames.size > 0 "> AND student_name IN <foreach collection="studentNames" item="studentName" open="(" separator="," close=")"> #{studentName, jdbcType=VARCHAR} foreach> if> <if test="studentName != null and studentName != '' "> AND student_name = #{studentName, jdbcType=VARCHAR} if> where> select> mapper>

2.示例代码

public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource); //1.获取SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取对象 SqlSession sqlSession = sqlSessionFactory.openSession(); //3.获取接口的代理类对象 StudentDao mapper = sqlSession.getMapper(StudentDao.class);
        StudentCondition condition = new StudentCondition();
        List studentNames = new LinkedList<>();
        studentNames.add("lct");
        studentNames.add("lct2");
        condition.setStudentNames(studentNames); //执行方法 List students = mapper.queryStudents(condition);
    }


2.2.3 查询过程分析

1.SqlSessionFactory的构建

先看SqlSessionFactory的对象的创建过程

//1.获取SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

代码中首先通过调用SqlSessionFactoryBuilder中的build方法来获取对象,进入build方法

 public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null);
  }

调用自身的build方法

图1 build方法自身调用调试图例

在这个方法里会创建一个XMLConfigBuilder的对象,用来解析传入的MyBatis的配置文件,然后调用parse方法进行解析

图2 parse解析入参调试图例

在这个方法中,会从MyBatis的配置文件的根目录中获取xml的内容,其中parser这个对象是一个XPathParser的对象,这个是专门用来解析xml文件的,具体怎么从xml文件中获取到各个节点这里不再进行讲解。这里可以看到解析配置文件是从configuration这个节点开始的,在MyBatis的配置文件中这个节点也是根节点

 configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" ""> <configuration> <properties> <property name="dialect" value="MYSQL" />  properties>

然后将解析好的xml文件传入parseConfiguration方法中,在这个方法中会获取在配置文件中的各个节点的配置

图3 解析配置调试图例

以获取mappers节点的配置来看具体的解析过程

 <mappers> <mapper resource="mappers/StudentMapper.xml"/> mappers>

进入mapperElement方法

mapperElement(root.evalNode("mappers"));

图4 mapperElement方法调试图例

看到MyBatis还是通过创建一个XMLMapperBuilder对象来对mappers节点进行解析,在parse方法中

public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace();
  } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements();
}

通过调用configurationElement方法来解析配置的每一个mapper文件

private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

以解析mapper中的增删改查的标签来看看是如何解析一个mapper文件的

进入buildStatementFromContext方法

private void buildStatementFromContext(List list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

可以看到MyBatis还是通过创建一个XMLStatementBuilder对象来对增删改查节点进行解析,通过调用这个对象的parseStatementNode方法,在这个方法里会获取到配置在这个标签下的所有配置信息,然后进行设置

图5 parseStatementNode方法调试图例

解析完成以后,通过方法addMappedStatement将所有的配置都添加到一个MappedStatement中去,然后再将mappedstatement添加到configuration中去

builderAssistant.addMappedStatement(id, 
阅读(333) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~