Java MyBatis

1. MyBatis 有什么特点?

  • MyBatis 中的 SQL 语句和主要业务代码分离,我们一般会把 MyBatis 中的 SQL 语句统一放在 XML 配置文件中,便于统一维护。

  • 解除 SQL 与程序代码的耦合,通过提供 DAO 层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。SQL 和代码的分离,提高了可维护性。

  • MyBatis 比较简单和轻量:本身就很小且简单。没有任何第三方依赖,只要通过配置 jar 包,或者如果使用 Maven 项目的话只需要配置 Maven 以来就可以。易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现。

  • 屏蔽样板代码:MyBatis 会屏蔽原始的 JDBC 样板代码,让你把更多的精力专注于 SQL 的书写和属性-字段映射上。

  • 编写原生 SQL,支持多表关联:MyBatis 最主要的特点就是可以手动编写 SQL 语句,能够支持多表关联查询。

  • 提供映射标签,支持对象与数据库的 ORM 字段关系映射

    对象关系映射(Object Relational Mapping,简称ORM) ,是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。本质上就是将数据从一种形式转换到另外一种形式。

  • 提供 XML 标签,支持编写动态 SQL。可以使用 MyBatis XML 标签,起到 SQL 模版的效果,减少繁杂的 SQL 语句,便于维护。

2. MyBatis 整体架构

MyBatis 最上面是接口层,接口层就是开发人员在 Mapper 或者是 Dao 接口中的接口定义,是查询、新增、更新还是删除操作;中间层是数据处理层,主要是配置 Mapper -> XML 层级之间的参数映射,SQL 解析,SQL 执行,结果映射的过程。上述两种流程都由基础支持层来提供功能支撑,基础支持层包括连接管理,事务管理,配置加载,缓存处理等。

MyBatis架构

接口层

在不与Spring 集成的情况下,使用 MyBatis 执行数据库的操作主要如下:

