demo 是根据 mybatis 官方给出的示例来写的。平是我们都是止步于会用,但是如果只是到这一层就不继续深入的话,我们永远都是一个 API 搬砖工。所以我们还是要继续往下走。
在自己建立的demo工程中,使用了推荐的xml配置方式,数据库连接信息和用来编写 sql 的 xml 文件路径都保存在了 config 配置文件中。然后通过 mybatis 自带的 stream 流读取配置文件信息。并由此创建出一个 sqlSessionFactory。
创建这个 demo 不是为了简单的复现这些逻辑和代码,是为了完整地了解在 democase 里,完成一次数据查询究竟做了哪些操作。
import com.gaop.model.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.InputStream;
/**
* @author [email protected]
* @description
* @date 2019-05-04 21:52
**/
public class MybatisTest {
//DBConfig/mybatis-config.xml
@Test
public void queryTest() {
try {
String resource = "DBConfig/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
Student student = sqlSession.selectOne("com.gaop.mapper.StudentMapper.selectStudent", 1);
System.out.println(student);
sqlSession.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
简单总结一下在这个示例demo中的代码,都做了哪些操作:
- 连接数据库
- 执行一个 sql 查询数据获取数据
- 将从数据库中查到的数据映射成 java 对象返回
- 关闭数据库连接(此时我们还不确定这个关闭连接的操作是在步骤2还是在步骤3完成之后做的)
然后我们简单根据代码来跟一下处理的流程:
- 使用 mybatis 封装的一个流从项目的相对路径下加载到配置文件数据
- 依赖前一步数据流加载,构建一个 sqlSessionFactory 对象
- 打开一个 sqlSession 会话对象
- 执行一个查询操作并返回了一个我们期望的可以直接使用的 java 对象
- 关闭 sqlSession
然后依次来分析每一个步骤
一 加载配置文件
目标配置文件中,包含了数据库连接信息、mysql 连接驱动名和 sql 语句映射 xml 文件地址。因此,这一步是在做数据库连接的准备工作。只有获取并加载到这个配置文件,我们才能去连接数据库。整个加载过程,大量地使用了 建造者设计模式。最终配置文件上的全部内容解析得到一个 Configuration 实例。实例上包含了配置文件上的所有信息。
ps:到源码一步步跟,可以看到这里的解析工具,还是用的 jdk 提供的DOM解析的方式。解析生成一个 Document 对象实例。这里有关对于 XML 文档编写规则的设计,我们在自己设计xml标签的时候,也可以学习一下,就是给出明确的 root 根标签,所有关键的信息都必须成为这个根标签的子标签信息。然后用这个根标签就可以获取到全部的配置信息,然后再解析。
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
得到这个 config 实例之后,SqlSessionFactoryBuilder 内一个专用的构建方法利用实例创建出一个最终可用的 SqlSessionFactory 对象实例。这个实例比较重,所以官方文档一直在建议只需要创建一个全局的实例。如果是在 Spring 中,那么只需要依赖 Spring 的 bean IOC 即可创建一个稳定可用的全局单例。不过如果要自己单独玩,那么可以试试 DCL 的单例设计模式,如果我们的工程确定要连接数据库的话,这种实例就可以直接用比较简单的 饿汉单例。
由上我们可以知道,配置文件中, configuration 为根节点,其根节点内包含一系列的子节点信息,比如 ==environment,typeAliases,properties和用户保存sql语句的mapper== 等等。
二 构建一个 SqlSessionFactory 对象实例
有了前面的一步的铺垫,后面的构建动作就比较简单了,SqlSessionFactoryBuilder 对象内有直接以 config 实例为入参的构建方法。方法里面点开看也很简单,整个工厂类的核心内容就是私有的 config 实例。
三 打开一个 sqlSession 会话对象
SqlSessionFactory 本身是一个标准接口,使用 openSession 的时候实际是调用到了其具体的实现,这里有两个实现。
示例实际上是调用到了 DefaultSqlSessionFactory 的 openSession 方法。
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 获取环境配置信息,每一个环境可以单独配置不同的数据源与数据库连接信息,比如用于区分 dev/beta
final Environment environment = configuration.getEnvironment();
// 根据获取的环境配置属性创建一个事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 获取一个可用的事务实例
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 获取一个执行器实例
final Executor executor = configuration.newExecutor(tx, execType);
// 根据前面得到的 事务实例/执行器实例和传入的是否自动提交事务配置,构建一个默认的 sqlSession 实例并返回
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();
}
}
四 执行一个查询操作并返回一个可用的 javabean
@Override
public T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List list = this.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;
}
}
这是 DefaultSqlSession 下对 sqlSession 的默认实现类。传入之前映射解析好的 sql 语句和入参,执行一次预期返回多个结果的查询。检查获取结果集中元素的数量,如果超过了 1 个,就对外抛出异常。这里还是能看到代码复用的习惯, selectOne 复用了 selectList 的代码。
@Override
public List 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();
}
}
DefaultSqlSession#selectList 下的列表查询,这里新增的一个参数是 RowBounds,用于查询分页,是 mybatis 内置的一个分页参数类。查看这个分页类的代码可以看到:这里默认的分页,是一个普通的逻辑分页,查询数据的起始下标是 0,而终点是 Integer.MAX_VALUE。
我们在测试类中传入用于映射 sql 语句的参数,是
com.gaop.mapper.StudentMapper.selectStudent
这个入参路径,是我们配置在 sqlMapper 中的语句映射路径,其格式是:namespace+id 拼接。所有的 sql 都是在初始化的时候就已经加载完成了。他们被加载保存在一个 map 数据结构中,其 key-value 映射关系就是依赖我们配置的 namespace+id,拿着这个 key 就可以找到对应的 sql 语句。所以看到这里我们就可以知道 为什么要求 mybatis sql 语句的 namespace+id 的设置要全局唯一,如果不唯一,保存到 map 里面,在查找映射的 sql 语句的时候就会出问题。
sql 语句的执行操作最终还是绑定到了执行器 executor 上面了。executor 简单分类的话,分为
- 基础执行器
- 简单执行器
- 复用执行器
- 批处理执行器
- 缓存执行器
执行器的细节,后面再讲。
public List 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 list;
try {
// 缓存结果集的获取
queryStack++;
list = resultHandler == null ? (List) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 执行一次 DB 请求获取数据
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;
}
这里我们直接看到基础执行器的细节代码,使用到了一级缓存,即我们调用某个查询 sql 的结果集,可能不是简单的获取-返回,而是在框架内做了缓存处理,如果此时的缓存仍然是有效的,那么此次的查询根本就没有打到数据库上,直接获取到缓存结果集并返回了。然后对应的数据库连接和缓存操作等等,本来是 JDBC 流程里需要我们手动处理的操作,都被封装到了这里,我们在实际使用中都不需要再去关注实际的内容。框架已经帮我们把活干了。
demo 的大致流程梳理就到这里,后面我们再根据这个基础,依次分析总结我们用到的各个关键组件。