Mybatis 框架基本介绍

基本介绍

​ MyBatis 架构图如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yA45HW5w-1630124750278)(/Users/heguitang/Library/Application Support/typora-user-images/mybatis结构图.webp)]

图片来源于:http://www.imooc.com/article/1291

对应其执行流程,如下图所示:
在这里插入图片描述

从上面连个图中可以看出,MyBatis 架构主要分为三层:接口层、数据处理层和基础支撑层,其中:

  • 接口层:接口层是 MyBatis 提供给开发人员的一套 API,通过 SqlSession 接口(传统的 MyBatis 提供的API)和 Mapper 接口,开发人员可以通知 MyBatis 框架调用哪一条 SQL 命令以及 SQL 命令关联参数。

    • SqlSession 接口:传递 Statement Id 和查询参数给 SqlSession 对象,使用 SqlSession 对象完成和数据库的交互;MyBatis 提供了非常方便和简单的API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和 MyBatis 自身配置信息的维护操作。

      这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯,由于面向接口的编程是面向对象的大趋势,MyBatis 为了适应这一趋势,增加了第二种使用 MyBatis 支持接口(Interface)调用方式。

    • MyBatis 将配置文件中的每一个 节点抽象为一个 Mapper 接口,而这个接口中声明的方法和跟 节点中的<select|update|delete|insert> 节点项对应,即 <select|update|delete|insert> 节点的id值为Mapper 接口中的方法名称,parameterType 值表示 Mapper 对应方法的入参类型,而 resultMap 值则对应了 Mapper 接口表示的返回值类型或者返回结果集的元素类型。

      根据MyBatis 的配置规范配置好后,通过SqlSession.getMapper(XXXMapper.class) 方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper 实例,我们使用Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select(“statementId”,parameterObject);或者SqlSession.update(“statementId”,parameterObject); 等等来实现对数据库的操作。

      引用 Mapper \接口这种调用方式,纯粹是为了满足面向接口编程的需要(用户也可以在接口上可以使用注解来配置SQL语句,这样就可以脱离XML配置文件,实现“0配置”)。

  • 数据处理层:数据处理层是 MyBatis 框架内部核心的实现,完成对映射文件的解析和数据处理。如:参数解析与参数绑定、SQL 解析、结果集映射解析与结果集映射处理。

    动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis 通过传入的参数值,使用 Ognl 来动态地构造SQL语句,使得 MyBatis 有很强的灵活性和扩展性。

  • 基础支撑层:基础支撑层是用来完成 MyBatis 与数据库基本连接方式,以及 SQL 命令与配置文件对应。如:MyBatis与数据库连接方式管理、MyBatis对事务管理方式、配置文件加载、MyBatis查询缓存管理。


架构流程

MyBatis 架构流程

其架构流程图如下所示:
在这里插入图片描述

从图中可以看出 MyBatis 流程涉及到了各个组件,其中:

  • MyBatis配置文件

    • SqlMapConfig.xml:此文件作为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息。
    • Mapper.xml:此文件作为 mybatis 的 sql 映射文件,文件中配置了操作数据库的 sql 语句。此文件需要在 SqlMapConfig.xml中加载。
  • SqlSessionFactory:通过mybatis环境等配置信息构造 SqlSessionFactory,即会话工厂。

  • SqlSession:通过会话工厂创建sqlSession,即会话。我们可以通过 sqlsession 会话接口对数据库进行增删改查操作。

  • Executor 执行器:mybatis 底层自定义了 Executor 执行器接口来具体操作数据库,Executor接口有两个实现,一个是基本执行器(默认)、一个是缓存执行器,sqlsession 底层是通过 executor 接口操作数据库的。

  • MappedStatement:也是 mybatis 一个底层封装对象,它包装了mybatis配置信息sql映射信息等。mapper.xml文件中的一个select\insert\update\delete标签对应一个 MappedStatement 对象, 其标签的 id 即是 MappedStatement 的 id。

    • MappedStatement 对sql执行输入参数进行定义,包括 HashMap、基本类型、pojo。Executor 通过 MappedStatement 在执行 sql 前将输入的 Java 对象映射至 sql 中,输入参数映射就是 jdbc 编程中对 preparedStatement 设置参数。
    • MappedStatement 对sql执行输出结果进行定义,包括 HashMap、基本类型、pojo。Executor 通过 MappedStatement 在执行 sql 后将输出结果映射至 java 对象中,输出结果映射过程相当于jdbc 编程中对结果的解析处理过程。

