Mybatis复习总结

Mybatis

什么是动态sql

什么是延迟加载

什么是动态代理?为什么Mapper接口不需要实现类?

涉众:哪些人提出问题,那些人受到影响,提出者和关联着

影响:对在问题域的影响

常见标签

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

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置。
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句。

结果映射 ResultMap

resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。

之前你已经见过简单映射语句的示例,它们没有显式指定 resultMap。比如:

<select id="selectUsers" resultType="map">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

上述语句只是简单地将所有的列映射到 HashMap 的键上,这由 resultType 属性指定。虽然在大部分情况下都够用,但是 HashMap 并不是一个很好的领域模型。你的程序更可能会使用 JavaBean 或 POJO(Plain Old Java Objects,普通老式 Java 对象)作为领域模型。

MyBatis 对两者都提供了支持。看看下面这个 JavaBean:

package com.someapp.model;
public class User {
  private int id;
  private String username;
  private String hashedPassword;

  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  public String getHashedPassword() {
    return hashedPassword;
  }
  public void setHashedPassword(String hashedPassword) {
    this.hashedPassword = hashedPassword;
  }
}

这样的一个 JavaBean 可以被映射到 ResultSet,就像映射到 HashMap 一样简单。

<select id="selectUsers" resultType="com.someapp.model.User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

类型别名是你的好帮手。使用它们,你就可以不用输入类的全限定名了。 比如:

<!-- mybatis-config.xml 中 -->
<typeAlias type="com.someapp.model.User" alias="User"/>

<!-- SQL 映射 XML 中 -->
<select id="selectUsers" resultType="User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

在这些情况下,MyBatis 会在幕后自动创建一个 ResultMap,再根据属性名来映射列到 JavaBean 的属性上。如果列名和属性名不能匹配上,可以在 SELECT 语句中设置列别名(这是一个基本的 SQL 特性)来完成匹配。比如:

<select id="selectUsers" resultType="User">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password     as "hashedPassword"
  from some_table
  where id = #{id}
</select>

在学习了上面的知识后,你会发现上面的例子没有一个需要显式配置 ResultMap,这就是 ResultMap 的优秀之处——你完全可以不用显式地配置它们。

显式使用外部的 resultMap :

<resultMap id="userResultMap" type="User">
  <id property="id" column="user_id" />
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>

然后在引用它的语句中设置 resultMap 属性就行了(注意我们去掉了 resultType 属性)。比如:

<select id="selectUsers" resultMap="userResultMap">
  select user_id, user_name, hashed_password
  from some_table
  where id = #{id}
</select>

ResultMap的使用语法

结果映射(resultMap)
  • constructor -用于在实例化类时,注入结果到构造方法中

    • idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    • arg - 将被注入到构造方法的一个普通结果
  • id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能

  • result – 注入到字段或 JavaBean 属性的普通结果

  • association– 一个复杂类型的关联;许多结果将包装成这种类型

    • 嵌套结果映射 – 关联可以是 resultMap 元素,或是对其它结果映射的引用
  • collection– 一个复杂类型的集合

    • 嵌套结果映射 – 集合可以是 resultMap 元素,或是对其它结果映射的引用
  • discriminator

    – 使用结果值来决定使用哪个

    resultMap

    • case– 基于某些值的结果映射
      • 嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射

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

#{} : MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法。

${} : 不会修改或转义该字符串了。例如:ORDER BY ${columnName} , 就把 columnName 看作字符串了。用这种方式接受用户的输入,并用作语句参数是不安全的,会导致潜在的 SQL 注入攻击。因此,要么不允许用户输入这些字段,要么自行转义并检验这些参数。

当 SQL 语句中的元数据(如表名或列名)是动态生成的时候,字符串替换将会非常有用。 举个例子,如果你想 select 一个表任意一列的数据时,不需要这样写:

Select("select * from user where id = #{id}")
User findById(@Param("id") long id);

@Select("select * from user where name = #{name}")
User findByName(@Param("name") String name);

@Select("select * from user where email = #{email}")
User findByEmail(@Param("email") String email);

// 其它的 "findByXxx" 方法

可以写成一个方法

@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

动态sql

1. 什么是动态SQL?

关于动态 SQL ,允许我们理解为 “ 动态的 SQL ”,其中 “ 动态的 ” 是形容词,“ SQL ” 是名词,那显然我们需要先理解名词,毕竟形容词仅仅代表它的某种形态或者某种状态。

