美文网首页
mybatis demo 初体验

mybatis demo 初体验

作者: 柯基去哪了 | 来源:发表于2019-05-19 11:13 被阅读0次

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

    有了前面的一步的铺垫,后面的构建动作就比较简单了,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 的大致流程梳理就到这里,后面我们再根据这个基础,依次分析总结我们用到的各个关键组件。

    相关文章

      网友评论

          本文标题:mybatis demo 初体验

          本文链接:https://www.haomeiwen.com/subject/xovfzqtx.html