下面给一段使用 JDBC 访问数据库的例子:

public class JdbcDemo {
    public void jdbcTest() {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet rs = null;
        try {
            // 加载数据库驱动
            Class.forName("com.mysql.jdbc.Driver");

            // 通过驱动管理类获取数据库链接connection = DriverManager
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8",
                    "root", "root");

            // 定义sql语句 ?表示占位符
            String sql = "select * from user where username = ?";

            // 获取预处理 statement
            preparedStatement = connection.prepareStatement(sql);

            // 设置参数,第一个参数为 sql 语句中参数的序号(从 1 开始),第二个参数为设置的
            preparedStatement.setString(1, "唐家三少");

            // 向数据库发出 sql 执行查询,查询出结果集
            rs = preparedStatement.executeQuery();

            // 遍历查询结果集
            while (rs.next()) {
                System.out.println(rs.getString("id") + " " + rs.getString("username"));
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (preparedStatement != null) {
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    // 异常信息的处理
                    e.printStackTrace();
                }
            }
        }
    }
}

从这段代码可以看出,直接使用 JDBC 访问数据库,需要自己创建连接(连接频繁创建和销毁会浪费资源)、存在硬编码等多种问题。而MyBatis就是对JDBC的一套流程进行抽象封装,是基于JDBC之上的。


MyBatis 与 JDBC 的关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-piYKDcqJ-1630124750296)(/Users/heguitang/Library/Application Support/typora-user-images/image-20210809193624498.png)]

分析上图中涉及到的各个组件:

  • SqlSession:接收开发人员提供 Statement Id 和参数;返回操作结果。
  • Executor:MyBatis 的执行器,是 MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
  • StatementHandler:封装了 JDBC Statement 操作,负责对 JDBC statement 的操作。如:设置参数、将 Statement 结果集转换成List集合。
  • ParameterHandler:负责将用户传递的参数转换成 JDBC Statement 所需要的参数。
  • ResultSetHandler:负责将 JDBC 返回的 ResultSet 结果集对象转换成List类型的集合。
  • TypeHandler:负责 Java 数据类型和 jdbc 数据类型之间的映射和转换。
  • MappedStatement:维护了一条<select|update|delete|insert>节点的封装。
  • SqlSource:负责根据用户传递的 parameterObject,动态地生成SQL语句,将信息封装到 BoundSql 对象中,并返回 BoundSql表示动态生成的SQL语句以及相应的参数信息。
  • Configuration:MyBatis所有的配置信息都维持在 Configuration 对象之中。
  • BoundSql:组合模式,将解析之后的SQL语句和解析出来的参数信息进行封装。

Executor、StatementHandler、ParameterHandler、ResultSetHandler 可称为完成 JDBC 操作的四大组件(主要是处理和执行)。


MyBatis 流程源码分析

基于mybatis-3.5.7 源码分析流程。

1、加载全局配置文件流程

org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties)

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // XMLConfigBuilder:用来解析XML配置文件
    // 使用构建者模式
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // parser.parse():使用XPATH解析XML配置文件,将配置文件封装为Configuration对象
    // 返回DefaultSqlSessionFactory对象,该对象拥有Configuration对象(封装配置文件信息)
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}
  • SqlSessionFactoryBuilder创建SqlsessionFactory时,需要传入一个Configuration 对象。
  • XMLConfigBuilder对象会去实例化Configuration。
  • XMLConfigBuilder对象会去初始化Configuration对象。
    • 通过XPathParser去解析全局配置文件,形成Document对象。
    • 通过XPathParser去获取指定节点的XNode对象。
    • 解析Xnode对象的信息,然后封装到Configuration对象中。