SQL 的全称是:

Structured Query Language,结构化查询语言。

动态 SQL:

一般指根据用户输入或外部条件 动态组合 的 SQL 语句块。

如果一个 SQL 语句在编译阶段无法确定主体结构,需要等到程序真正 “运行时” 才能最终确定,那么我们称之为动态 SQL

<!-- 1、定义SQL -->
<mapper namespace="dao">
    <select id="selectAll" parameterType="user">
        select * from t_user 
        <if test="id != null">
            where id = #{id}
        </if>
    </select>
</mapper>

静态SQL:

如果一个SQL语句在“编译阶段”就已经能确定主体结构,那就称之为静态SQL

<!-- 1、定义SQL -->
<mapper namespace="dao">
    <select id="selectAll" resultType="user">
        select * from t_user
    </select>
</mapper>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p3Ta0ao2-1664329772535)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20220914100133657.png)]

还有一种常见的情况,大家看看下面这个 SQL 语句算是动态 SQL 语句吗?

<!-- 1、定义SQL -->
<mapper namespace="dao">
    <select id="selectAll" parameterType="user">
        select * from t_user where id = #{id} 
    </select>
</mapper>

答案:不属于动态 SQL !

原因很简单,这个 SQL 在编译阶段就已经明确主体结构了,虽然外部动态的传入一个 id ,可能是1,可能是2,可能是100,但是因为它的主体结构已经确定,这个语句就是查询一个指定 id 的用户记录,它最终执行的 SQL 语句不会有任何动态的变化,所以顶多算是一个支持动态传参的静态 SQL 。

2.动态SQL的诞生记

那这么多好工具,都提供什么功能呢?相信我们平时接触最多的就是接收执行 SQL 语句的输入界面(也称为查询编辑器),这个输入界面几乎支持所有 SQL 语法,例如我们编写一条语句查询 id 等于15 的用户数据记录:

很显然,在这个输入界面内输入的任何 SQL 语句,对于数据库管理工具来说,都是 动态 SQL

早期动态SQL构建方式

// 外部条件id
Integer id = Integer.valueOf(15);

// 动态拼接SQL
StringBuilder sql = new StringBuilder();
sql.append(" select  *   ");
sql.append("   from user ");

// 根据外部条件id动态拼接SQL
if ( null != id ){
    sql.append(" where id = " + id);
}

// 执行语句
connection.prepareStatement(sql);

只不过,这种构建动态 SQL 的方式,存在很大的安全问题和异常风险(我们第5点会详细介绍),所以不建议使用,后来 Mybatis 入世之后,在对待动态 SQL 这件事上,就格外上心,它默默发誓,一定要为使用 Mybatis 框架的用户提供一套棒棒的方案(标签)来灵活构建动态 SQL!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3cVbjwDc-1664329772537)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20220914100447208.png)]

于是乎,Mybatis 借助 OGNL 的表达式的伟大设计,可算在动态 SQL 构建方面提供了各类功能强大的辅助标签,我们简单列举一下有:if、choose、when、otherwise、trim、where、set、foreach、bind等

3.动态sql的九大标签

3.1 if 标签

if 标签,绝对算得上是一个伟大的标签,任何不支持流程控制(或语句控制)的应用程序,都是耍流氓,几乎都不具备现实意义,实际的应用场景和流程必然存在条件的控制与流转,而 if 标签在 单条件分支判断 应用场景中就起到了舍我其谁的作用,语法很简单,如果满足,则执行,不满足,则忽略/跳过。

  • if 标签 : 内嵌于 select / delete / update / insert 标签,如果满足 test 属性的条件,则执行代码块
  • test 属性 :作为 if 标签的属性,用于条件判断,使用 OGNL 表达式
