欢迎在文章下方评论
我们知道mybatis可以看成是对jdbc的一种封装。二为什么要封装和封装什么呢?如下
在之后我们会一一来聊聊这些封装的具体细节!
谈到mybatis,一开始我们接触的应该就是SqlSession吧。当然,现在我们项目用的比较多的是SqlSessionTemplate(对SqlSession等做了封装)。
分三步:
1. 通过配置文件获取数据库连接相关信息:Reader reader = Resources.getResourceAsReader("com/yezhihao/www/config/Configuration.xml")
;
2. 通过配置信息构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessioFactoryBuilder.build(reader)
3. 通过SqlSessionFactory打开数据库会话
SqlSession sqlSession = sqlSeesionFactory.openSession()
在很多mybatis的初级教程中,上面的都是mybatis入门级代码,但对于我们来说,不能单单限于入门级。所以下面说下其原理。
Reader reader = Resources.getResourceAsReader("com/yezhihao/www/config/Configuration.xml")
这句话中就是配置文件的读入,任何框架的初始化,无非是加载自己运行时所需要的配置信息,源码没有聊的。SqlSessionFactory sqlSessionFactory = new SqlSessioFactoryBuilder.build(reader)
这句话中,建立了sqlSessionFactory,首先这是一种Builder模式应用,会根据情况提供不同的参数,创建不同的sqlSessionFactory。由于构造时参数不定,可以为其创建一个构造器Builder,将SqlSessionFactory的构建过程和表示分开。关于Builder模式,推荐大家看《设计模式之禅》
SqlSession sqlSession = sqlSeesionFactory.openSession()
在这里,我们就跟下源码,看看mybatis是操作数据库是一个怎么样的过程。 @Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
我们看到是调用openSessionFromDataSource()
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);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} 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();
}
}
(1)获取前面我们加载配置文件的环境信息,并且获取环境信息中配置的数据源。 (2)通过数据源获取一个连接,对连接进行包装代理(通过JDK的代理来实现日志功能)。 (3)设置连接的事务信息(是否自动提交、事务级别),从配置环境中获取事务工厂,事务工厂获取一个新的事务。 (4)传入事务对象获取一个新的执行器,并传入执行器、配置信息等获取一个执行会话对象。
说白了就是加载对应数据源,创建执行器。
UserInfo user = (UserInfo) session.selectOne("User.selectUser", "1");
为例。进入源码,我们发现在sqlsession这个接口有三个实现类分别是DefaultSqlSession,SqlSessionManager,SqlSessionTemplate(之后会讲)。这里,我们能看到DefaultSqlSession里面有各种各样的SQL执行方法,主要用于SQL操作的对外接口,它会的调用执行器来执行实际的SQL语句。
进入源码,我们发现
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
调用的是本类的selectList()方法 看看:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
根据SQL的ID到配置信息中找对应的MappedStatement,在之前配置被加载初始化的时候我们看到了系统会把配置文件中的SQL块解析并放到一个MappedStatement里面,并将MappedStatement对象放到一个Map里面进行存放,Map的key值是该SQL块的ID。 调用执行器的query方法,传入MappedStatement对象、SQL参数对象、范围对象(此处为空)和结果处理方式。
对我来说,mybatis的源码不是一般的复杂(哭。。。)找了好久,
首先在BaseExecutor,找到相应query()方法,
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
在这里,mybatis先会看看在其缓存中有没有相应的数据,如果有就返回,没有的话,通过调用queryFromDatabase(),很明显,命名上就是从数据库查找。进去看下,发现是通过调用doQuery()来最终执行sql看下doquery()
@Override
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();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
最后,在PreparedStatementHandler()中找到了query
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
终于,我们发现了在jdbc中熟悉的代码,在这里,执行了直接操作数据库的语句。
在跟源码的时候发现,mybatis真的封装的很好,看看能不能出个mybatis的整体架构的博文,努力。
SqlSessionTemplate是MyBatis-Spring的核心。这个类负责管理MyBatis的SqlSession,调用MyBatis的SQL方法。SqlSessionTemplate是线程安全的,可以被多个DAO所共享使用。 我们在结合spring的时候,我们会发现SqlSessionTemplate是单例的。通过源码我们何以看到 SqlSessionTemplate 实现了SqlSession接口,也就是说我们可以使用SqlSessionTemplate 来代理以往的DefailtSqlSession完成对数据库的操作,但是DefailtSqlSession这个类不是线程安全的,所以这个类不可以被设置成单例模式的。在我们就疑惑了,怎么判断一个类是否为线程安全的。可以移到我的博客线程安全基础
这样就可以对操作数据库了,是不是很神奇呢?我想你一定会想知道它是怎么实现的吧!不急,在下面我们就好好说说它的原理。
接口式编程的要求和规范说完之后,我们就来说下它的原理吧。
下面,我们就以下面的代码(没有spring结合)作为入口,讲下源码。
User user = sqlSession.getMapper(User.class);
userList = user.queryUserList();
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
很明显,首先是根据配置和传入接口产生代理工厂,其中mapperProxy是处理类,然后生成一个代理类,然后返回。为什么sqlSession.getMapper(.class)可以根据传入的参数,返回一个对应的类型,因为泛型,mybatis已经为我们完成了强转。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
上面代码中,首先是无法进入if的,因为没有实现类。然后是调用
final MapperMethod mapperMethod = cachedMapperMethod(method);
在这里得到queryUserList()方法的代理并在下一行mapperMethod.execute(sqlSession, args);
被调用。
最后:Mybatis加载文件时,利用namespace加载了一个class,然后把这个class与代码中传入接口的class进行匹配,方法执行所需要的信息就是来自于已经匹配成功的配置文件中,当结果与配置文件对应上后,调用接口的方法执行sql语句。在这里没什么好学,都是一些简单业务,就不一一演示了。自己跟下源码吧。MapperMethod->SqlCommand->...
更下去自然会发现。