InputStream is = Resources.getResourceAsStream("myBatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
sqlSession = factory.openSession();

其中的 SqlSessionFactory,SqlSession 是 MyBatis 接口的核心类,尤其是 SqlSession,这个接口是MyBatis 中最重要的接口,这个接口能够执行命令,获取映射,管理事务。

数据处理层

配置解析

在 Mybatis 初始化过程中,会加载 mybatis-config.xml 配置文件、映射配置文件以及 Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到 Configration 对象中。之后,根据该对象创建 SqlSessionFactory 对象。待 Mybatis 初始化完成后,可以通过 SqlSessionFactory 创建 SqlSession 对象并开始数据库操作。

SQL 解析与 scripting 模块

Mybatis 实现的动态 SQL 语句,几乎可以编写出所有满足需要的 SQL。

Mybatis 中 scripting 模块会根据用户传入的参数,解析映射文件中定义的动态 SQL 节点,形成数据库能执行的 SQL 语句。

SQL 执行

SQL 语句的执行涉及多个组件,包括 MyBatis 的四大核心,它们是: ExecutorStatementHandlerParameterHandlerResultSetHandler。SQL 的执行过程可以用下面这幅图来表示:

MyBatis中sql执行过程

MyBatis 层级结构各个组件的介绍(这里只是简单介绍,具体介绍在后面):

  • SqlSession:它是 MyBatis 核心 API,主要用来执行命令,获取映射,管理事务。接收开发人员提供 Statement Id 和参数。并返回操作结果。
  • Executor :执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成以及查询缓存的维护。
  • StatementHandler : 封装了 JDBC Statement 操作,负责对 JDBC Statement 的操作,如设置参数、将 Statement 结果集转换成 List 集合。
  • ParameterHandler : 负责对用户传递的参数转换成 JDBC Statement 所需要的参数。
  • ResultSetHandler : 负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合。
  • TypeHandler : 用于 Java 类型和 JDBC 类型之间的转换。
  • MappedStatement : 动态 SQL 的封装
  • SqlSource : 表示从 XML 文件或注释读取的映射语句的内容,它创建将从用户接收的输入参数传递给数据库的 SQL。
  • Configuration : MyBatis 所有的配置信息都维持在 Configuration 对象之中。

基础支持层

反射模块

Mybatis 中的反射模块,对 Java 反射进行了很好的封装,提供了简易的 API,方便上层调用,并且对反射操作进行了一系列的优化,比如,缓存了类的元数据(MetaClass)和对象的元数据(MetaObject),提高了反射操作的性能。

类型转换模块

Mybatis 的别名机制,能够简化配置文件,该机制是类型转换模块的主要功能之一。类型转换模块的另一个功能是实现 JDBC 类型与 Java 类型的转换。在 SQL 语句绑定参数时,会将数据由 Java 类型转换成 JDBC 类型;在映射结果集时,会将数据由 JDBC 类型转换成 Java 类型。

日志模块

在 Java 中,有很多优秀的日志框架,如 Log4j、Log4j2、slf4j 等。Mybatis 除了提供了详细的日志输出信息,还能够集成多种日志框架,其日志模块的主要功能就是集成第三方日志框架。

资源加载模块

该模块主要封装了类加载器,确定了类加载器的使用顺序,并提供了加载类文件和其它资源文件的功能。

解析器模块

该模块有两个主要功能:一个是封装了 XPath,为 Mybatis 初始化时解析 mybatis-config.xml 配置文件以及映射配置文件提供支持;另一个为处理动态 SQL 语句中的占位符提供支持。

数据源模块

Mybatis 自身提供了相应的数据源实现,也提供了与第三方数据源集成的接口。数据源是开发中的常用组件之一,很多开源的数据源都提供了丰富的功能,如连接池、检测连接状态等,选择性能优秀的数据源组件,对于提供 ORM 框架以及整个应用的性能都是非常重要的。

事务管理模块

一般地,Mybatis 与 Spring 框架集成,由 Spring 框架管理事务。但 Mybatis 自身对数据库事务进行了抽象,提供了相应的事务接口和简单实现。

缓存模块

Mybatis 中有一级缓存和二级缓存,这两级缓存都依赖于缓存模块中的实现。但是需要注意,这两级缓存与Mybatis 以及整个应用是运行在同一个 JVM 中的,共享同一块内存,如果这两级缓存中的数据量较大,则可能影响系统中其它功能,所以需要缓存大量数据时,优先考虑使用 Redis、Memcache 等缓存产品。

Binding 模块

在调用 SqlSession 相应方法执行数据库操作时,需要制定映射文件中定义的 SQL 节点,如果 SQL 中出现了拼写错误,那就只能在运行时才能发现。为了能尽早发现这种错误,Mybatis 通过 Binding 模块将用户自定义的 Mapper 接口与映射文件关联起来,系统可以通过调用自定义 Mapper 接口中的方法执行相应的 SQL 语句完成数据库操作,从而避免上述问题。注意,在开发中,我们只是创建了 Mapper 接口,而并没有编写实现类,这是因为 Mybatis 自动为 Mapper 接口创建了动态代理对象。

3. Mybatis核心组件(Mybatis中SQL语句执行过程)

这些组件实现了从 SQL 语句到映射到 JDBC 再到数据库字段之间的转换,执行 SQL 语句并输出结果集。

SqlSessionFactory

对于任何框架而言,在使用该框架之前都要经历过一系列的初始化流程,MyBatis 也不例外。MyBatis 的初始化流程如下:

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSessionFactory.openSession();

上述流程中比较重要的一个对象就是 SqlSessionFactorySqlSessionFactory 是 MyBatis 框架中的一个接口,它主要负责的是

  • MyBatis 框架初始化操作
  • 为开发人员提供SqlSession 对象

MyBatis中sqlSessionFactory类

SqlSessionFactory 有两个实现类,一个是 SqlSessionManager 类,一个是 DefaultSqlSessionFactory

  • DefaultSqlSessionFactory : SqlSessionFactory 的默认实现类,是真正生产会话的工厂类,这个类的实例的生命周期是全局的,它只会在首次调用时生成一个实例(单例模式),就一直存在直到服务器关闭。
  • SqlSessionManager :已被废弃,原因大概是: SqlSessionManager 中需要维护一个自己的线程池,而使用 MyBatis 更多的是要与 Spring 进行集成,并不会单独使用,所以维护自己的 ThreadLocal 并没有什么意义,所以 SqlSessionManager 已经不再使用。

SqlSessionFactory 的执行流程

首先第一步是 SqlSessionFactory 的创建

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

从这行代码入手,首先创建了一个 SqlSessionFactoryBuilder 工厂,这是一个建造者模式的设计思想,由 builder 建造者来创建 SqlSessionFactory 工厂

然后调用 SqlSessionFactoryBuilder 中的 build 方法传递一个 InputStream 输入流,Inputstream 输入流中就是你传过来的配置文件 mybatis-config.xmlSqlSessionFactoryBuilder 根据传入的 InputStream 输入流和 environmentproperties 属性创建一个 XMLConfigBuilder 对象。SqlSessionFactoryBuilder 对象调用 XMLConfigBuilderparse() 方法,流程如下。

SqlSessionFactory 的执行流程

XMLConfigBuilder 会解析 /configuration 标签,configuration 是 MyBatis 中最重要的一个标签。

parseConfiguration方法

每一个属性都对应着一个解析方法,都是使用 XPath 把标签进行解析,解析完成后返回一个 DefaultSqlSessionFactory 对象,它是 SqlSessionFactory 的默认实现类。这就是 SqlSessionFactoryBuilder 的初始化流程,通过流程我们可以看到,初始化流程就是对一个个 /configuration 标签下子标签的解析过程。

SqlSession

在 MyBatis 初始化流程结束,也就是 SqlSessionFactoryBuilder -> SqlSessionFactory 的获取流程后,我们就可以通过 SqlSessionFactory 对象得到 SqlSession 然后执行 SQL 语句了。

sqlSessionFactory获取sqlSession流程

SqlSessionFactory.openSession 过程中我们可以看到,会调用到 DefaultSqlSessionFactory 中的 openSessionFromDataSource 方法,这个方法主要创建了两个与分析执行流程重要的对象,一个是 Executor 执行器对象,一个是 SqlSession 对象。

SqlSession 对象是 MyBatis 中最重要的一个对象,这个接口能够执行命令,获取映射,管理事务。SqlSession 中定义了一系列模版方法,让你能够执行简单的 CRUD 操作,也可以通过 getMapper 获取 Mapper 层,执行自定义 SQL 语句,因为 SqlSession 在执行 SQL 语句之前是需要先开启一个会话,涉及到事务操作,所以还会有 commit、 rollback、close 等方法。这也是模版设计模式的一种应用。

MapperProxy

MapperProxy 是 Mapper 映射 SQL 语句的关键对象,我们写的 Dao 层或者 Mapper 层都是通过 MapperProxy 来和对应的 SQL 语句进行绑定的。绑定过程如下:

MapperProxy绑定过程

这就是 MyBatis 的核心绑定流程,可以看到 SqlSession 首先调用 getMapper 方法, SqlSession 是大哥级别的人物,只定义标准。

SqlSession 把事情交给 Configuration 去做,然后 Configuration 有调用 MapperRegistryMapperRegistry 相当于项目经理,项目经理只从大面上把握项目进度,不需要知道手下的小弟是如何工作的,把任务完成了就好。最终真正干活的还是 MapperProxyFactory。通过这段代码 Proxy.newProxyInstance ,可以看出使用了动态代理。

也就是说,MyBatis 中 Mapper 和 SQL 语句的绑定正是通过动态代理来完成的。

通过动态代理,就可以方便的在 Dao 层或者 Mapper 层定义接口,实现自定义的增删改查操作了。

接下来再看看 sql 语句执行过程

MapperProxy中sql语句执行过程

MapperProxyFactory 会生成代理对象,这个对象就是 MapperProxy,最终会调用到 mapperMethod.execute 方法,execute 方法比较长,其实逻辑比较简单,就是判断是插入、更新、删除还是查询语句,其中如果是查询的话,还会判断返回值的类型:

mapperMethod.execute方法

以 selectList 为例,来看一下下面的执行过程。

DefaultSqlSession中selectList源码

这是 DefaultSqlSessionselectList 的代码,可以看到出现了 executor

Executor

Executor 的创建如下:由 Configuration 对象创建了一个 Executor 对象

Executor创建

Executor 的继承结构

每一个 SqlSession 都会拥有一个 Executor 对象,这个对象负责增删改查的具体操作,可以简单的将它理解为 JDBC 中 Statement 的封装版。也可以理解为 SQL 的执行引擎,要干活总得有一个发起人吧,可以把 Executor 理解为发起人的角色。

Executor 的继承体系如下:

Executor继承结构

如上图所示,位于继承体系最顶层的是 Executor 执行器,它有两个实现类,分别是 BaseExecutorCachingExecutor

BaseExecutor 是一个抽象类,这种通过抽象的实现接口的方式是适配器设计模式之接口适配的体现,是Executor 的默认实现,实现了大部分 Executor 接口定义的功能,降低了接口实现的难度。BaseExecutor 的子类有三个,分别是 SimpleExecutorReuseExecutorBatchExecutor

  • SimpleExecutor : 简单执行器,是 MyBatis 中默认使用的执行器,每执行一次 update 或 select,就开启一个Statement 对象,用完就直接关闭 Statement 对象(可以是 Statement 或者是 PreparedStatment 对象)
  • ReuseExecutor : 可重用执行器,这里的重用指的是重复使用 Statement,它会在内部使用一个 Map 把创建的 Statement 都缓存起来,每次执行 SQL 命令的时候,都会去判断是否存在基于该 SQL 的 Statement 对象,如果存在 Statement 对象并且对应的 connection 还没有关闭的情况下就继续使用之前的 Statement 对象,并将其缓存起来。因为每一个 SqlSession 都有一个新的 Executor 对象,所以我们缓存在 ReuseExecutor 上的 Statement 作用域是同一个 SqlSession。
  • BatchExecutor : 批处理执行器,用于将多个 SQL 一次性输出到数据库
  • CachingExecutor : 缓存执行器,先从缓存中查询结果,如果存在就返回之前的结果;如果不存在,再委托给 Executor delegate 去数据库中取,delegate 可以是上面任何一个执行器。

Executor的创建和选择

Executor 是由 Configuration 创建的,Configuration 会根据执行器的类型创建,如下:

Executor的创建和选择

这一步就是执行器的创建过程,根据传入的 ExecutorType 类型来判断是哪种执行器,如果不指定 ExecutorType ,默认创建的是简单执行器。它的赋值可以通过两个地方进行赋值:

可以通过标签来设置当前工程中所有的 SqlSession 对象使用默认的 Executor:

<settings>
 <!--取值范围 SIMPLE, REUSE, BATCH -->
    <setting name="defaultExecutorType" value="SIMPLE"/>
</settings>

另外一种直接通过Java对方法赋值的方式:

session = factory.openSession(ExecutorType.BATCH);

Executor 的具体执行过程

Executor 中的大部分方法的调用链其实是差不多的,执行流程图如下:

Executor的具体执行过程

紧跟着上面的 selectList 继续分析,它会调用到 executor.query 方法。

当有一个查询请求访问的时候,首先会经过 Executor 的实现类 CachingExecutor ,先从缓存中查询 SQL 是否是第一次执行,如果是第一次执行的话,那么就直接执行 SQL 语句,并创建缓存,如果第二次访问相同的 SQL 语句的话,那么就会直接从缓存中提取。

下面这段代码是从 selectList -> 从缓存中 query 的具体过程。

Executor具体执行过程2-从缓存获取

下面代码会判断缓存中是否有这条 SQL 语句的执行结果,如果没有的话,就再重新创建 Executor 执行器执行 SQL 语句,注意, list = doQuery 是真正执行 SQL 语句的过程,这个过程中会创建我们上面提到的三种执行器,这里我们使用的是简单执行器。

Executor具体执行过程2-直接执行sql语句

到这里,执行器所做的工作就完事了,Executor 会把后续的工作交给 StatementHandler 继续执行。

StatementHandler

StatementHandler 是四大组件中最重要的一个对象,负责操作 Statement 对象与数据库进行交互,在工作时还会使用 ParameterHandlerResultSetHandler 对参数进行映射,对结果进行实体类的绑定,这两个组件我们后面说。

在搭建原生 JDBC 的时候,会有这样一行代码:

Statement stmt = conn.createStatement(); //也可以使用PreparedStatement来做

这行代码创建的 Statement 对象或者是 PreparedStatement 对象就是由 StatementHandler 进行管理的。

StatementHandler 的继承结构

StatementHandler 的继承结构

有没有感觉和 Executor 的继承体系很相似呢?最顶级接口是四大组件对象,分别有两个实现类 BaseStatementHandlerRoutingStatementHandlerBaseStatementHandler 有三个实现类, 他们分别是 SimpleStatementHandlerPreparedStatementHandlerCallableStatementHandler

RoutingStatementHandler : RoutingStatementHandler 并没有对 Statement 对象进行使用,只是根据 StatementType 来创建一个代理,代理的就是对应 Handler 的三种实现类。在MyBatis 工作时,使用的 StatementHandler 接口对象实际上就是 RoutingStatementHandler 对象。

BaseStatementHandler : 是 StatementHandler 接口的另一个实现类,它本身是一个抽象类,用于简化 StatementHandler 接口实现的难度,属于适配器设计模式体现,它主要有三个实现类

  • SimpleStatementHandler: 管理 Statement 对象并向数据库中推送不需要预编译的SQL语句。
  • PreparedStatementHandler: 管理 Statement 对象并向数据中推送需要预编译的SQL语句。
  • CallableStatementHandler:管理 Statement 对象并调用数据库中的存储过程。

这里注意一下,SimpleStatementHandlerPreparedStatementHandler 的区别是 SQL 语句是否包含变量,是否通过外部进行参数传入。SimpleStatementHandler 用于执行没有任何参数传入的 SQL,PreparedStatementHandler 需要对外部传入的变量和参数进行提前参数绑定和赋值。

StatementHandler 的创建和源码分析

继续来分析上面 query 的调用链路,StatementHandler 的创建过程如下:

StatementHandler 的创建和源码分析

MyBatis 会根据 SQL 语句的类型进行对应 StatementHandler 的创建。以预处理 StatementHandler 为例,创建过程如下:

PrepareStatementHandler创建过程

执行器不仅掌管着 StatementHandler 的创建,还掌管着创建 Statement 对象,设置参数等,在创建完 PreparedStatement 之后,就需要对参数进行处理了。

综上,sql语句的执行流程如下:

从executor到handler执行流程

ParameterHandler

ParameterHandler 介绍

ParameterHandler 相比于其他的组件就简单很多了,ParameterHandler 译为参数处理器,负责为 PreparedStatement 的 sql 语句参数动态赋值,这个接口很简单只有两个方法:

ParameterHandler接口

ParameterHandler 只有一个实现类 DefaultParameterHandler , 它实现了这两个方法。

  • getParameterObject:用于读取参数
  • setParameters: 用于对 PreparedStatement 的参数赋值

ParameterHandler 的解析过程

上面我们讨论过了 ParameterHandler 的创建过程,下面继续上面 parameterSize 流程:

ParameterHandler 的解析过程

这就是具体参数的解析过程了,注释如下:

public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  // parameterMappings 就是对 #{} 或者 ${} 里面参数的封装
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
    // 如果是参数化的SQL,便需要循环取出并设置参数的值
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      // 如果参数类型不是 OUT ,这个类型与 CallableStatementHandler 有关
      // 因为存储过程不存在输出参数,所以参数不是输出参数的时候,就需要设置。
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        // 得到 #{}  中的属性名
        String propertyName = parameterMapping.getProperty();
        // 如果 propertyName 是 Map 中的key
        if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
          // 通过key 来得到 additionalParameter 中的value值
          value = boundSql.getAdditionalParameter(propertyName);
        }
        // 如果不是 additionalParameters 中的key,而且传入参数是 null, 则value 就是null
        else if (parameterObject == null) {
          value = null;
        }
        // 如果 typeHandlerRegistry 中已经注册了这个参数的 Class 对象,即它是 Primitive 或者是String 的话
        else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          // 否则就是 Map
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // 在通过 SqlSource 的parse 方法得到parameterMappings 的具体实现中,我们会得到parameterMappings 的 typeHandler
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        // 获取 typeHandler 的jdbc type
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
        }
        try {
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        } catch (TypeException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        } catch (SQLException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
      }
    }
  }
}

