mybatis的实现原理


MyBatis是一个流行的ORM框架,面向接口编程的方式,大大简化了数据库的操作。MyBatis的底层操作封装了JDBC的API,MyBatis的工作原理以及核心流程与JDBC的使用步骤一脉相承,MyBatis的核心对象(SqlSession,Executor)与JDBC的核心对象(Connection,Statement)相互对应。本文的核心观点是:从JDBC入手并立足于JDBC,才能深入的理解MyBatis的工作原理以及核心流程。

要理解Mybatis的实现原理就要结合JDBC来理解MyBatis的工作原理往往才能更透彻。我们知道,JDBC有四个核心对象:

  • (1)DriverManager,用于注册数据库连接
  • (2)Connection,与数据库连接对象
  • (3)Statement/PrepareStatement,操作数据库SQL语句的对象
  • (4)ResultSet,结果集或一张虚拟表

而MyBatis也有四大核心对象:

  • (1)SqlSession对象,该对象中包含了执行SQL语句的所有方法【1】。类似于JDBC里面的Connection 【2】。
  • (2)Executor接口,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。类似于JDBC里面的Statement/PrepareStatement。
  • (3)MappedStatement对象,该对象是对映射SQL的封装,用于存储要映射的SQL语句的id、参数等信息。
  • (4)ResultHandler对象,用于对返回的结果进行处理,最终得到自己想要的数据格式或类型。可以自定义返回类型。

MyBatis的框架设计

MyBatis的工作原理如下图所示

上面中流程就是MyBatis内部核心流程,每一步流程的详细说明如下文所述:

  • (1)读取MyBatis的配置文件。mybatis-config.xml为MyBatis的全局配置文件,用于配置数据库连接信息。
  • (2)加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
  • (3)构造会话工厂。通过MyBatis的环境配置信息构建会话工厂SqlSessionFactory。
  • (4)创建会话对象。由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。
  • (5)Executor执行器。MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。
  • (6)MappedStatement对象。在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。
  • (7)输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。
  • (8)输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

接口层—和数据库交互的方式

MyBatis 将配置文件中的每一个< mapper> 节点抽象为一个 Mapper 接口,而这个接口中声明的方法和跟< mapper> 节点中的<select|update|delete|insert> 节点项对应,即<select|update|delete|insert> 节点的id值为Mapper 接口中的方法名称,parameterType 值表示Mapper 对应方法的入参类型,而resultMap 值则对应了Mapper 接口表示的返回值类型或者返回结果集的元素类型。

根据MyBatis 的配置规范配置好后,通过SqlSession.getMapper(XXXMapper.class) 方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper 实例,我们使用Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select(“statementId”,parameterObject);或者SqlSession.update(“statementId”,parameterObject); 等等来实现对数据库的操作。

动态代理

Mapper接口并没有实现类,它又是如何去完成实际的数据库操作的呢。这里底层实现是使用了动态代理,看如下代码,定义了cn.test.mybatis.mapper.UserMapper接口

public interface UserMapper {
    void save(User u);
}

UserMapper.xml映射文件

<mapper namespace="cn.test.mybatis.mapper.UserMapper">
    <insert id="save">
        INSERT INTO user (id, username, password) VALUES (NULL, #{username}, #{password})
    </insert>
</mapper>

UserServiceImpl业务方法

public void save(User u) throws IOException {
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession session = factory.openSession();
        UserMapper mapper = session.getMapper(UserMapper.class);
        mapper.save(u);
        session.commit();
        session.close();
    }

代码中可以看出,我们只需要给MyBatis提供Mapper接口和与之匹配的映射文件,就能够让MyBatis按照我们的需求执行到对应的SQL,主要代码再 getMapper 方法中,DefaultSqlSession:

public <T> T getMapper(Class<T> type) {
        return this.configuration.getMapper(type, this);
    }

Configuration:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return this.mapperRegistry.getMapper(type, sqlSession);
    }

MapperRegistry:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
        if(mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        } else {
            try {
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception var5) {
                throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
            }
        }
    }

该方法中最关键代码:mapperProxyFactory.newInstance(sqlSession)。继续看MapperProxyFactory:

protected T newInstance(MapperProxy<T> mapperProxy) {
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
        return this.newInstance(mapperProxy);
    }

在该类中可以看到 Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);最终由JDK的动态代理,动态的为我们在内存中创建了一个代理对象

MyBatis 引用Mapper 接口这种调用方式,纯粹是为了满足面向接口编程的需要。(其实还有一个原因是在于,面向接口的编程,使得用户在接口上可以使用注解来配置SQL语句,这样就可以脱离XML配置文件,实现“0配置”)

数据处理层

数据处理层可以说是MyBatis 的核心,从大的方面上讲,它要完成两个个功能:1. 通过传入参数构建动态SQL语句;2. SQL语句的执行以及封装查询结果集成List<E>

动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis 通过传入的参数值,使用 Ognl 来动态地构造SQL语句,使得MyBatis 有很强的灵活性和扩展性。参数映射指的是对于Java 数据类型和jdbc数据类型之间的转换:这里有包括两个过程:查询阶段,我们要将java类型的数据,转换成jdbc类型的数据,通过 preparedStatement.setXXX() 来设值;另一个就是对resultset查询结果集的jdbcType 数据转换成java 数据类型。

动态SQL语句生成之后,MyBatis 将执行SQL语句,并将可能返回的结果集转换成List< E> 列表。MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
分布式全局唯一ID解决方案 分布式全局唯一ID解决方案
在公司去Oracle的要求下,需要对原有系统进行一些改造优化,由此引出了对分布式全局ID的一些思考。在平常的开发工作中可能不会对id做考量,一般都是使用数据库自带的自增主键或是sequence函数来生成id,但是随着业务的发展,系统会越来越
2022-02-19
Next 
分区分表分库 分区分表分库
互联网时代,传统应用都有这样一个特点:访问量、数据量都比较小,单库单表都完全可以支撑整个业务。随着业务的发展和用户规模的迅速扩大,对系统的要求也越来越高。因此传统的MySQL单库单表架构的性能问题就暴露出来了。而有下面几个因素会影响数据库性
2022-02-09
  TOC