mybatis demo 初体验

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 gaopeng@doctorwork.com
 * @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中的代码,都做了哪些操作:

  1. 连接数据库
  2. 执行一个 sql 查询数据获取数据
  3. 将从数据库中查到的数据映射成 java 对象返回
  4. 关闭数据库连接(此时我们还不确定这个关闭连接的操作是在步骤2还是在步骤3完成之后做的)

然后我们简单根据代码来跟一下处理的流程:

  1. 使用 mybatis 封装的一个流从项目的相对路径下加载到配置文件数据
  2. 依赖前一步数据流加载,构建一个 sqlSessionFactory 对象
  3. 打开一个 sqlSession 会话对象
  4. 执行一个查询操作并返回了一个我们期望的可以直接使用的 java 对象
  5. 关闭 sqlSession
process.png

然后依次来分析每一个步骤

一 加载配置文件

目标配置文件中,包含了数据库连接信息、mysql 连接驱动名和 sql 语句映射 xml 文件地址。因此,这一步是在做数据库连接的准备工作。只有获取并加载到这个配置文件,我们才能去连接数据库。整个加载过程,大量地使用了 建造者设计模式。最终配置文件上的全部内容解析得到一个 Configuration 实例。实例上包含了配置文件上的所有信息。

config.png

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 对象实例

WechatIMG4.png

有了前面的一步的铺垫,后面的构建动作就比较简单了,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> 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;
    }
  }

这是 DefaultSqlSession 下对 sqlSession 的默认实现类。传入之前映射解析好的 sql 语句和入参,执行一次预期返回多个结果的查询。检查获取结果集中元素的数量,如果超过了 1 个,就对外抛出异常。这里还是能看到代码复用的习惯, selectOne 复用了 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();
    }
  }

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 <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 {
        // 执行一次 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 的大致流程梳理就到这里,后面我们再根据这个基础,依次分析总结我们用到的各个关键组件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容