下面用一个流程图表示一下 ParameterHandler 的解析过程,以简单执行器为例:

ParameterHandler 的解析过程流程图

在完成 ParameterHandler 对 SQL 参数的预处理后,回到 SimpleExecutor 中的 doQuery 方法:

SimpleExecutor中的 doQuery方法

上面又引出来了一个重要的组件那就是 ResultSetHandler

ResultSetHandler

ResultSetHandler 简介

ResultSetHandler 也是一个非常简单的接口:

ResultHandler接口

ResultSetHandler 是一个接口,它只有一个默认的实现类,像是 ParameterHandler 一样,它的默认实现类是 DefaultResultSetHandler

ResultSetHandler 解析过程

MyBatis 只有一个默认的实现类就是 DefaultResultSetHandlerDefaultResultSetHandler 主要负责处理两件事:

  • 处理 Statement 执行后产生的结果集,生成结果列表
  • 处理存储过程执行后的输出参数

按照 Mapper 文件中配置的 ResultTypeResultMap 来封装成对应的对象,最后将封装的对象返回即可。

public List<Object> handleResultSets(Statement stmt) throws SQLException {
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

  final List<Object> multipleResults = new ArrayList<Object>();

  int resultSetCount = 0;
  // 获取第一个结果集
  ResultSetWrapper rsw = getFirstResultSet(stmt);
  // 获取结果映射
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  // 结果映射的大小
  int resultMapCount = resultMaps.size();
  // 校验结果映射的数量
  validateResultMapsCount(rsw, resultMapCount);
  // 如果ResultSet 包装器不是null, 并且 resultmap 的数量  >  resultSet 的数量的话
  // 因为 resultSetCount 第一次肯定是0,所以直接判断 ResultSetWrapper 是否为 0 即可
  while (rsw != null && resultMapCount > resultSetCount) {
    // 从 resultMap 中取出 resultSet 数量
    ResultMap resultMap = resultMaps.get(resultSetCount);
    // 处理结果集, 关闭结果集
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

  // 从 mappedStatement 取出结果集
  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);
}

