Mybatis(一) - 概述

1. MyBatis的整体架构

首先小册先来讲解一下 MyBatis 的整体架构,这里面涉及到的内容可能小伙伴在刚开始学习 MyBatis 的时候就接触过了,不过由于是初学,可能当时理解起来不是那么直接,也可能没有完全的吃透,所以小册先来带各位回顾一下 MyBatis 的整体架构。

下面这张图,很多小伙伴看着都很眼熟吧:

img

自上而下我们一步一步来说:

  1. 从 MyBatis 解析全局配置文件开始,它会将其中定义好的 mapper.xmlMapper 接口一并加载,并统一保存到全局 Configuration 配置对象中;

  2. SqlSessionFactory 的构建,需要全局的 Configuration 配置,而 SqlSessionFactory 可以创建出 SqlSession 供我们与 MyBatis 交互;

  3. SqlSession 在执行时,底层是通过一个 Executor ,根据要执行的 SQL Id (即 statementId ),找到对应的 MappedStatement ,并根据输入的参数组装 SQL ;

  4. MappedStatement 组装好 SQL 后,底层会操作原生 jdbc 的 API ,去数据库执行 SQL ,如果是查询的话,会返回查询结果集 ResultSet

  5. MyBatis 拿到 ResultSet 后,由 ResultHandler 负责封装结果集,根据我们事先定义好的结果集类型,封装好结果集后返回。

这里面除了 SqlSessionFactorySqlSession 之外,有两个相当重要的类需要我们前置了解,之前的章节我们只是遇到它们时简单的提一下,但现在我们要掌握全局的生命周期,就必须对它们也深入了解。下面我们来详细了解一下它们。

2. Executor

Executor ,字面意为 “执行器” ,它在整个 MyBatis 的执行流程中起到了 “枢纽” 的角色,SqlSession 的所有 SQL 执行,底层都是委托 Executor 执行,而 Executor 又是直接跟底层 jdbc 的 API 打交道的,所以我们一定要搞明白这个 Executor 的设计。

2.1 结构设计

Executor 本身是一个接口,它里面定义了好多方法,我们挑几个比较重要的方法先来看一下:

public interface Executor {
    
    int update(MappedStatement ms, Object parameter) throws SQLException;
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                      ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                      ResultHandler resultHandler) throws SQLException;
    Transaction getTransaction();
    void commit(boolean required) throws SQLException;
    void rollback(boolean required) throws SQLException;
    // ......
}

可以发现,Executor 中真的把需要与数据库交互的操作,基本都定义好了,这里面大体包含这么多吧:

  • CRUD 操作。

  • 事务控制和获取。

  • 二级缓存的控制。

  • 延迟加载等。

不过这也仅仅是定义的接口方法,下面肯定还有落地实现,Executor 本身有两个直接的抽象实现类,共 5 个最终落地实现类:

img

下面我们挑其中重要的实现类来讲解。

2.2 基础子类:BaseExecutor

Base 开头,很明显它就是个抽象的父类,不过这个 BaseExecutor 实现的倒是不少,大多都是逻辑骨架之类的代码实现,我们可以展开聊聊。

2.2.1 重要成员

这个类的成员有不少重要的,而且不乏一些我们之前看到过的:

public abstract class BaseExecutor implements Executor {

    private static final Log log = LogFactory.getLog(BaseExecutor.class);

    protected Transaction transaction;
    protected Executor wrapper;
    protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
    protected PerpetualCache localCache;
    protected PerpetualCache localOutputParameterCache;
    protected Configuration configuration;
    protected int queryStack;
    private boolean closed;

注释我们都标注好了,其中有几个重要的或者容易引起好奇心的,小册可以多说两嘴:

  • Executor wrapperExecutor 本身也有装饰者的设计(比方说普通的 Executor 可以通过套一层装饰者,实现二级缓存的预检查和存储);

  • PerpetualCache localOutputParameterCache :这个东西跟底层操作 jdbc 中 Statement 的类型有关,一般情况下我们不会接触到它,所以不用管了;

