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 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。