其中涉及的主要对象有:

  • ResultSetWrapper : 结果集的包装器,主要针对结果集进行的一层包装,它的主要属性有
  • ResultSet : Java JDBC ResultSet 接口表示数据库查询的结果。有关查询的文本显示了如何将查询结果作为java.sql.ResultSet 返回。然后迭代此ResultSet以检查结果。
  • TypeHandlerRegistry: 类型注册器,TypeHandlerRegistry 在初始化的时候会把所有的 Java类型和类型转换器进行注册。
  • ColumnNames: 字段的名称,也就是查询操作需要返回的字段名称
  • ClassNames: 字段的类型名称,也就是 ColumnNames 每个字段名称的类型
  • JdbcTypes: JDBC 的类型,也就是 java.sql.Types 类型
  • ResultMap: 负责处理更复杂的映射关系

DefaultResultSetHandler 中处理完结果映射,并把上述结构返回给调用的客户端,从而执行完成一条完整的SQL语句。

SQL 执行过程总结

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
DeptDao deptDao = sqlSession.getMapper(DeptDao.class);

首先是 SqlSessionFactory 的创建:

由上面可以看到通过 一个 SqlSessionFactoryBuilder 工厂,这是一个建造者模式的设计思想,由 builder 建造者来创建 SqlSessionFactory 工厂