  • queryStack :当遇到嵌套查询时,这个计数器可以及时的记录,并在合适的位置控制检查是否需要清空缓存。

简单了解一下就可以哈,下面遇到它们的时候,小册还会说的。

下面我们来看看 BaseExecutor 都提供了哪些基础的方法。

2.2.2 query

论基础且最重要的方法,那必属于我 CRUD 四大金刚了。我们先看查询的 query 方法吧:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                         ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

乍一看,这个 query 方法很简单啊,不要着急,这个方法只是个中转站,这里它主要的工作,是获取到要发送的 SQL ,以及根据要去往数据库的查询请求,构造出缓存的标识(即 CacheKey ),之后继续往下传。

下面重载的 query 方法才是重头戏,小册已经把整体流程都标注了注释,各位先整体扫一遍流程,理一下脉络:

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.");
    }
    // 如果是刚开始查询,并且statement定义的需要刷新缓存,则前置清空一次一级缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        // 开始查询,计数器+1
        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 {
        // 查询完成后计数器-1
        queryStack--;
    }
    // 计数器归零时,证明所有查询都已经完成,处理后续动作
    if (queryStack == 0) {
        // 处理延迟加载
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        deferredLoads.clear();
        //如果全局配置文件中声明的一级缓存作用域是statement,则应该清空一级缓存(因为此时statement已经处理完毕了)
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        }
    }
    return list;
}

扫一遍下来,我们可以很清楚的从这段源码中获得这样的思路:

  1. 查询之前计数,并清空一级缓存(有必要的话)

  2. 先不着急去数据库里查,先看看缓存里有没有

    • 如果有,直接取缓存

    • 没有,就查数据库

  1. 查完了处理后续工作

OK ,有这个思路就够了,具体的方法我们没有必要现在就着急深入探索,后面我们跑全流程的时候自然会深入的。

2.2.3 update

除了 query 之外,另一个重要的 CRUD 方法就是 update 了,我们都知道,insert update delete 语句都可以使用 update 的动作完成,所以 Executor 也只设计了一个 update 方法完事,BaseExecutor 中的 update 方法倒是简单:

public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
}

这很明显是模板方法的套路啊,doUpdate 才是真正干活的吧!我们往下翻一下:

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

嗯,果然不出我们所料 ~ 具体 doUpdate 都怎么实现,我们现在也是不着急深入哈,后面全流程走的时候自然会看到的。

哦对了,我们多留意一点,发现 doUpdate 方法是个模板方法的时候,下面居然还有个 doQuery 方法:

protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, 
        RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

诶?这就奇了怪了,上面看 query 方法的时候,也没看到有 doQuery 方法啊,咋回事呢?如果小伙伴有这个疑问,我只能说一句你太棒了,你的主动思考能力就是驱使你去探索学习的源动力,请保持住这种感觉,加油 ~

至于 doQuery 方法搁哪儿调用的,我们同样也是跟 doUpdate 一块儿,后面再找。

2.2.4 其他方法

除了读和写的动作之外,Executor 中也负责事务的提交和回滚:

public void commit(boolean required) throws SQLException {
    if (closed) {
        throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache();
    flushStatements();
    if (required) {
        transaction.commit();
    }
}

public void rollback(boolean required) throws SQLException {
    if (!closed) {
        try {
            clearLocalCache();
            flushStatements(true);
        } finally {
            if (required) {
                transaction.rollback();
            }
        }
    }
}

注意 Executor 不负责开启事务,因为事务在 Executor 的创建时期就已经传入进去了:

protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    // ......
}

还有一些别的方法,我们后面走全流程的时候遇到再说吧,这里我们可以做到对 BaseExecutor 有一个整体的认识就可以了。

2.3 基本实现类:SimpleExecutor

下面我们来介绍 BaseExecutor 的一个重要的实现子类,它也是最简单最基础的实现类: SimpleExecutor,由于 BaseExecutor 中有设计一些模板方法,SimpleExecutor 就只是负责把这些方法实现了,没有别的多余的设计。

首先是 doQuery 方法,这基本就是直接操作 jdbc 的 API 了:

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);
    }
}