<select id="findUser">
    select * from User where 1=1
    <if test=" age != null ">
        and age > #{age}
    </if>
    <if test=" name != null ">
        and name like concat(#{name},'%')
    </if>
</select>

每一个 if 标签在进行单条件判断时,需要把判断条件设置在 test 属性中,这是一个常见的应用场景,我们常用的用户查询系统功能中,在前端一般提供很多可选的查询项,支持性别筛选、年龄区间筛查、姓名模糊匹配等,那么我们程序中接收用户输入之后,Mybatis 的动态 SQL 节省我们很多工作,允许我们在代码层面不进行参数逻辑处理和 SQL 拼接,而是把参数传入到 SQL 中进行条件判断动态处理,我们只需要把精力集中在 XML 的维护上,既灵活也方便维护,可读性还强。

注意:if 标签作为单条件分支判断,只能控制与非此即彼的流程,例如以上的例子,如果年龄 age 和姓名 name 都不存在,那么系统会把所有结果都查询出来,但有些时候,我们希望系统更加灵活,能有更多的流程分支,例如像我们 Java 当中的 if else 或 switch case default,不仅仅只有一个条件分支,所以接下来我们介绍 choose 标签,它就能满足多分支判断的应用场景

3.2 choose 、when 、otherwise 标签

有些时候,我们并不希望条件控制是非此即彼的,而是希望能提供多个条件并从中选择一个,所以贴心的 Mybatis 提供了 choose 标签元素,类似我们 Java 当中的 if else 或 switch case default,choose 标签必须搭配 when 标签和 otherwise 标签使用,验证条件依然是使用 test 属性进行验证。

  • choose 标签:顶层的多分支标签,单独使用无意义
  • when 标签:内嵌于 choose 标签之中,当满足某个 when 条件时,执行对应的代码块,并终止跳出 choose 标签,choose 中必须至少存在一个 when 标签,否则无意义
  • otherwise 标签:内嵌于 choose 标签之中,当不满足所有 when 条件时,则执行 otherwise 代码块,choose 中 至多 存在一个 otherwise 标签,可以不存在该标签
  • test 属性 :作为 when 与 otherwise 标签的属性,作为条件判断,使用 OGNL 表达式

依据下面的例子,当应用程序输入年龄 age 或者姓名 name 时,会执行对应的 when 标签内的代码块,如果 when 标签的年龄 age 和姓名 name 都不满足,则会拼接 otherwise 标签内的代码块。

<select id="findUser">
    select * from User where 1=1 
    <choose>
        <when test=" age != null ">
            and age > #{age}
        </when>
        <when test=" name != null ">
            and name like concat(#{name},'%')
        </when>
        <otherwise>
            and sex = '男'
        </otherwise>
    </choose>
</select>

很明显,choose 标签作为多分支条件判断,提供了更多灵活的流程控制,同时 otherwise 的出现也为程序流程控制兜底,有时能够避免部分系统风险、过滤部分条件、避免当程序没有匹配到条件时,把整个数据库资源全部查询或更新。

3.3 foreach 标签

有些场景,可能需要查询 id 在 1 ~ 100 的用户记录

有些场景,可能需要批量插入 100 条用户记录

有些场景,可能需要更新 500 个用户的姓名

有些场景,可能需要你删除 10 条用户记录

Mybatis 提供了 foreach 标签来处理这几类需要遍历集合的场景,foreach 标签作为一个循环语句,他能够很好的支持数组、Map、或实现了 Iterable 接口(List、Set)等,尤其是在构建 in 条件语句的时候,我们常规的用法都是 id in (1,2,3,4,5 … 100) ,理论上我们可以在程序代码中拼接字符串然后通过 ${ ids } 方式来传值获取,但是这种方式不能防止 SQL 注入风险,同时也特别容易拼接错误,所以我们此时就需要使用 #{} + foreach 标签来配合使用,以满足我们实际的业务需求。譬如我们传入一个 List 列表查询 id 在 1 ~ 100 的用户记录:

<select id="findAll">
    select  * from user where ids in 
    <foreach collection="list"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

最终拼接完整的语句就变成:

select  * from user where ids in (1,2,3,...,100);

当然你也可以这样编写:

<select id="findAll">
    select  * from user where 
    <foreach collection="list"
        item="item" index="index" 
        open=" " separator=" or " close=" ">
            id = #{item}
    </foreach>
</select>

最终拼接完整的语句就变成:

select  * from user where id =1 or id =2 or id =3  ... or id = 100;
3.4 where 标签、set 标签

我们把 where 标签和 set 标签放置一起讲解,一是这两个标签在实际应用开发中常用度确实不分伯仲,二是这两个标签出自一家,都继承了 trim 标签,放置一起方便我们比对追根。(其中底层原理会在第4部分详细讲解)

之前我们介绍 if 标签的时候,相信大家都已经看到,我们在 where 子句后面拼接了 1=1 的条件语句块,目的是为了保证后续条件能够正确拼接,以前在程序代码中使用字符串拼接 SQL 条件语句常常如此使用,但是确实此种方式不够体面,也显得我们不高级。

<select id="findUser">
    select * from User where 1=1
    <if test=" age != null ">
        and age > #{age}
    </if>
    <if test=" name != null ">
        and name like concat(#{name},'%')
    </if>
</select>

以上是我们使用 1=1 的写法,那 where 标签诞生之后,是怎么巧妙处理后续的条件语句的呢?

<select id="findUser">
    select * from User 
    <where>
        <if test=" age != null ">
            and age > #{age}
        </if>
        <if test=" name != null ">
            and name like concat(#{name},'%')
        </if>
    </where>
</select>

我们只需把 where 关键词以及 1=1 改为 < where > 标签即可,另外还有一个特殊的处理能力,就是 where 标签能够智能的去除(忽略)首个满足条件语句的前缀,例如以上条件如果 age 和 name 都满足,那么 age 前缀 and 会被智能去除掉,无论你是使用 and 运算符或是 or 运算符,Mybatis 框架都会帮你智能处理。

值得注意的是,where 标签 只会 智能的去除(忽略)首个满足条件语句的前缀,所以就建议我们在使用 where 标签的时候,每个语句都最好写上 and 前缀或者 or 前缀,否则像以下写法就很有可能出大事:

<select id="findUser">
    select * from User 
    <where>
        <if test=" age != null ">
             age > #{age} 
             <!-- age 前缀没有运算符-->
        </if>
        <if test=" name != null ">
             name like concat(#{name},'%')
             <!-- name 前缀也没有运算符-->
        </if>
    </where>
</select>

当 age 传入 10,name 传入 ‘潘潘’ 时,最终的 SQL 语句是:

select * from User 
where 
age > 10 
name like concat('潘%')
-- 所有条件都没有and或or运算符
-- 这让age和name显得很尴尬~
  • set 标签
<update id="updateUser">
    update user 
       set age = #{age},
           username = #{username},
           password = #{password} 
     where id =#{id}
</update> 

以上语句是我们日常用于更新指定 id 对象的 age 字段、 username 字段以及 password 字段,但是很多时候,我们可能只希望更新对象的某些字段,而不是每次都更新对象的所有字段,这就使得我们在语句结构的构建上显得惨白无力。于是有了 set 标签元素。

用法与 where 标签元素相似

  • set 标签:顶层的遍历标签,需要配合 if 标签使用,单独使用无意义,并且只会在子元素(如 if 标签)返回任何内容的情况下才插入 set 子句。另外,若子句的 开头或结尾 都存在逗号 “,” 则 set 标签都会将它替换去除。
<update id="updateUser">
    update user 
        <set>
           <if test="age !=null">
               age = #{age},
           </if>
           <if test="username !=null">
                  username = #{username},
           </if> 
           <if test="password !=null">
                  password = #{password},
           </if>
        </set>    
     where id =#{id}
</update> 

很简单易懂,set 标签会智能拼接更新字段,以上例子如果传入 age =10 和 username = ‘潘潘’ ,则有两个字段满足更新条件,于是 set 标签会智能拼接 " age = 10 ," 和 “username = ‘潘潘’ ,” 。其中由于后一个 username 属于最后一个子句,所以末尾逗号会被智能去除,最终的 SQL 语句是:

update user set age = 10,username =  '潘潘' 
3.5 trim 标签

上面我们介绍了 where 标签与 set 标签,它俩的共同点无非就是前置关键词 where 或 set 的插入,以及前后缀符号(例如 AND | OR | ,)的智能去除。基于 where 标签和 set 标签本身都继承了 trim 标签,所以 trim 标签的大致实现我们也能猜出个一二三。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ImHgzRTG-1664329772537)(https://segmentfault.com/img/remote/1460000039335738)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CmXjOdsQ-1664329772538)(https://segmentfault.com/img/remote/1460000039335738)]

其实 where 标签和 set 标签都只是 trim 标签的某种实现方案,trim 标签底层是通过 TrimSqlNode 类来实现的,它有几个关键属性:

  • prefix :前缀,当 trim 元素内存在内容时,会给内容插入指定前缀
  • suffix :后缀,当 trim 元素内存在内容时,会给内容插入指定后缀
  • prefixesToOverride :前缀去除,支持多个,当 trim 元素内存在内容时,会把内容中匹配的前缀字符串去除。
  • suffixesToOverride :后缀去除,支持多个,当 trim 元素内存在内容时,会把内容中匹配的后缀字符串去除。

所以 where 标签如果通过 trim 标签实现的话可以这么编写:

<!--
  注意在使用 trim 标签实现 where 标签能力时
  必须在 AND 和 OR 之后添加空格
  避免匹配到 android、order 等单词 
-->
<trim prefix="WHERE" prefixOverrides="AND | OR" >
    ...
</trim>

而 set 标签如果通过 trim 标签实现的话可以这么编写:

<trim prefix="SET" prefixOverrides="," >
    ...
</trim>

或者

<trim prefix="SET" suffixesToOverride="," >
    ...
</trim>

所以可见 trim 是足够灵活的,不过由于 where 标签和 set 标签这两种 trim 标签变种方案已经足以满足我们实际开发需求,所以直接使用 trim 标签的场景实际上不太很多(其实是我自己使用的不多,基本没用过)。

3.6 bind 标签

简单来说,这个标签就是可以创建一个变量,并绑定到上下文,即供上下文使用,就是这样,我把官网的例子直接拷贝过来:

<select id="selecUser">
  <bind name="myName" value="'%' + _parameter.getName() + '%'" />
  SELECT * FROM user
  WHERE name LIKE #{myName}
</select>

大家应该大致能知道以上例子的功效,其实就是辅助构建模糊查询的语句拼接,那有人就好奇了,为啥不直接拼接语句就行了,为什么还要搞出一个变量,绕一圈呢?

这是mysql里面模糊查询语句:

select * from user where name like concat('%',#{name},'%')

当切换到Oracle中:就使用不了,得改成下列形式

select * from user where name like '%'||#{name}||'%'

bind标签就解决了这种问题,但实际工作中发生概率不大。

3.7 sql标签 + include 标签

sql 标签与 include 标签组合使用,用于 SQL 语句的复用,日常高频或公用使用的语句块可以抽取出来进行复用,其实我们应该不陌生,早期我们学习 JSP 的时候,就有一个 include 标记可以引入一些公用可复用的页面文件,例如页面头部或尾部页面代码元素,这种复用的设计很常见。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z4Uvfw9m-1664329772539)(https://segmentfault.com/img/remote/1460000039335741)]

简单的复用代码块可以是:

<!-- 可复用的字段语句块 -->
<sql id="userColumns">
    id,username,password 
</sql>

查询或插入时简单复用:

<!-- 查询时简单复用 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"></include> 
  from user 
</select>

<!-- 插入时简单复用 -->
<insert id="insertUser" resultType="map">
  insert into user(
    <include refid="userColumns"></include> 
  )values(
    #{id},#{username},#{password} 
  )  
</insert>

当然,复用语句还支持属性传递,例如:

<!-- 可复用的字段语句块 -->
<sql id="userColumns">
    ${pojo}.id,${pojo}.username 
</sql>

这个 SQL 片段可以在其它语句中使用:

<!-- 查询时复用 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns">
        <property name="pojo" value="u1"/>
    </include>,
    <include refid="userColumns">
        <property name="pojo" value="u2"/>
    </include>
  from user u1 cross join user u2
</select>

mybatis运行过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsTDkHb3-1664329772539)(https://segmentfault.com/img/remote/1460000039107459/view)]

根据以上框架流程图进行讲解

  • 创建配置并调用API:这一环节发生在应用程序端,是开发人员在实际应用程序中进行的两步操作,第一步创建核心配置文件 Configuration.xml 和映射文件 mapper.xml (通过注解方式也可创建以上两种配置),准备好基础配置和 SQL 语句之后;第二步就是直接调用 Mybatis 框架中的数据库操作接口。
  • 加载配置并初始化:Mybatis 框架会根据应用程序端提供的核心配置文件与 SQL 映射文件的内容,使用资源辅助类 Resources 把配置文件读取成输入流,然后通过对应的解析器解析并封装到 Configuration 对象和 MappedStatement 对象,最终把对象存储在内存之中。
  • 创建会话并接收请求:在 Mybatis 框架加载配置并初始化配置对象之后,会话工厂构建器 SqlSessionFactoryBuilder 同时创建会话工厂 SqlSessionFactory,会话工厂会根据应用程序端的请求,创建会话 SqlSession,以便应用程序端进行数据库交互。
  • 处理请求:SqlSession 接收到请求之后,实际上并没有进行处理,而是把请求转发给执行器 Executor,执行器再分派到语句处理器 StatementHandler ,语句处理器会结合参数处理器 ParameterHandler ,进行数据库操作(底层封装了 JDBC Statement 操作)。
  • 返回处理结果:每个语句处理器 StatementHandler 处理完成数据库操作之后,会协同 ResultSetHandler 以及类型处理器 TypeHandler ,对底层 JDBC 返回的结果集进行映射封装,最终返回封装对象。

1.Configuration – 全局配置对象

Configuration 对象的结构和 config.xml 配置文件的内容几乎相同,涵盖了properties (属性),settings (设置),typeAliases (类型别名),typeHandlers (类型处理器),objectFactory (对象工厂),mappers (映射器)等等

在这里插入图片描述

配置对象 Configuration 通过解析器 XMLConfigBuilder 进行解析,把全局配置文件 Config.xml 与 映射器配置文件 Mapper.xml 中的配置信息全部构建成完整的 Configuration 对象,后续我们源码分析时详细剖析整个过程。

2. Resources – 资源辅助类

像 Configuration 和 Mapper 的配置信息存放在 XML 文件中,Mybatis 框架在构建配置对象时,必须先把 XML 文件信息加载成流,再做后续的解析封装,而 Resources 作为资源的辅助类,恰恰干的就是这个活,无论是通过加载本地资源或是加载远程资源,最终都会通过 类加载器 访问资源文件并输出文件流。

在这里插入图片描述

//加载核心配置文件
InputStream resourceAsStream = 
    Resources.getResourceAsStream("Config.xml");

3. SqlSessionFactoryBuilder – 会话工厂构建器

我们一撞见 xxxBuilder ,就大致能知道它是某类对象的构建器,这里 SqlSessionFactoryBuilder 也是一样,它是 Mybatis 中的一个会话工厂构建器,在资源辅助类 Resources 读取到文件流信息之后,它负责解析文件流信息并构建会话工厂 SqlSessionFactory。(解析的配置文件包含:全局配置 Configuration 与映射器 Mapper)

在这里插入图片描述

在程序应用端,我们一般使用 SqlSessionFactoryBuilder 直接构建会话工厂:

// 获得sqlSession工厂对象
SqlSessionFactory sqlSessionFactory = 
    new SqlSessionFactoryBuilder().build(resourceAsStream);

当然,如果你集成了 Spring 框架的项目,则不需要自己手工去构建会话工厂,直接在 Spring 配置文件中指定即可,例如指定一个 bean 对象,id 是 sqlSessionFactory,而 class 类指定为 org.mybatis.spring.SqlSessionFactoryBean 。

什么是构建这模式?

创建者模式又叫建造者模式,是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

创建者模式隐藏了复杂对象的创建过程,它把复杂对象的创建过程加以抽象,通过子类继承或者重载的方式,动态的创建具有符合属性的对象

会话工厂构建器 SqlSessionFactoryBuilder 应用了构建者模式,主要目的就是为了构建 SqlSessionFactory 对象,以便后续生产 SqlSession 对象,这个构造器基本上算是 Mybatis 框架的入口构建器,它提供了一系列多态方法 build(),支持用户使用 XML 配置文件或 Java API (Properties)来构建会话工厂 SqlSessionFactory 实例。

在这里插入图片描述

SqlSessionFactoryBuilder 的一生只为成就 SqlSessionFactory,当 SqlSessionFactory 一经实例,SqlSessionFactoryBuilder 使命完成,便可消亡,便可被丢弃

在这里插入图片描述

因此 SqlSessionFactoryBuilder 实例的最佳作用域是 方法作用域(也就是局部方法变量)。

4. SqlSessionFactory – 会话工厂

会话工厂 SqlSessionFactory 是一个接口,作用是生产数据库会话对象 SqlSession ,有两个实现类:

  • DefaultSqlSessionFactory (默认实现)
  • SqlSessionManager (仅多实现了一个 Sqlsession 接口,已弃用)

在介绍会话工厂构建器 SqlSessionFactoryBuilder 的时候,我们了解到构建器默认创建了 DefaultSqlSessionFactory 实例,并且会话工厂本身会绑定一个重要的属性 Configuration 对象,在生产会话时,最终也会把 Configuration 配置对象传递并设置到会话 SqlSession 上。

在这里插入图片描述

会话工厂可以简单创建 SqlSession 实例:

// 创建 SqlSession 实例
SqlSession session = sqlSessionFactory.openSession();

另外,会话工厂其实提供了一系列接口来灵活生产会话 SqlSession,你可以指定:

  • 事务处理:你希望在 session 作用域中使用/开启事务作用域(也就是不自动提交事务),还是使用自动提交(auto-commit),sqlSession 默认不提交事务,对于增删改操作时需要手动提交事务。
  • 数据库连接:你希望 MyBatis 帮你从已配置的数据源获取连接,还是使用自己提供的连接,可以动态创建数据源对象 Connection。
  • 执行器类型:你希望指定某类执行器来创建/执行/预处理语句,可以有普通执行器(SimpleExecutor),或复用执行器(ReuserExecutor)、还是批量执行器(BatchExecutor)等,下面介绍执行器时会详细说明。
  • 事务隔离级别支持:支持 JDBC 的五个隔离级别(NONE、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ 和 SERIALIZABLE),对于事务相关的内容,我们后续 《spring 系列全解》 会详细讲,这里简单说明一下就是事务隔离级别主要为了解决例如脏读、不可重复读、幻读等问题,使用不同的事务隔离级别势必会导致不同的数据库执行效率,因此我们再不同的系统/功能中,对隔离级别有不同的需求。

SqlSessionFactory 一旦被创建就应该在 应用的运行期间 一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是 应用作用域。 最简单的就是使用单例模式或者静态单例模式

5. SqlSession – 会话

SqlSession 是一个接口,有两个实现类:

  • DefaultSqlSession(默认实现)
  • SqlSessionManager (已弃用)

简单来说,通过会话工厂构建出 SqlSession 实例之后,我们就可以进行增删改查了,默认实例 DefaultSqlSession 提供了如此多的方法供用户使用,有超过30个:

在这里插入图片描述

sqlSession 的方法除了 CURD,还提供了事务的控制例如提交/关闭/回滚等、提供了配置对象的获取例如 getConfiguration()、提供了批量语句的执行更新例如 flushStatements()、提供了缓存清除例如 clearCache() 、提供了映射器的使用 getMapper() 等等。

对于客户端应用层面来说,熟悉 sqlSession 的 API 基本就可以任意操作数据库了,不过我们希望想进一步了解 sqlSession 内部是如何执行 sql 呢?其实 sqlSession 是 Mybatis 中用于和数据库交互的 顶层类,通常将它与本地线程 ThreadLocal 绑定,一个会话使用一个 SqlSession,并且在使用完毕之后进行 关闭

Spring 集成 Mybatis 之后,通过依赖注入可以创建线程安全的、基于事务的 SqlSession ,并管理他们的生命周期,推荐搭配使用。

6.Executor – 执行器

Executor 是一个执行器接口,是 Mybatis 的调度核心,它定义了一组管理 Statement 对象与获取事务的方法,并负责 SQL 语句的生成和一级/二级查询缓存的维护等,SqlSessionFactory 在创建 SqlSession 时会同时创建执行器,并指定执行器类型,默认使用 SimpleExecutor 。

执行器接口有5个子孙实现类,其中 BaseExecutor 是抽象类,另外4个子孙实现类分别是:SimpleExecutor 、BatchExecutor、ReuseExecutor、CachingExecutor。

在这里插入图片描述

  • BaseExecutor:基础执行器(抽象类),对Executor接口进行了基本实现,为下一级实现类执行器提供基础支持。BaseExecutor 有三个子类分别是 SimpleExecutor、ResuseExecutor、BatchExecutor。

  • SimpleExecutor:普通执行器,继承 BaseExecutor 抽象类,是 MyBatis 中 默认 使用的执行器. 每执行一次 update 或 select ,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。(可以是 Statement 或 PrepareStatement 对象)。

  • ReuseExecutor:复用执行器,继承 BaseExecutor 抽象类,这里的复用指的是重复使用 Statement . 它会在内部利用一个 Map 把创建的 Statement 都缓存起来,每次在执行一条 SQL语 句时,它都会去判断之前是否存在基于该 SQL 缓存的 Statement 对象,存在且之前缓存的 Statement 对象对应的 Connection 还没有关闭则会继续使用之前的 Statement 对象,否则将创建一个新的 Statement 对象,并将其缓存起来。因为每一个新的 SqlSession 都有一个新的 Executor 对象,所以我们缓存在 ReuseExecutor 上的 Statement 的作用域是同一个 SqlSession 。

  • BatchExecutor:批处理执行器,继承 BaseExecutor 抽象类,通过批量操作来提高性能,用于将多个 sql 语句一次性输送到数据库执行。由于内部有缓存的实现,所以使用完成后需要调用 flushStatements() 来清除缓存。

  • CachingExecutor : 缓存执行器,继承 BaseExecutor 抽象类,它为 Executor 对象增加了 二级缓存 的相关功能,cachingExecutor 有一个重要属性 delegate,即为委托的执行器对象,可以是 SimpleExecutor、ReuseExecutor、BatchExecutor 中任意一个。CachingExecutor 在执行数据库 update 操作时,它直接调用 委托对象 delegate 的 update 方法;而执行查询时,它会先从缓存中获取查询结果,存在就返回,不存在则委托 delegate 去数据库取,然后存储到缓存 cache 中。

Mybatis 在构建 Configuration 配置类时默认把 ExecutorType.SIMPLE 作为执行器类型,当我们的会话工厂 DefaultSqlSessionFactory 开始生产 SqlSession 会话时,会同时构建执行器,此时就会依据配置类 Configuration 构建时指定的执行器类型来实例具体执行器

7. StatementHandler – 语句处理器

StatementHandler 是一个语句处理器接口,它封装了 JDBC Statement 操作,负责对 JDBC Statement 的操作,如 设置参数、结果集映射,是实际跟数据库做交互的一道。

StatementHandler 语句处理器实例,是在执行器具体执行 CRUD 操作时构建的,默认使用 PrepareStatementHandler。语句处理器接口有5个子孙实现类,其中 BaseStatementHandler 是抽象类,另外4个子孙实现类分别是:SimpleStatementHandler、PrepareStatementHandler、CallableStatementHandler、RoutingStatementHandler。

在这里插入图片描述

  • BaseStatementHandler:基础语句处理器(抽象类),它基本把语句处理器接口的核心部分都实现了,包括配置绑定、执行器绑定、映射器绑定、参数处理器构建、结果集处理器构建、语句超时设置、语句关闭等,并另外定义了新的方法 instantiateStatement 供不同子类实现以便获取不同类型的语句连接,子类可以普通执行 SQL 语句,也可以做预编译执行,还可以执行存储过程等。

  • SimpleStatementHandler:普通语句处理器,继承 BaseStatementHandler 抽象类,对应 java.sql.Statement 对象的处理,处理普通的不带动态参数运行的 SQL,即执行简单拼接的字符串语句,同时由于 Statement 的特性,SimpleStatementHandler 每次执行都需要编译 SQL (注意:我们知道 SQL 的执行是需要编译和解析的)。

  • PreparedStatementHandler:预编译语句处理器,继承 BaseStatementHandler 抽象类,对应 java.sql.PrepareStatement 对象的处理,相比上面的普通语句处理器,它支持可变参数 SQL 执行,由于 PrepareStatement 的特性,它会进行预编译,在缓存中一旦发现有预编译的命令,会直接解析执行,所以减少了再次编译环节,能够有效提高系统性能,并预防 SQL 注入攻击(所以是系统默认也是我们推荐的语句处理器)。

  • CallableStatementHandler:存储过程处理器,继承 BaseStatementHandler 抽象类,对应 java.sql.CallableStatement 对象的处理,很明了,它是用来调用存储过程的,增加了存储过程的函数调用以及输出/输入参数的处理支持。

8. ParamerHandler – 参数处理器

ParameterHandler 是一个参数处理器接口,它负责把用户传递的参数转换成 JDBC Statement 所需要的参数,底层做数据转换的工作会交给类型转换器 TypeHandler

很显然,需要对传入的参数进行转换处理的 StatementHandler 实例只有两个,分别是:

  • PrepareStatementHandler 预编译处理器
  • CallableStatementHandler 存储过程处理器

在这里插入图片描述

9. ResultSetHandler – 结果集处理器

ResultSetHandler 是一个结果集处理器接口,它负责负责将 JDBC 返回的结果集 resultSet 对象转换为 List 类型的集合,是在语句处理器构建实例时被同时创建,底层做数据转换的工作会交给类型转换器 TypeHandler,它有1个默认实现类 DefaultResultSetHandler,该接口有3个方法,分别是:

在这里插入图片描述

  • handleResultSets:负责结果集处理,完成映射返回结果对象
  • handleCursorResultSets :负责游标对象处理
  • handleOutputParameters :负责存储过程的输出参数处理

参考文章: MyBatis常见面试题总结 | JavaGuide

java - Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神!_个人文章 - SegmentFault 思否

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值