然后调用 SqlSessionFactoryBuilder 中的 build 方法传递一个 InputStream 输入流,Inputstream 输入流中就是你传过来的配置文件 mybatis-config.xmlSqlSessionFactoryBuilder 根据传入的 InputStream 输入流和 environmentproperties 属性创建一个 XMLConfigBuilder 对象,然后调用 XMLConfigBuilderparse() 方法,该方法首先创建一个 XMLConfigBuilder 对象,通过这个对象的 parser() 方法对 xml 文件从 configuration 节点进行解析,对一个属性都对应一个解析方法,都是使用 XPath 把标签进行解析,解析完成后将这个配置好的 configuration 放到工厂的 configuration 变量里,最终 build 方法返回一个 DefaultSqlSessionFactory 对象,它是 SqlSessionFactory 的默认实现类。

接着是 SqlSession 的创建:

通过 SqlSessionFactory 工厂的 openSession 方法可以获取到一个 SqlSession ,该方法通过调用 DefaultSqlSessionFactoryopenSessionFromDataSource 方法,会配置好 configurationexecutor 执行器、事务属性等,然后传入 DefaultSqlSession 的构造方法创建出 DefaultSqlSession

Executor 的创建:

Executor 的创建是在创建 SqlSession 过程中,openSessionFromDataSource 会利用配置好的 Configuration 对象创建 Executor 对象,Configuration 会根据传入的执行器的类型创建相应的执行器。

再接下来是 Dao 层接口和 SQL 语句的绑定:

SqlSessiongetMapper 方法会调用 ConfigurationgetMapper 方法,其又会调用 MapperRegistry 中的 getMapper 方法,在这个方法中,会将 Class 作为参数构建 MapperProxyFactory,接着通过动态代理调用 MapperProxyFactorynewInstance 方法把当前 sqlSession传入,从而实现 DaoSql 的绑定。

MapperProxyFactory 会生成代理对象,这个对象就是 MapperProxy,其中 invoke 方法中,就实现了相应的绑定方法,其实现调用到 mapperMethod.execute 方法,这个方法判断是插入、更新、删除还是查询语句,然后调用 sqlSession 相应的方法,其中如果是查询的话,还会判断返回值的类型。以 selectList 为例,其方法最后调用的是 executor 执行器的 query 方法。

Executor 具体执行过程:

首先调用的是 CachingExecutorquery 方法,先从缓存中查询 SQL 是否是第一次执行,如果是第一次执行的话(cache == null),那么就直接执行 SQL 语句,并创建缓存,如果第二次访问相同的 SQL 语句的话,那么就会直接从缓存中提取。

在找不到缓存的情况下,会重新创建 Executor 执行器来执行 SQL 语句,首先调用 BaseExecutorqueryFromDatabase 方法,该方法又会调用 SimpleExecutordoQuery 方法,在这个方法里,会获取 Configuration 并通过 Configuration 获取 StatementHandler 处理器,由它来继续往下执行。Configuration 会根据 SQL 语句的类型进行对应 StatementHandler 的创建。

之后会调用 Executor 中的 preparedStatement方法,该方法会调用传入的 StatementHandlerprepare 方法获取一个 PreparedStatement 对象,同时调用 handlerparameterize 方法,该方法会调用 ParameterHandlersetParameters 方法将 PreparedStatement传入,用于为 PreparedStatement 的 sql 语句参数动态赋值。

在完成 ParameterHandler 对 SQL 参数的预处理后,回到 SimpleExecutor 中继续往下执行,会执行 PreparedStatementHandlerdoQuery 方法,该方法中 PreparedStatement 执行 execute 方法进行查询,然后通过 ResultSetHandlerhandleResultSets 对会返结果集进行处理并返回,这就完成了一次完整的 SQL 执行过程。

4. configuration标签下有哪些标签?

properties

外部属性,这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来传递。

<properties>
    <property name="driver" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/test" />
    <property name="username" value="root" />
    <property name="password" value="root" />
</properties>

一般用来给 environment 标签中的 dataSource 赋值

<environment id="development">
  <transactionManager type="JDBC" />
  <dataSource type="POOLED">
    <property name="driver" value="${driver}" />
    <property name="url" value="${url}" />
    <property name="username" value="${username}" />
    <property name="password" value="${password}" />
  </dataSource>
</environment>

settings

MyBatis 中极其重要的配置,它们会改变 MyBatis 的运行时行为。

settings 中配置有很多。这里介绍几个平常使用过程中比较重要的配置:

MyBatis中Setting节点内容

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
</settings>

typeAliases

类型别名,类型别名是为 Java 类型设置的一个名字。它只和 XML 配置有关。

<typeAliases>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
</typeAliases>

当这样配置时,Blog 可以用在任何使用 domain.blog.Blog 的地方。

typeHandlers

类型处理器,无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。

org.apache.ibatis.type 包下有很多已经实现好的 TypeHandler,可以参考如下:

MyBatis 实现好的 TypeHandler

可以重写类型处理器或创建自己的类型处理器来处理不支持的或非标准的类型。

具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很方便的类 org.apache.ibatis.type.BaseTypeHandler, 然后可以选择性地将它映射到一个 JDBC 类型。

objectFactory

对象工厂,MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。如果想覆盖对象工厂的默认行为,则可以通过创建自己的对象工厂来实现。

public class ExampleObjectFactory extends DefaultObjectFactory {
  public Object create(Class type) {
    return super.create(type);
  }
  public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
    return super.create(type, constructorArgTypes, constructorArgs);
  }
  public void setProperties(Properties properties) {
    super.setProperties(properties);
  }
  public <T> boolean isCollection(Class<T> type) {
    return Collection.class.isAssignableFrom(type);
  }
}

然后需要在 XML 中配置此对象工厂