接下来是 doUpdate 方法,发现套路几乎是一样的,只是一个调用 StatementHandlerquery 方法,一个是调用 update 方法:

public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.update(stmt);
    } finally {
        closeStatement(stmt);
    }
}

注意留意!这里面涉及到了之前在讲解 MyBatis 插件时说的 StatementHandler !之前我们提到过,MyBatis 的插件 / 拦截器,可以拦截 StatementHandler 的部分方法,其中就有 queryupdate 方法,这里我们要稍微留意一下哈。

2.4 带二级缓存的实现类:CachingExecutor

再然后,是可以体现出 MyBatis 二级缓存的 CachingExecutor 了,我们之前在二级缓存的章节中遇见过它,不过当时小册只是顺口提了一下这个类名(毕竟当时我们要关注的是二级缓存的生效原理),现在我们也简单看看它。

请注意,CachingExecutor 本身是一个装饰者,所以它并没有继承自 BaseExecutor (如果继承了,那就意味着 CachingExecutor 内部也会组合一个一级缓存,这显然不符合设计),而是只实现了 Executor 接口。装饰者,那内部一定要有一个 delegate

public class CachingExecutor implements Executor {

    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

另外还有一个 TransactionalCacheManager ,至于为什么设计它,小册在第 15 章的 2.3.3 章节已经讲解过了,忘记的小伙伴可以返回去看。

除此之外,CachingExecutor 具体在执行过程中都怎么干活,我们也是放到后面的全流程执行中研究。

OK ,有关 Executor 的部分小册就讲这么多,下面是另一个同等重要的核心 API :**MappedStatement**

3. MappedStatement

Executor 同等重要的,还有它下游的 MappedStatement ,这个 Statement 的概念,就是我们从一开始学习 MyBatis 的时候提到的,mapper.xml 中写的一个一个的 <select> 也好,<insert> 等等也好,这些都会在底层封装为一个一个的 MappedStatement ,前面在解析 mapper.xml 和注解 Mapper 接口时我们也都看到了基本的脉络,本章我们重点关注它的结构。

3.1 重要成员

MappedStatement 本身是一个独立的类,没有继承也没有扩展,所以我们不用去分析继承结构体系,只需要看看这里面组合了哪些重要的成员就 OK 了。

不过有点头疼的是,MappedStatement 本身的成员属性实在是有点多,小册只捡一些重要的,咱们一起来看看就 OK 了:

public final class MappedStatement {

