先从简单的源码入手:MyBatis 工作原理分析
做 Java 开发的同学都清楚,目前最流行的两种 ORM 框架就是 JPA 和 MyBatis。有说法是国内用 mybatis 的多,国外用 JPA 的多,JPA 的底层就是 Hibernate。
大家在开始接触 Java web 开发的时候都会碰到 SSH 和 SSM。前些年比较流行的框架组合是SSH,也就是Struts2、Spring 和Hibernate 的整合。SSM 框架是 Spring 、Spring MVC 和 mybatis 的整合,是最近几年比较流行的。
就我的经验来看,如果项目的数据比较单纯、结构比较简单,用 JPA 可以大大的提高开发效率。相反,如果项目的数据结构比较复杂,反应到 SQL 层面就是需要编写比较复杂灵活的 SQL 语句,比如两三张表联查等,那最好还是选择 mybatis ,更加灵活自由一点。
今天试着来说一下 mybatis 的工作原理。
mybaits 与 SQL 执行过程
第一步:创建数据库连接,有连接池的情况下,就从连接池获取。
想当年在学校使用 vb 的日子,哪里用什么数据库连接池,都是一个连接一个连接的创建和关闭。
第二步:打开连接;
第三步:数据库层进行缓存查询、语法解析、加锁等处理;
第四步:返回数据,mybatis 拿到返回的数据并抽象到 Java 实体;
第五步:关闭连接,或者将连接返还给线程池;
mybatis 参与其中一、二、四、五这几个步骤,来简化我们的数据库操作流程。
mybatis 负责向数据库或向连接池申请数据库连接,然后打开连接,调用数据库 API 执行 SQL,然后拿到返回的数据。
mybaits 在这一过程中要对 SQL 语句进行加工,并对返回的数据进行抽象,抽象到对应的 Java 实体。
纯粹的使用 mybatis
我们在平时的开发中,一般都是用 Spring MCV 或者 Spring Boot,而在这两个框架之下使用的其实并不是单纯的 mybaits,而是 mybatis-spring,它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException。最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring。这个我们以后再说,先说如何纯粹的使用 mybatis。
我创建了一个非 Java web 项目,用来演示一下 mybaits 简单的使用方法。数据库是 mysql,仅创建了一个简单的 user 表做测试,数据库命名为 study,sql 如下:
1、引入 mybatis 包,目前最新版本是 3.5.4。同时,还要引入 mysql 包。
2、声明 user 表对应的 Java 实体类
3、创建对应的 mapper 接口类,并定义一个 selectOneUser 方法,根据 id 查询用户,返回用户实体对象
4、编写对应的 UserMapper.xml 文件,其中有上面接口定义的 selectOneUser 方法的具体 sql 语句。
5、数据库相关参数的配置文件 config.properties
6、定义 mybatis 核心配置文件 mybaits-config.xml,具体配置含义请看文件内注释。
7、在代码中执行,查询并打印返回值
执行之后,会正常的打印出 id 为 1 的记录。
整个项目结构如下图。
mybaits 两类 xml 文件
mybatis 主要由配置信息和具体的 XML 映射文件组成,也就是上面提到的 mybatis-config.xml 和 UserMapper.xml(以及同类型的映射文件),使用 mybatis 的过程大部分就是完善和添加这两类 xml 文件的过程。
mybatis 全局配置
下面列出了 mybatis-config.xml 配置节点层次结构。
对应到 xml 文件中就是这个样子,以 configuration 为根节点,
<configuration>
<!--引入 config.properties 配置文件-->
<properties resource="config.properties">
</properties>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
<typeAliases>
<typeAlias alias="User" type="org.kite.purely.mybatis.entity.User"/>
</typeAliases>
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
<!--定义 development 作为默认配置环境-->
<environments default="development">
<environment id="development">
<!--事务管理类型为 JDBC-->
<transactionManager type="JDBC"/>
<!--数据源参数设置-->
<dataSource type="POOLED">
<property name="driver" value="${driverClass}"/>
<property name="url" value="${jdbcUrl}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>
<!--引入 mappers -->
<mappers>
<package name="org.kite.purely.mybatis.mapper"/>
</mappers>
</configuration>
在官网上有对这些配置项的详细解释:https://mybatis.org/mybatis-3/zh/configuration.html,建议使用 mybatis 的同学必须读几遍。
其中有些是必要的,有些是可以选择配置的,比如 dataSource、mappers 是必须的。
settings 都有默认值,typeHandlers 一般也不需要自定义,plugins 可以用作监控,比如记录多少次更新操作、多少次查询操作等。
SQL 映射文件配置
映射文件指的是那些 xxxMapper.xml, 是具体的 Java 实体类和数据库映射配置,比如上面提到的 UserMapper.xml,里面可以定义数据库的增删改查行为,以及查询参数、返回结果与 Java 类型的映射关系。我们系统中具体的数据库查询逻辑都在这里定义。
配置里的顶级元素有如下几个,使用过的同学再熟悉不过了,具体的使用方式在官网有详细说明和例子:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html
cache
– 对给定命名空间的缓存配置。cache-ref
– 对其他命名空间缓存配置的引用。resultMap
– 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。sql
– 可被其他语句引用的可重用语句块。insert
– 映射插入语句update
– 映射更新语句delete
– 映射删除语句select
– 映射查询语句
mybatis 执行流程和原理
从这段代码说起
public class App {
public static void main( String[] args ) {
FileReader fileReader = null;
try {
URL url = App.class.getClassLoader().getResource("");
fileReader = new FileReader(url.getPath()+"mybatis-config.xml");
}catch (IOException e){
e.printStackTrace();
}
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(fileReader,"development");
try (SqlSession sqlSession = factory.openSession()){
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectOneUser(1);
System.out.println(user.toString());
}
}
}
这段代码是一个简单的使用方法,分为如下几步:
1、 获取 mybatis-config.xml ,并将其填充到一个 FileReader 中;
2、 使用 SqlSessionFactoryBuilder.build(Reader reader, String environment) 这个重载方法生成 SqlSessionFactory 工厂实例;
3、 SqlSessionFactory 实例产生一个 SqlSession,SqlSessionFactory 最好设置为全局的单例模式;
4、 SqlSession 实例调用 getMapper() 方法,获取加工后的 UserMapper 实例,在一次事务结束后要关闭 SqlSession 实例;
5、调用 UserMappper 定义的方法;
SqlSessionFactory 工厂类创建
SqlSessionFactory 是一个工厂类接口,它的作用是产生 SqlSession,SqlSessionFactory 应该在项目中已单例形式存在,不要每次需要 SqlSession 的时候都重新 new SqlSessionFactory,SqlSessionFactory 创建的过程复杂,开销比较大。
SqlSessionFactoryBuilder 类提供了 9 个创建 SqlSessionFactory 的重载方法。
第一部分红色框和第二部分蓝色框都是从 mybatis-config.xml 读取配置,支持 Reader 和 InputStream 两种方式读取文件。
可以选择 environment ,比如我们的项目有开发环境、测试环境、生产环境,每个环境的数据库配置都有所不同,可以提前在 mybatis-config.xml 中配置多个 environment,然后再创建 SqlSessionFactory 的时候指定采用哪个。
还可以单独指定 properties,指定 properties 后会覆盖在 xml 文件中配置的属性。
mybatis 除了支持 xml 方式之外,也可以用纯 Java 代码的方式构造配置项,就是利用最后一个方法,其参数是 Configuration,比如下面这段代码。
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
当然了,上面的 8 个方法在获取配置文件中的配置后最终也是要调用这个方法的,最后返回的 SqlSessionFactory 实际是 DefaultSqlSessionFactory 实例,它实现了 SqlSessionFactory 接口。
大致的过程如下:
其中 parseConfiguration() 方法会读取 mybatis-config.xml 中的每一个顶层元素,对其进行相应的解析,下面代码每个调用方法的参数就是要解析的配置文件的对应元素。
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
解析完成后将对应的结果赋值给 org.apache.ibatis.session.Configuration 类中对应的属性,比如 protected Environment environment;
就是用来存放配置文件中的 environment 节点内容的。所有的配置文件中可配置的内容都在这个类中有对应的属性,具体代码你可以进去看一下。
构造 Configuration 对象是至关重要的一步,它相当于把你的配置文件抽象到当前上下文中,方便之后随时拿来使用。
SqlSession 创建
SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。SqlSession 通过 SqlSessionFactory 的 openSession() 方法获取。
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
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();
}
}
实际最后返回的对象是 DefaultSqlSession 实例,下图是 DefaultSqlSession 的继承关系。
从代码可以看出在构造 SqlSession 的过程中除了 configuration 这个参数,还有 executor 和 autoCommit。
executor 是 sql 语句的执行器,包含了执行 sql 语句的方法。共有四类 Executor,分别是 SimpleExecutor、ReuseExecutor、ReuseExecutor 和 ReuseExecutor,默认情况下都是选择 SimpleExecutor 执行器。以下是他们的继承关系图:
autoCommit 参数表示是否自动提交事务。当事务提交之后,才会释放对应的锁。
如何通过 mapper 接口执行具体的 sql
先思考一下,我们定义的 mapper 都是接口类,只有方法,没有实现。方法名在 mapper.xml 中有对应的关系。那么,我们调用 mapper 接口的方法的时候,是怎么对应到 mapper.xml 中的具体 sql 语句的呢。
答案是肯定有 mybatis 帮我们做了什么。一说到这种场景,自然就想到了一种设计模式-「动态代理模式」。
Spring AOP 的原理就是用了动态代理模式。不理解这种模式的同学可以看一下我的旧文 Spring AOP 和动态代理技术
下面我们来理一下这个过程。
在创建 SqlSessionFactory 的过程中会构造 Configuration 对象,其中就包括解析
//Configuration 类中存放 mapper 接口类集合的属性
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
下面看一下解析
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果使用的是 package 元素,获取 name 属性,
//然后查找包下的所有 mapper 接口类,并存入 mapperRegistry 中
//若使用 package 则只能使用注解的方式实现 sql 映射,不能使用 mapper.xml 文件
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class");
// 使用 resource 指定 mapper.xml 所在位置的方式
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//解析 resouce 文件,根据文件中的 namespace 反射构造接口 Class
// 并添加到 mapperRegistry
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//使用 url 方式,指定的 mapper.xml 是一个项目外目录,比如在服务器的某个目录下
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//直接指定 mapperClass
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
因为我们是用的 resource 属性配置的,
<mapper resource="mappers/UserMapper.xml"/>
所以,执行的实际上主要是这两行代码:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
主要的行为在 mapperParser.parse() 方法内:
public void parse() {
// 判断 mapper.xml 文件是否被加载过,如果没有
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
/**
* 解析 mapper.xml 中的各种元素,
* 比如 namespace、resultMap、parameterMap、sql 等
**/
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
/**
* 绑定 namespace 对应的 mapper 接口类
**/
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
其中 bindMapperForNamespace() 方法便是根据 mapper.xml 中指定的命名空间找到对应的 mapper 接口类,然后把 mapper 加入到 Configuration 中的结合中,看上面中的 configuration.addMapper(boundType)
就是关键,我们看看这个方法干了什么,就能大概清楚开头提到的那个问题 - mybatis 如何实现的调用接口方法就执行对应的 sql 语句。
Configuration 类中用了以下对象来存储 mapper 接口。
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
然而添加操作另有玄机
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
// 如何是注解方式才会
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
实际上最终保存 mapper 接口的属性是 knownMappers,它是个什么样子的呢
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
看到了吗,一个 map,key 是 Class 泛型,也就是对应具体的接口类,value 是一个 MapperProxyFactory 泛型,看名字也能看出个大概,这是一个 mapper 接口代理工厂类。
好,看完生成 mapper 接口类-准确的说是 mapper 接口类的代理工厂类集合后,我们来看一下取的时候,也就是 SqlSession.getMapper 方法,获取对应的 mapper 的过程。
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);
}
}
上面是 getMapper 接口的最终实现,在 knownMappers 中根据 mapper 接口类 type 获取 MapperProxyFactory 泛型,然后通过 newInstance 方法,返回一个实例,这也是工厂模式的标准写法。
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
最终构造除了一个代理类,所以我们调用 SqlSession.getMapper 方法后,返回的是 mapper 接口的代理 MapperProxy 泛型类。
而最终调用 mapper 接口类的方法,实际上是代理调用了 invoke 方法,并对原始方法进行了一些列加工,mybaits 这里的加工就是找到 mapper 接口方法对应的 mapper.xml 对应的 id 和方法名相同的元素块儿,并对 sql 代码块进行解析。
比如我这个例子中,我执行 UserMapper 接口的 selectOneUser 方法,执行的就是 UserMapper.xml 中对应的这部分:
<select id="selectOneUser" resultType="org.kite.purely.mybatis.entity.User">
select * from user where id = #{id} or id=#{id2}
</select>
以下代码是代理类的 invoke 方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
判断不是 Object 类,执行以下代码。
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
这段代码分两部分,首先调用 cachedInvoker(method) 方法,返回的是 PlainMethodInvoker,然后调用它的 invoke 方法
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
之后,execute 方法会根据 sql 语句的类型是 INSERT、UPDATE、DELETE、SELECT 做区分,然后执行对应的逻辑。
因为我这个方法是一个 select 方法,所以最后实际执行的方法是 DefaultSqlSession 的 selectOne 方法,command.getName() 就是方法全名,param 是方法参数。
result = sqlSession.selectOne(command.getName(), param);
而 selectOne 方法会调用 selectList 方法,然后根据返回记录数来决定是返回一个列表还是一个单独实体。
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();
}
}
MappedStatement 包含 sql 查询所需的所有属性,比如 resultMap 集合、SqlSource 等。
executor 是 sql 执行器,上面已经介绍了有四种执行器,在默认情况下 mybatis 是开启自身缓存的,所以用到的执行器是 CachingExecutor,这里执行的 query 方法就是 CachingExecutor 的 query 方法。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
首先判断是否存在缓存,如果存在,就从缓存直接获取,如果不存在,就执行 delegate 的 query 方法,delegate 是一个执行器,当缓存不存在时,delegate 就是 SimpleExecutor 执行器。
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.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
然后 prepareStatement() 是返回数据库连接并把参数设置到 sql 语句的占位符。
最后一步,就是将返回的数据集转换为对应的 Java 实体对象或集合。
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
这个过程还包括每个数据库字段类型映射到 Java 类型,也就是各种 typeHander。
总结
本篇只分析了 mybatis 工作的主要框架,另外,还有缓存实现、插件实现等诸多细节没有展开。源码这东西,看别人讲不如自己看,不仅能加深对开源框架的理解。而且,我们从优秀的开源代码中可以学到很多东西,比如说设计模式的运用、继承实现关系的运用等等。
跟诸多其他框架比起来,mybatis 算是比较简单的,有时间大家都可以读一读。
参考文章:
https://mybatis.org/mybatis-3/zh/configuration.html
https://mybatis.org/spring/zh/index.html
还可以读:
-----------------------
公众号:古时的风筝
一个斜杠程序员,一个纯粹的技术公众号,多写 Java 相关技术文章,不排除会写其他内容。
【今天小雨】