<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>

plugins

插件开发,插件开发是 MyBatis 设计人员给开发人员留给自行开发的接口,MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用。MyBatis 允许使用插件来拦截的方法调用包括:ExecutorParameterHandlerResultSetHandlerStatementHandler 接口,这几个接口也是 MyBatis 中非常重要的接口。

environments

MyBatis 环境配置,MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。

这里注意一点,虽然 environments 可以指定多个环境,但是 SqlSessionFactory 只能有一个,为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可。

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);

环境配置如下:

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

databaseIdProvider

数据库厂商标示,MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>
  <property name="Oracle" value="oracle" />
</databaseIdProvider>

mappers

映射器,这是告诉 MyBatis 去哪里找到这些 SQL 语句,mappers 映射配置有四种方式

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>

<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

5. 什么是Mybatis?

Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。

MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

通过 xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。(从执行 sql 到返回 result 的过程)

6. MyBatis的优缺点

优点

  1. 基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。
  2. 与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;
  3. 很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要JDBC 支持的数据库 MyBatis 都支持)。
  4. 能够与 Spring 很好的集成;
  5. 提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点

  1. SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写
  2. SQL 语句的功底有一定要求。
  3. SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

7. MyBatis 框架适用场合

MyBatis 专注于 SQL 本身,是一个足够灵活的 DAO 层解决方案。

对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis 将是不错的选择。

8. #{}和${}的区别是什么?

# 表示的是占位符,#{} 是预编译处理,${} 是字符串替换。Mybatis 在处理 #{} 时,会将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatementset 方法来赋值;

Mybatis 在处理 ${} 时,就是把 {} 替换成变量的值。

使用 #{} 可以有效的防止 SQL 注入,提高系统安全性。

  • ${} 是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于静态文本替换,比如 ${driver} 会被静态替换为 com.mysql.jdbc.Driver
  • #{}是 sql 的参数占位符,Mybatis 会将 sql 中的 #{} 替换为 ? 号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的 ? 号占位符设置参数值,比如 ps.setInt(0, parameterValue)#{item.name} 的取值方式为使用反射从参数对象中获取 item 对象的 name 属性值,相当于 param.getItem().getName()

9. 当实体类中的属性名和表中的字段名不一样 ,怎么办?

当对类中的某个属性定义的属性名与所对应的数据表的字段名不一致时,需要手动配置指定对应字段名,否则会读取不到数据,有两种方法进行指定:

  1. 通过在查询的 sql 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致
<select id="selectBlog" parameterType="int" resultType="Blog">
    select id,title,author_id as authorId,featured,style from Blogs where id = #{id}
</select>
  1. 配置 resultMap,在 mapper 配置文件中,添加 resultMap 节点,同时在 statement 中指定 resultMap,通过 <resultMap> 来映射字段名和实体类属性名的一一对应的关系。
<mapper namespace="com.xm.mapper.BlogMapper">
    <resultMap id="blogResultMap" type="Blog">
        <id column="id" property="id" javaType="INTEGER"/>
        <result column="author_id" property="authorId" javaType="INTEGER"/>
    </resultMap>

    <select id="selectBlog2" parameterType="int" resultMap="blogResultMap">
        select * from blogs where id = #{id}
    </select>
</mapper>

10. 模糊查询 like 语句该怎么写?

  1. 在 Java 代码中添加 sql 通配符。
String wildcardname = "%smi%";
List<name> names = mapper.selectlike(wildcardname);
<select id="selectlike">
select * from foo where bar like #{value}
</select>
  1. 在 sql 语句中拼接通配符,会引起 sql 注入
String wildcardname = "smi";
List<name> names = mapper.selectlike(wildcardname);
<select id="selectlike">
select * from foo where bar like "%"#{value}"%"
</select>

11. 通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?

  1. Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的参数,就是传递给 sql 的参数。
  2. Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个<select><insert><update><delete> 标签,都会被解析为一个 MapperStatement 对象。
  3. 举例: com.mybatis3.mappers.StudentDao.findStudentById ,可以唯一找到 namespacecom.mybatis3.mappers.StudentDao 下面 idfindStudentById 的 MapperStatement。
  4. Mapper 接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK 动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。

12. Mybatis 是如何进行分页的?分页插件的原理是什么?

Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页。可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。

13. Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?

第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系。

第二种是使用 sql 列的别名功能,将列的别名书写为对象属性名。有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

14. 如何执行批量插入?

首先,创建一个简单的 insert 语句:

<insert id="insertname">
insert into names (name) values (#{value})
</insert>

然后在 java 代码中像下面这样执行批处理插入:

List < string > names = new arraylist();
names.add(“fred”);
names.add(“barney”);
names.add(“betty”);
names.add(“wilma”);
// 注意这里 executortype.batch
sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch);
try {
    namemapper mapper = sqlsession.getmapper(namemapper.class);
    for (string name: names) {
        mapper.insertname(name);
    }
    sqlsession.commit();
}catch (Exception e) {
    e.printStackTrace();
    sqlSession.rollback();
    throw e;
}finally {
    sqlsession.close();
}

15. 如何获取自动生成的(主)键值?

有三种方式进行设置:

在mapper中配置insert节点的属性

seGeneratedKeys = true keyProperty=”id”

useGeneratedKeys 仅对 insertupdate 有用,这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。

keyProperty 仅对 insertupdate 有用,唯一标记一个属性,指定主键名是什么,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认值:未设置(unset)。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。

insert 方法总是返回一个 int 值 ,这个值代表的是插入的行数。

如果采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。

<insert id="insertname" usegeneratedkeys="true" keyproperty="id">
insert into names (name) values (#{name})
</insert>
Name name = new name();
name.setname("fred");
int rows = mapper.insertname(name);
// 完成后,id 已经被设置到对象中
System.out.println("rows inserted = " + rows);
System.out.println("generated key value = " + name.getid());

在全局配置文件中配置settings节点

在全局配置文件中配置 settings 节点,并且在 mapperinsert 节点指定主键名,即配置keyProperty="id"

<settings>
    <setting name="useGeneratedKeys" value="true"/>
</settings>

针对Oracle等无主键的数据库)在 insert 节点中加入 selectKey 属性