    // 当前mapper的来源(mapper.xml / Mapper.class的路径)
    private String resource;
    private Configuration configuration;
    private String id;
    // statement内部封装的SQL
    private SqlSource sqlSource;
    // 当前statement对应的mapper.xml或Mapper接口的namespace下的二级缓存
    private Cache cache;
    // 如果是select,则此处存放返回值的映射(resultMap和resultType都在这里)
    private List<ResultMap> resultMaps;
    // 执行此条SQL之前是否需要清空二级缓存
    private boolean flushCacheRequired;
    // 当前SQL是否使用二级缓存
    private boolean useCache;
    // ......
}

可以发现,MappedStatement 中保存的其实就是 mapper.xml 或者注解 Mapper 接口中定义的那些元素的信息(有点元信息的味了),当然也有最最重要的 SQL 语句封装,注意它不是用一个普通的 String 去封装 SQL ,而是一个专门的 SqlSource ,为什么要设计它,我们马上就说。

3.2 SqlSource的设计

先解释一下为什么不用普通的 String 去封装 SQL 。

3.2.1 SqlSource的意义

其实原因很简单,比方说下面的一条 mapper 定义:

<select id="findAll" parameterType="Department" resultType="Department">
    select * from tbl_department
    <where>
        <if test="id != null">
            and id = #{id}
        </if>
        <if test="name != null">
            and name like concat('%', #{name}, '%')
        </if>
    </where>
</select>

请问这条 SQL 要如何封装呢?动态 SQL 在实际查询的时候,应该是根据传入的参数,动态的组合 if 标签来生成 SQL ,所以用 String 来记录 SQL 是不现实的。

3.2.2 SqlSource的定义

SqlSource 本身是一个接口,它只有一个方法,就是获取 SQL :

public interface SqlSource {
    BoundSql getBoundSql(Object parameterObject);
}

注意这里它的返回值又不是 String ,而是一个 BoundSql ,看到这里可能小伙伴头顶上的问号越来越大了,这都图个啥啊???不要方,小册给你一个简单的图理解一下:

img

如图中所示,SqlSource 我们可以理解为带动态 SQL 标签的 SQL 定义 ,在程序运行期间,给 SqlSource 传入 SQL 必需的参数后,它会解析这些动态 SQL ,并生成一条真正可用于 PreparedStatement 的 SQL ,并且把这些参数也都保存好,而保存 SQL 和参数的载体就是 BoundSql 了。

当然,话又说回来,并不是所有的 SQL 定义都是动态的,也存在一些很简单的 SQL 呀(比方说 findById 这样的),这种情况下就没有必要用复杂 SQL 的模型组合了,所以 SqlSource 本身是有好几种实现的,下面我们来看它的一些实现。

img

3.2.3 简单实现:StaticSqlSource

静态的 SQL ,这个就是上面提到的,类似于 findAllfindById 的那种 SQL ,它们的 SQL 定义都没有动态标签,MyBatis 底层就会用这种方式封装。

StaticSqlSource 的设计相当简单,底层就是封装的明文 SQL :

public class StaticSqlSource implements SqlSource {

    private final String sql;
    private final List<ParameterMapping> parameterMappings;
    private final Configuration configuration;


    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        return new BoundSql(configuration, sql, parameterMappings, parameterObject);
    }
}

3.2.4 动态SQL:DynamicSqlSource

动态的 SQL ,对应的就是那些用了动态 SQL 标签的 statement 了,它的设计就不像 StaticSqlSource 那么简单了,它的底层是一个 SqlNode

public class DynamicSqlSource implements SqlSource {

    private final Configuration configuration;
    private final SqlNode rootSqlNode;

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        rootSqlNode.apply(context);
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        context.getBindings().forEach(boundSql::setAdditionalParameter);
        return boundSql;
    }
}

而且看下面 getBoundSql 方法的逻辑也比较奇怪,它又借助 DynamicContextSqlSourceBuilder 这两个东西来辅助生成 SQL ,这几个东西又是干什么的呢?这里我们先不展开了,后面我们会讲解这里面的每一步动作。

3.2.5 基于Provider:ProviderSqlSource

还记得第 12 章我们有讲 Provider 系列的注解吗,使用 Provider 的方式定义的 statement ,MyBatis 会选用这种 SqlSource 封装。它的底层是一系列反射的元素:

public class ProviderSqlSource implements SqlSource {

    private final Configuration configuration;
    // Provider对应的类
    private final Class<?> providerType;
    private final LanguageDriver languageDriver;
    private final Method mapperMethod;
    // 提供SQL的方法
    private final Method providerMethod;
    private final String[] providerMethodArgumentNames;
    private final Class<?>[] providerMethodParameterTypes;
    // 参数支持ProviderContext
    private final ProviderContext providerContext;
    private final Integer providerContextIndex;

这种方式我们平时用的不多,MyBatis 本身也不是很喜欢我们使用这种方式,所以小册也不展开研究了,感兴趣的小伙伴可以自行研究一下。

3.3 MappedStatement的重要方法

好了话说回来,MappedStatement 的内部核心有 SQL 的定义,也有这些 statement 对应的一些配置元信息的存储,除此之外,它还有一个重要的方法,就是直接从 MappedStatement 上解析 SQL :

public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
        boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
        String rmId = pm.getResultMapId();
        if (rmId != null) {
            ResultMap rm = configuration.getResultMap(rmId);
            if (rm != null) {
                hasNestedResultMaps |= rm.hasNestedResultMaps();
            }
        }
    }

    return boundSql;
}

说是重要,其实也没有多重要,因为它内部就是转发了一层而已,实际干活的还是上面提到的 SqlSource

最后更新于

这有帮助吗?