从一个简单例子聊 MyBatis(一)
本文通过一个简单的```select`` 查询语句,通过源码分析聊聊Mybatis的运行机制以及他是如何处理SQL语句的。文末会给出这个例子的源代码。
我是一个栗子
先看一下这个例子,这个例子是最“纯粹”的MyBatis的使用,并没有结合Spring。
首先在单元测试代码里,通过studentService
查询id为1的学生,代码如下
@Test
public void testFindStudentById(){
Student student = studentService.findStudentById(1);
Assert.assertNotNull(student);
System.out.println(student);
}
studentService
中findStudentById
的代码如下,通过MyBatisSqlSessionFactory
获取sqlSession
, 再通过sqlSession
传递studentMapper得到接口实现,最后执行查询方法。
public Student findStudentById(Integer studId){
SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();
try {
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
return studentMapper.findStudentById(studId);
}finally {
sqlSession.close();
}
}
StudentMapper接口的定义如下:
public interface StudentMapper {
.....
Student findStudentById(Integer id);
.....
}
XML映射文件如下:
<?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="dao.StudentMapper">
....
<select id="findStudentById" parameterType="int" resultType="Student">
SELECT STUD_ID AS STUDID,NAME,EMAIL,DOB FROM STUDENTS WHERE STUD_ID=#{ID}
</select>
.....
</mapper>
那么MyBatis是如何找到XML的映射文件的呢?
下一节会通过源码分析深入理解MyBatis的执行过程和各个组件的作用。
MyBatis的层次结构
上面的例子中MyBatisSqlSessionFactory
主要做了两件事:
1、SqlSessionFactoryBuilder
通过读取mybatis-config.xml
配置文件的方式生成Configuration组件并且返回SqlSessionFactory对象。
2、通过SqlSessionFactory
中的openSession
方法获取sqlSession
。
private static SqlSessionFactory sqlSessionFactory;
public static SqlSessionFactory getSqlSessionFactory(){
if(sqlSessionFactory == null){
InputStream inputStream;
try{
inputStream = Resources.getResourceAsStream("mybatis-config.xml");
//(1)读取配置文件生成Configuration
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}catch (IOException e){
System.out.println(e.getMessage());
}
}
return sqlSessionFactory;
}
public static SqlSession openSession(){
//(2)返回sqlSession对象
return getSqlSessionFactory().openSession();
}
Configuration
进一步跟进上述代码中的build方法。可以看到这里是生成XMLConfigBuilder来解析配置文件。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
.....
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
....
}
在XMLConfigBuilder通过parse
函数返回Configuration
, 而该函数最终调用parseConfiguration
来解析我们写的mybatis-config.xml
文件中的各个元素。这里我们抓到了Mybatis的第一个组件Configuration
public Configuration parse() {
....
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//对Mapper进行解析
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我们以上述mapperElement
方法为例,看一下MyBatis是如何解析mybatis-config配置文件的<mappers>
元素的。
解析的mappers的调用链路比较长,最终我们发现是调用XMLStatementBuilder
的parseStatementNode
方法,这里的id 的值就是<select>
标签中的findStudentById
。
public void parseStatementNode() {
.......
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver);
}
跟进到addMappedStatement
方法中,可以看到该方法对id进行了处理
public MappedStatement addMappedStatement(
....
// 对id加上了接口的全限定名。id就变成了dao.StudentMapper.findStudentById
// 可以唯一找到namespace为dao.StudentMapper下面id = findStudentById的MappedStatement
id = applyCurrentNamespace(id, false);
....
MappedStatement statement = statementBuilder.build();
// 把生成的MappedStatement保存到configuration中
configuration.addMappedStatement(statement);
return statement;
}
进入Configuration
中的addMappedStatement
方法看到MapperedStatement最终被放到了configuration的map中
....
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>
.....
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
....
经过上面的分析我们看到在Mybatis中,每一个<select>
、<insert>
、<update>
、<delete>
标签,都会被解析为一个MappedStatement对象。
Configuration
MyBatis所有的配置信息都维持在Configuration对象之中。
Executor
回到MyBatisSqlSessionFactory
中的openSession
方法,最终定位到DefaultSqlSessionFactory
类中的openSessionFromDataSource
方法,在这个方法中,我们看到了创建出了MyBatis的四个核心组件:Environment
、TransactionFactory
、Executor
、DefaultSqlSession
。这里主要分析后面两个组件,Environment
、TransactionFactory
留到后面有机会在讲。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//(1) 使用configuration创建Executor
final Executor executor = configuration.newExecutor(tx, execType, autoCommit);
// (2) 返回默认的DefaultSqlSession
return new DefaultSqlSession(configuration, executor);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
进一步进入newExecutor方法, 如果没有在配置文件中指定,MyBatis 默认的ExecutorType 为Simple, cacheEnabled 默认为true。
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
//(1) 创建SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
//(2) 创建代理executor,cacheEnabled 默认true
executor = new CachingExecutor(executor, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
查看的SimpleExecutor的继承结构看到它继承BaseExecutor。
image.png
这里找了MyBatis另一个核心组件BoundSql, 我会在下一篇文章中介绍。下面我们一起分析一下BaseExecutor的query方法。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1.根据具体传入的参数,动态地生成需要执行的SQL语句,用BoundSql对象表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 2.为当前的查询创建一个缓存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
....
//(1)判断缓存是否有值
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//(2) 没有的情况下从数据库读取
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
}
.....
return list;
}
queryFromDatabase方法会调用子类的doQuery执行查询。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//(1)调用子类的doQuery执行查询,返回List 结果,然后将查询的结果放入缓存之中
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
查看子类SimpleExecutor中的doQuery方法。可以看到通过StatementHandler实例,执行SQL语句,这里我们找到了StatementHandler核心组件,该组件将在下一篇文章中介绍。
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//(1)根据参数创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
//(2)创建java.Sql.Statement对象,传递给StatementHandler对象
stmt = prepareStatement(handler, ms.getStatementLog());
//(3)调用StatementHandler.query()方法,返回List结果集
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
Executor
MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
SqlSession
在分析Executor的那一节我们看到opensession返回的是DefaultSqlSession
,这里我们找到了MyBatis的了另外一个核心组件SqlSession。查看DefaultSqlSession
的接口可以看到包含了所有增删改查的接口。
定位SqlSession
的查询方法,发现最终会调用selectList
。进入该方法,可以看到先是通过configuration得到我们之前分析的MappedStatement, 然后交给Executor 做具体的执行。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//(1) 采用之前生成的Configuration获取配置文件对应的Mapper
MappedStatement ms = configuration.getMappedStatement(statement);
//(2) 调用MyBatis的Executor执行查询
List<E> result = executor.<E>query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
SqlSession
SqlSession提供了MyBatis操作数据库的API,包含了增删改查接口,是MyBatis的顶层接口
总结
本篇文章通过代码分析了MyBatis的Configuration、Executor、SqlSession的核心组件,说明了他们的主要功能,下一篇文章会解析其他核心组件,并会分析SQL语句在MyBatis中的执行流程。
PS:
测试项目源代码MyBatisTest
数据库脚本如下
create table STUDENTS(
stud_id int(11) not null auto_increment,
name varchar(50) not null,
email varchar(50) not null,
dob date default null,
primary key(stud_id)
);
insert into STUDENTS(stud_id,name,email,dob) values(1,"student1","student1@email.com","1999-08-08");
insert into STUDENTS(stud_id,name,email,dob) values(2,"student2","student2@email.com","2000-01-01");
select * from STUDENTS;
网友评论