其有以下变量属性:

  • keyProperty:selectKey 语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
  • keyColumn:匹配属性的返回结果集中的列名称。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
  • resultType:结果的类型。MyBatis 通常可以推断出来,但是为了更加精确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列,则可以使用一个包含期望属性的 Object 或一个 Map。
  • order:这可以被设置为 BEFOREAFTER。如果设置为 BEFORE,那么它会首先生成主键,设置 keyProperty 然后执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句(这和 Oracle 数据库的行为相似,在插入语句内部可能有嵌入索引调用)。
  • statementTypeSTATEMENTPREPAREDCALLABLE 的一个。这会让 MyBatis 分别使用 StatementPreparedStatementCallableStatement,默认值:PREPARED
<insert id="insertBlog2" parameterType="Blog" keyProperty="id">
    <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
        select LAST_INSERT_ID()
    </selectKey>
    insert into blogs (title, author_id, featured, style) value (#{title},#{authorId},#{featured},#{style})
</insert>

16. 在 mapper 中如何传递多个参数?

使用索引:按照参数排序,arg从0开始,param从1开始

DAO 层的函数

public UserselectUser(String name,String area);

对应的 xml,#{0} 代表接收的是 dao 层中的第一个参数,#{1} 代表 dao 层中第二
参数,更多参数一致往后加即可。

<select id="selectUser"resultMap="BaseResultMap">
    select * fromuser_user_t whereuser_name = #{0} and user_area=#{1}
</select>

使用 @param 注解

List<Blog> selectBlogByPage2(
        @Param(value = "offset") int offset,
        @Param(value = "pagesize") int pagesize);
<select id="selectBlogByPage2" resultMap="blogResultMap">
    select * from blogs limit #{offset},#{pagesize}
</select>

多个参数封装成 map

mapper 的配置保持不变,其中的占位符参数名要和测试中 map 的 key 一一对应

public void testSelectBlogByPage3(){
    SqlSession sqlSession = MyBatisUtil.sqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
    //自己构建一个map
    Map<String,Object> map = new HashMap<String, Object>();
    //注意key要和参数名对应
    map.put("offset",0);
    map.put("pagesize",2);
    List<Blog> blogs = mapper.selectBlogByPage3(map);
    sqlSession.close();
    for (Blog blog : blogs) {
        System.out.println(blog);
    }
}

17. Mybatis 动态 sql 有什么用?执行原理?有哪些动态 sql?

Mybatis 动态 sql 可以在 Xml 映射文件内,以标签的形式编写动态 sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接 sql 的功能。

Mybatis 提供了 9 种动态 sql 标签:

  • if:提供了可选的查询方案,如果 if 中的条件成立,则执行 if 中的语句,否则只执行 if 外的 sql 语句
  • choose(when,otherwise):用于多个条件判断,当满足其中某个 when 条件时,sql 语句加上其中的内容,否则只执行外部的内容,一旦某个when条件成立,就不会再判断下面的when语句了。三者搭配使用,相当于java中 switch,case,default用法
  • where:如果希望 where 条件语句也在需要的时候才加入,可以使用 where 来进行判断。where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入 WHERE 子句。而且,若语句的开头为 ANDOR,where 元素也会将它们去除(搭配 if 使用)
  • set:可以使用 set 元素实现按需更新,指定具体需要更新的字段,set 元素会动态前置 SET 关键字,同时也会删掉无关的逗号。
  • trim:trim 可以用于指定加入的前缀和指定自动去除的前面或后面的内容,它有下面四个属性
  1. prefix :加入的前置名
  2. suffix:加入的后缀名
  3. prefixOverrides:自动去除前面的内容
  4. suffiexOverrides:自动去除后面的内容
  • foreach:foreach 是对一个集合进行遍历,通常是在构建 IN 条件语句的时候。

18. Xml 映射文件中,除了常见的 select、insert、updae、delete 标签之外,还有哪些标签?

<resultMap><parameterMap><sql><include><selectKey>,加上动态 sql 的 9 个标签,其中 <sql> 为 sql 片段标签,通过 <include> 标签引入 sql 片段,<selectKey> 为不支持自增的主键生成策略标签。

19. Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复?

不同的 Xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果没有配置 namespace,那么 id 不能重复;

原因就是 namespace+id 是作为 Map<String, MapperStatement>的 key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id 自然也就不同

20. 为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?

Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 Mybatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具

21. 一对一、一对多的关联查询

一对一 xml 配置方案:

在需要进行关联查询的 mapper 中配置 association 节点,其有以下几个属性:

  • property:对应该实体类关联另一个实体类的变量名
  • column:对应表的字段名
  • javaType:对应关联的实体类
  • select:指定子查询(注意要加入名称空间)

一对多 xml配置方案:

在 mapper 文件中使用 collection 节点,其有以下几个属性:

  • property:对应表中的 commentsList
  • column:当前表与外表关联的键(一般是id)
  • javaType:对应 property 的类型
  • ofType:对应外表的实体类
<mapper namespace="com.lcb.mapping.userMapper">
    <!--association 一对一关联查询 -->
    <select id="getClass" parameterType="int" resultMap="ClassesResultMap">
        select * from class c,teacher t where c.teacher_id=t.t_id and c.c_id=#{id}
    </select>
    <resultMap type="com.lcb.user.Classes" id="ClassesResultMap">
        <!-- 实体类的字段名和数据表的字段名映射 -->
        <id property="id" column="c_id"/>
        <result property="name" column="c_name"/>
        <association property="teacher" javaType="com.lcb.user.Teacher">
            <id property="id" column="t_id"/>
            <result property="name" column="t_name"/>
        </association>
    </resultMap>

    <!--collection 一对多关联查询 -->
    <select id="getClass2" parameterType="int" resultMap="ClassesResultMap2">
        select * from class c,teacher t,student s where c.teacher_id=t.t_id 
        and c.c_id=s.class_id and c.c_id=#{id}
    </select>
    <resultMap type="com.lcb.user.Classes" id="ClassesResultMap2">
        <id property="id" column="c_id"/>
        <result property="name" column="c_name"/>
        <association property="teacher" javaType="com.lcb.user.Teacher">
            <id property="id" column="t_id"/>
            <result property="name" column="t_name"/>
        </association>
        <collection property="student" ofType="com.lcb.user.Student">
            <id property="id" column="s_id"/>
            <result property="name" column="s_name"/>
        </collection>
    </resultMap>
</mapper>

22. MyBatis 实现一对一有几种方式?具体怎么操作的?

有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在 resultMap 里面配置 association 节点配置一对一的类就可以完成;

嵌套查询是先查一个表,根据这个表里面的结果的外键 id,再去另外一个表里面查询数据,也是通过 association 配置,但另外一个表的查询通过 select 属性配置。

同理,对于一对多也有两种,对应将 association 节点改成 collection 即可。

23. Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?

Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false

它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke() 方法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。这就是延迟加载的基本原理。

延迟加载分为积极延迟加载和非积极延迟加载,当开启延迟加载后,默认是积极延迟加载。

  • 积极延迟加载:只要获取任意属性内容,无论属性是否是关联表的内容,都会执行关联的表查询
  • 非积极延迟加载:使用非延迟加载,只有当访问关联表数据时,才会执行关联表的查询。
<settings >
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

24. Mybatis 的一级、二级缓存

一级缓存: 基于 PerpetualCache(永久缓存) 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空。另外,如果在两次查询中间有增删改的操作,那么 MyBatis 就会重新刷新清空缓存区,那么下一次查询会重新执行 sql 语句默认打开一级缓存。

二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),在它的映射文件中配置 <cache/>