查看类DefaultSqlSessionFactory,发现,内部包含了 Configuration 对象。

public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private final Configuration configuration;

}

在加载全局配置文件完成后,我们就可以获取 SqlSession 和 Mapper 接口(从Configuration对象中get)。

1.1、获取Mapper代理对象流程

org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper

@Override
public <T> T getMapper(Class<T> type) {
  // 从Configuration对象中,根据Mapper接口,获取Mapper代理对象
  return configuration.getMapper(type, this);
}
2、加载映射文件流程

org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement

/**
   * 解析<mappers>标签
   *
   * @param parent mappers标签对应的XNode对象
   * @throws Exception
   */
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 获取<mappers>标签的子标签
    for (XNode child : parent.getChildren()) {
      // <package>子标签
      if ("package".equals(child.getName())) {
        // 获取mapper接口和mapper映射文件对应的package包名
        String mapperPackage = child.getStringAttribute("name");
        // 将包下所有的mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
        configuration.addMappers(mapperPackage);
      } else {
        // <mapper>子标签
        // 获取<mapper>子标签的resource属性
        String resource = child.getStringAttribute("resource");
        // 获取<mapper>子标签的url属性
        String url = child.getStringAttribute("url");
        // 获取<mapper>子标签的class属性
        String mapperClass = child.getStringAttribute("class");
        // 按照 resource ---> url ---> class的优先级去解析取<mapper>子标签,它们是互斥的
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
            // 专门用来解析mapper映射文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          try(InputStream inputStream = Resources.getUrlAsStream(url)){
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          }
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          // 将指定mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

这里,加载了Mapper.xml映射文件,并把解析结果放入了Configuration对象中。之后,就可以创建出SqlSource,而SqlSource中,又能获取 BoundSql。

2.1、SqlSource创建流程

org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource(org.apache.ibatis.session.Configuration, org.apache.ibatis.parsing.XNode, java.lang.Class<?>)

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  // 初始化了动态SQL标签处理器
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  // 解析动态SQL
  return builder.parseScriptNode();
}
2.2、BoundSql获取流程

org.apache.ibatis.mapping.MappedStatement#getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
  // 调用SqlSource获取BoundSql
  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;
}
3、SqlSession执行主流程

org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    // 根据传入的statementId,获取MappedStatement对象
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 调用执行器executor的查询方法
    // RowBounds是用来逻辑分页
    // wrapCollection(parameter)是用来装饰集合或者数组参数
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
  • DefaultSqlSession#selectLIst,调用 executor.query()
    • CachingExecutor#query,调用 delegate.query()
      • BaseExecutor#query,调用 queryFromDatabase()
        • BaseExecutor#queryFromDatabase,调用 doQuery()
          • SimpleExecutor#doQuery
            • 调用 configuration.newStatementHandler()
              • new RoutingStatementHandler,根据路由规则,设置不同的StatementHandler
            • SimpleExecutor#prepareStatement,主要是设置 PreparedStatement 的参数
              • BaseExecutor#getConnection,获取数据库连接
              • handler.prepare,创建 PreparedStatement对象
              • handler.parameterize,设置PreparedStatement的参数
            • PreparedStatementHandler#query,主要是用来执行SQL语句,及处理结果集
              • PreparedStatement#execute,调用JDBC的api执行Statement
              • DefaultResultSetHandler#handleResultSets,处理结果集
4、参数映射流程

org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize

@Override
public void parameterize(Statement statement) throws SQLException {
  // 通过ParameterHandler处理参数
  parameterHandler.setParameters((PreparedStatement) statement);
}
5、结果集映射流程

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets

@Override
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;
  // 获取第一个结果集,并放到ResultSet装饰类
  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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值