Mybatis源码解析--SqlSession
SqlSession介绍
Mybatis框架的主要目的就是简化JDBC操作数据库的繁琐流程(忘了JDBC流程的可以参考这篇),只需要提供sql语句和相关参数即可,不用再对参数进行手动设置,以及手动遍历结果集封装目标对象,不用担心资源的释放等等。而SqlSession
这个类是整个Mybatis框架提供用于操作数据库的入口,提供了相关API,开发人员只需要了解该类提供方法就可以对数据库进行操作。下图展示了SqlSession在Mybatis中所起的作用。
Mybatis通过对外暴露SqlSession这个类,使得开发人员不需要了解Mybatis内部具体实现的情况下就能快速操作数据库(门面模式的思想),本篇主要按照官网的Demo来学习SqlSession的创建过程、相关API的学习以及提供StatementId和Mapper代理对象两种接口调用方式的对比。
SqlSession的创建
构建SqlSessionFactory
我们模拟官网的示例写一个demo
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
通过Mybatis提供的Resources
工具类将配置文件转换成流对象交给SqlSessionFactoryBuilder,并通过它
构建SqlSessionFactory工厂对象
。这里使用到了建造者模式和工厂模式,那我们研究一下SqlSessionFactoryBuilder
是如何构建SqlSessionFactory。
SqlSessionFactoryBuilder类
SqlSessionFactoryBuilder
提供了两大类重载的方法:1) 通过Reader
构建 2) 通过InputStream
构建
通过Reader
构建:通过入参构建一个XMLConfigBuilder
对象,该对象的目的就是解析Mybatis配置文件,将配置文件中的静态配置通过解析封装到Configuration这个对象里面。
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
//对Mybatis配置文件的解析细节全部由该类封装
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
}
通过InputStream
构建:和上面的处理方式一样,只不过使用InputStream流对象;两者的区别这里就不再提了。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
//对Mybatis配置文件的解析细节全部由该类封装
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
}
最终都通过调用公共方法build(Configuration)创建SqlSessionFactory工厂对象
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
通过上面的源码跟踪,我们看到了它的“庐山真面目”,原来是通过Configuration构建
了一个DefaultSqlSessionFactory
对象(SqlSessionFactory
接口的默认实现类)
创建SqlSessionFactory时序图
SqlSessionFactory提供的API
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
}
SqlSessionFactory
提供了大量创建SqlSession
的API,上面已经知道Mybatis实际创建的是DefaultSqlSessionFactory对象
,通过查看该类的具体实现,我们可以将上面的API分为两大类:1) 通过DataSource获取 2)通过Connetion获取
通过DataSource获取
这块请对照DefaultSqlSessionFactory的源码一起查看,可以得出下图,左边这些实现方法最终都调用右边这个方法
openSessionFromDataSource方法具体实现
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
final Environment environment = configuration.getEnvironment();
// 事务相关
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 创建Executor
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
看到这块可能会有点懵,Enviroment
、TransactionFactory
、Executor
这3个对象是干什么的,好像只有TransactionFactory
似乎认识,应该和事务有关。其实我们可以先只关心返回值和创建返回值的入参。下面对这3个入参进行分析:
-
configuration:该对象是解析Mybatis配置文件后对应的配置管理类,在创建
DefaultSqlSessionFactory
时通过构造传入作为其一个属性 -
executor:创建的执行器,sqlSession会将所有的操作最终交给executor去完成,SqlSession仅仅是一个外壳。对外统一暴露,隐藏了内部具体实现。
-
autoCommit:是否自动提交事务
通过DataSource获取
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
boolean autoCommit;
try {
// 从connection获取是否自动提交信息
autoCommit = connection.getAutoCommit();
} catch (SQLException e) {
// Failover to true, as most poor drivers
// or databases won't support transactions
autoCommit = true;
}
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 通过connection对象创建事务对象
final Transaction tx = transactionFactory.newTransaction(connection);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
这个方法和上面的类似,主要区别在于是否自动提交和事务的创建是通过Connection
完成的
SqlSession的内部原理
上面我们已经知道了SqlSession的创建流程并得到了SqlSession对象,该对象是Mybatis对数据库操作的门面类,所有操作都是通过它完成。内部提供了事务相关、资源的关闭、增删改查等API。其中事务我们不在这讨论,只关心增删改查的操作,可以对其分成两大类:StatementId和Mapper接口。
第一类:statementId方式相关接口
-
T selectOne(String statement, Object parameter)
-
List<E> selectList(String statement, Object parameter, RowBounds rowBounds)
-
Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds)
-
Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds)
-
void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)
-
int insert(String statement, Object parameter)
-
int update(String statement, Object parameter)
-
int delete(String statement, Object parameter)
通过对SqlSession
提供API分类后,我们发现有几个频繁出现的参数String statement
、RowBounds rowBounds
、Object parameter
,我们先来看一下这些入参信息。
String statement
该参数用来标识一个SQL语句,一般我们会在Mapper的xml配置文件或者是在Mapper接口方法上使用注解定义SQL,这两种方式定义的SQL最终都会被Mybatis框架解析封装到某个类(MappedStatement)中交给”大总管“Configuration
进行管理,并且每个SQL都会有一个唯一标识,所以这个statement
参数就是那个唯一标识用来获取对应SQL的作用。
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.keminapera.mybatis.mapper.StudentMapper">
<insert id="insertStudent" parameterType="com.keminapera.pojo.Student">
insert into student (n_id, c_name, n_age, c_school) values (#{id}, #{name}, #{age}, #{school})
</insert>
</mapper>
xml方式通过解析生成的唯一标识是由<mapper>
标签的namespace
属性和某个<insert><update><select><delete>
标签的id属性组合而成,一般namespace对应Mapper接口的全限定类名,id是对应Mapper接口内的方法名.(虽然StatementId方式可以随意指定,只要唯一即可)
public interface StudentMapper {
@Insert({"INSERT INTO student",
"(n_id, c_name, n_age, c_school)",
"VALUES",
"(#{id}, #{name}, #{age}, #{school})"})
int insertStudent(Student student);
}
注解方式默认会将Mapper接口的全限定名+对应方法名作为唯一标识
RowBounds rowBounds
该类是Mybatis框架提供用来分页的,注意该类只能进行逻辑上分页,本质还是将数据库所有结果加载到内存,然后再进行截取的方式,推荐使用流式查询
或分页插件
public class RowBounds {
public static final int NO_ROW_OFFSET = 0;
public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
public static final RowBounds DEFAULT = new RowBounds();
private final int offset;
private final int limit;
public RowBounds() {
this.offset = NO_ROW_OFFSET;
this.limit = NO_ROW_LIMIT;
}
public RowBounds(int offset, int limit) {
this.offset = offset;
this.limit = limit;
}
}
内部两个属性,offset
指定从哪个下标开始读取,limit
指定读取多少条数据
Object parameter
该参数是用于给sql进行传参.一般查询会有查询条件,添加需要指定新对象,修改需要更新后的对象,删除需要指定记录的标识,这些数据是要替换掉前面sql
中的占位符,最终才能交给DBMS执行.
上面已经理清了这些入参的含义,下面我们主要看一下这些方法的内部具体实现
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
selectMap方法
public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
final List<? extends V> list = selectList(statement, parameter, rowBounds);
final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<>(mapKey,
configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
final DefaultResultContext<V> context = new DefaultResultContext<>();
for (V o : list) {
context.nextResultObject(o);
mapResultHandler.handleResult(context);
}
return mapResultHandler.getMappedResults();
}
selectCursor方法
public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
MappedStatement ms = configuration.getMappedStatement(statement);
Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds);
registerCursor(cursor);
return cursor;
}
insert方法
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
注意: 这块insert
操作调用update
操作
public int delete(String statement, Object parameter) {
return update(statement, parameter);
}
注意: 这块delete
操作调用update
操作
public int update(String statement, Object parameter) {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
}
和上面select
操作一样,都是先获取MappedStatement
,最后转交给executor
去完成
第二类:mapper代理对象方式相关接口(推荐)
-
T getMapper(Class<T> type)
该方法会返回一个Mapper接口的代理对象,代理对象的生成过程请参考这篇博客
两种方式的使用示例
StatementId方式:通过一串字符
@Test
public void insertStudentWithStatementId() {
try(SqlSession sqlSession = sessionFactory.openSession(true)){
int count = sqlSession.insert("com.keminapera.mybatis.mapper.StudentMapper.insertStudent", getStudent());
}
}
mapper接口方式:通过指定的mapper接口类对象
@Test
public void insertStudentWithMapper() {
try(SqlSession sqlSession = sessionFactory.openSession(true)){
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
studentMapper.insertStudent(getStudent());
}
}
两种方式的比较
通过上面示例的比较,很明显使用mapper代理的方式更面向对象,更容易理解,只需要从SqlSession
中获取对应的Mapper对象,然后调用内部方法即可。相比第一种,需要传入一个字符串字面量,而且要和SQL标识保持一致,容易出错
作用域和生命周期
SqlSessionFactoryBuilder:通过上面源码的分析,我们知道在该类中对Mybatis的配置文件进行了解析,最后得到一个configuration对象,通过configuration对象创建了一个DefaultSqlSessionFactory返回。配置文件的解析伴随着io读取操作,以及里面标签的解析,所以很重;再说,一般我们直接一次性将Mybatis配置好的,要是没有修改,没有必要进行多次解析。官网推荐最好是方法的局部变量,不保存引用。
SqlSessionFactory:该对象可以反复使用,主要作用是创建sqlsession对象,该对象作用域是整个应用范围
SqlSession:该对象线程不安全,主要作用就是执行数据库相关操作
小结
SqlSession是整个Mybatis对外暴露的门面类,通过它进行数据库操作,为了简化创建SqlSession
的过程框架提供了几个辅助类,整体之间的关系如下:
建造好SqlSession
对象后发现该对象内部什么都没有干,最终全部转交给Executor
对象完成,调用关系图如下:
涉及的设计模式:门面模式、建造者模式、工厂模式
注意事项:SqlSession对象不是线程安全的,所以每次操作都要获取一个新的SqlSession对象