public class Blogs implements Serializable {
    ...
}
<cache  />

对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存 Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。

25. 什么是 MyBatis 的接口绑定?有哪些实现方式?

接口绑定,就是在 MyBatis 中任意定义接口,然后把接口里面的方法和 SQL 语句绑定, 我们直接调用接口方法就可以,这样比起原来了 SqlSession 提供的方法我们可以有更加灵活的选择和设置。

接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上 @Select@Update 等注解,里面包含 Sql 语句来绑定;另外一种就是通过 xml 里面写 SQL 来绑定, 在这种情况下,要指定 xml 映射文件里面的 namespace 必须为接口的全路径名。当 Sql 语句比较简单时候,用注解绑定, 当 SQL 语句比较复杂时候,用 xml 绑定,一般用 xml 绑定的比较多。

26. 使用 MyBatis 的 mapper 接口调用时有哪些要求?

  1. Mapper 接口方法名和 mapper.xml 中定义的每个 sql 的 id 相同;
  2. Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的 parameterType 的类型相同;
  3. Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的 resultType 的类型相同;
  4. Mapper.xml 文件中的 namespace 即是 mapper 接口的类路径。

27. Mapper 编写有哪几种方式?

第一种:使用传统Dao层开发

接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写 mapper 接口,mapper 接口实现类、mapper.xml 文件。

  1. 在 sqlMapConfig.xml 中配置 mapper.xml 的位置
<mappers>
    <mapper resource="mapper.xml 文件的地址" />
    <mapper resource="mapper.xml 文件的地址" />
</mappers>
  1. 定义 mapper 接口
  2. 实现类继承 SqlSessionDaoSupport,mapper 方法中可以通过 this.getSqlSession()进行数据增删改查。
public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {
    @Override
    public User selectUserById(int id) {
        User user = this.getSqlSession().selectOne("com.xm.dao.UserDao.selectUserById", id);//指定到后面需要写的mapper映射文件的命名空间下
        return user;
    }
}
  1. spring 配置
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"></property>
    <!-- 注入mybatis配置文件 -->
    <property name="configLocation" value="classpath:spring-mybatis.xml"></property>
</bean>

第二种:Mapper动态代理开发(不用自己写Dao实现类)

  1. 使用 org.mybatis.spring.mapper.MapperFactoryBean :在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xmlmapper 接口的名称相同且在同一个目录,这里可以不用配置:
<mappers>
    <mapper resource="mapper.xml 文件的地址" />
    <mapper resource="mapper.xml 文件的地址" />
</mappers>
  1. 定义 mapper 接口:
  • mapper.xml 中的 namespace 为 mapper 接口的地址
  • mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致
  • Spring 中定义
<bean id="userDao" class="org.mybatis.spring.mapper.MapperFactoryBean">
    <property name="sqlSessionFactory" ref="sqlSessionFactoryBean"/>
    <!-- 此处关联到dao层接口 -->
    <property name="mapperInterface" value="com.xm.dao.UserDao"/>
</bean>

第三种:使用 mapper 扫描器

  1. mapper.xml 文件编写:
  • mapper.xml 中的 namespacemapper 接口的地址;
  • mapper 接口中的方法名和 mapper.xml 中的定义的 statementid 保持一致;
  • 如果将 mapper.xml 和 mapper接口的名称保持一致则不用在 sqlMapConfig.xml 中进行配置。
  1. 定义 mapper 接口:注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录
  2. 配置 mapper 扫描器:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!-- 基本包 -->
    <property name="basePackage" value="com.xm.dao"/>
</bean>
  1. 使用扫描器后从 spring 容器中获取 mapper 的实现对象。

28. 简述 Mybatis 的插件运行原理,以及如何编写一个插件。

Mybatis 仅可以编写针对 ParameterHandlerResultSetHandlerStatementHandlerExecutor 这 4 种接口的插件,Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandlerinvoke()方法,当然,只会拦截那些你指定需要拦截的方法。

编写插件:实现 Mybatis 的 Interceptor 接口并复写 ntercept() 方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。

参考目录

面试官问你MyBatis SQL是如何执行的?把这篇文章甩给他
Mybatis面试题吐血总结