简介

什么是 MyBatis?

  • MyBatis 是一款优秀的持久层框架
  • 它支持定制化 SQL、存储过程以及高级映射。
  • MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集(ResultSet)
  • MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中 的记录。

如何获得 MyBatis?

1
2
3
4
5
6
7
8
<!‐‐    https://mvnrepository.com/artifact/org.mybatis/mybatis    ‐‐>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>

<!-- 用3.5.3是因为用的人最多,目前最新3.5.6 -->

持久化

  • 数据持久化

    持久化就是将程序的数据在持久状态和瞬时状态转化的过程

  • 内存特性:断电即失
    因此有了:数据库(JDBC),io 文件持久化 进行存储

  • 生活:冷藏、罐头。

  • 为什么需要持久化? 因为有一些对象,不能让他丢掉。

持久层

层次分为:Dao 层、Service 层、Controller 层…..
因此 持久层 = 完成持久化工作的代码块

为什么需要 Mybatis?

  1. 帮助程序员将数据存入到数据库中。 方便
  2. 传统的 JDBC 代码太复杂了。简化、框架、自动化。
  3. 不用 Mybatis 也可以。更容易上手。但是是技术没有高低之分
  4. 优点:
    1. 简单易学
    2. 灵活
    3. sql 和代码的分离,提高了可维护性。
    4. 提供映射标签,支持对象与数据库的orm 字段关系映射
    5. 提供对象关系映射标签,支持对象关系组建维护
    6. 提供 xml 标签,支持编写动态 sql。
    7. 最重要的一点:使用的人多!
    8. 和 Spring SpringMVC SpringBoot 一起搭配使用

第一个 Mybatis 程序

首先需要注意 ,如果用的是 IDEA,把配置文件写在 resource 包下的话,需要引用以下代码到 pom.xml 以解决资源路径问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</build>

思路:搭建环境‐‐>导入 Mybatis‐‐>编写代码‐‐>测试!

目录结构:

image-20210305220008983

搭建数据库:

1
2
3
4
5
6
CREATE TABLE tbl_employee(
id INT(11) PRIMARY KEY AUTO_INCREMENT,
last_name VARBINARY(255),
GENDER CHAR(1),
email VARCHAR(255)
)

新建项目:

依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>

<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

创建实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.hpg.mybatis.pojo;

public class Employee {
private Integer id;
private String lastName;
private String email;
private String gender;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}
}

编写全局配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC" />
<property name="username" value="root" />
<property name="password" value="123456" />
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="com/hpg/mybatis/Mapper/EmployeeMapper.xml"/>
</mappers>
</configuration>

编写映射文件(Mapper 文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hpg.mybatis.Mapper.EmployeeMapper">
<!--
namespace:名称空间;指定为接口的全类名
id:唯一标识
resultType:返回值类型
#{id}:从传递过来的参数中取出id值

public Employee getEmpById(Integer id);
-->
<select id="getEmpById" resultType="com.hpg.mybatis.pojo.Employee">
select id,last_name lastName,email,gender from tbl_employee where id = #{id}
</select>
</mapper>

编写测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import com.hpg.mybatis.pojo.Employee;
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.IOException;
import java.io.InputStream;

public class MyTest {
@Test

public void mybatistest() throws IOException {
//根据xml配置文件创建一个sqlSessionFactory

// 2、获取sqlSession实例,能直接执行已经映射的sql语句
// sql的唯一标识:statement Unique identifier matching the statement to use.
// 执行sql要用的参数:parameter A parameter object to pass to the statement.
String resource = "com\\hpg\\mybatis\\Mapper\\mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

//获取sqlSession实例 能够执行已经映射的sql语句
SqlSession sqlSession = sqlSessionFactory.openSession();

try {
Employee employee = sqlSession.selectOne("com.hpg.mybatis.Mapper.EmployeeMapper.getEmpById", 1);
System.out.println(employee);
}finally {
sqlSession.close();
}

}
}

对于 selectOne 函数:

image-20210305211236568

第一个参数:sql 唯一标识符;

第二个参数:执行 sql 要用的参数

打印结果:

image-20210305220024227

总的来说可以分成这几步:

  1. 根据 xml 配置文件(全局配置文件)创建一个 SqlSessionFactory 对象 有数据源一些运行环境信息
  2. sql 映射文件;配置了每一个 sql,以及 sql 的封装规则等。
  3. 将 sql 映射文件注册在全局配置文件中
  4. 写代码
    1. 根据全局配置文件得到 SqlSessionFactory;
    2. 使用 sqlSession 工厂,获取到 sqlSession 对象使用他来执行增删改查,一个 sqlSession 就是代表和数据库的一次会话,用完关闭
    3. 使用 sql 的唯一标志来告诉 MyBatis 执行哪个 sql。sql 都是保存在 sql 映射文件中的。

接口式编程

目录结构:

image-20210305222537083

编写接口:

1
2
3
4
5
6
7
8
9
package com.hpg.mybatis.dao;

import com.hpg.mybatis.pojo.Employee;

public interface EmployeeMapper {

public Employee getEmpById(Integer id);
}

将 EmployeeMapper 中的命名空间替换掉:

1
2
3
4
<!--原来-->
<mapper namespace="com.hpg.mybatis.Mapper.EmployeeMapper">
<!--现在-->
<mapper namespace="com.hpg.mybatis.dao.EmployeeMapper">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hpg.mybatis.dao.EmployeeMapper">
<!--
namespace:名称空间;指定为接口的全类名
id:唯一标识
resultType:返回值类型
#{id}:从传递过来的参数中取出id值

public Employee getEmpById(Integer id);
-->
<select id="getEmpById" resultType="com.hpg.mybatis.pojo.Employee">
select id,last_name lastName,email,gender from tbl_employee where id = #{id}
</select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public SqlSessionFactory getSqlSessionFactory() throws IOException {
String resource = "com\\hpg\\mybatis\\Mapper\\mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
return new SqlSessionFactoryBuilder().build(inputStream);
}

@Test
public void InterfaceTest() throws IOException {
//获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
//获取sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
//获取接口的实现类对象
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = mapper.getEmpById(1);
System.out.println(employee);
}finally {
sqlSession.close();
}

}

打印结果:

image-20210305222804382

明明我们没写实现类,那究竟结果是怎么实现的呢?

是由 Mybatis 为接口自动创建的一个代理对象sqlSession.getMapper(xxxx.class)由这个对象去实现 CRUD 的,当然了,使用之前需要将接口和 xml 进行绑定操作

  • 接口式编程

    • 原生:Dao → DaoImpl、
    • Mybatis:Mapper → XXXMapper.xml
  • SqlSession 代表和数据库的一次会话,因此用完必须关闭

  • SqlSession 和 connection 一样是非线程安全的,因此不能够设置 private …每次使用的时候应该去主动获取

  • 有两个重要的配置文件

    • mybatis 全局配置文件(可以不专门写一个出来,可以 new 出来):包含了数据库连接池信息,事务管理器信息,系统运行环境信息等
    • sql 映射文件:保存了每一个 sql 语句的映射信息;这个文件将 sql 抽取了出来

全局配置文件

configuration 下有以下几个标签:

image-20210306150224976

properties 标签

image-20210306150215773

  • resource:resource 属性是按照类路径的写法来写的,因此必须存在于类路径下

  • url:URL: Uniform Resource Locator 统一资源定位符

    比如这个就是一个 URL
    http://localhost:8080/ABC/DEF
    其中
    协议是:http 主机是:localhost 端口是 8080
    其他的都是 URI(在这里就是/ABC/DEF)

编写一个配置文件 properties

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC
jdbc.username=root
jdbc.password=123456

修改一下全局配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

<properties resource="dbconfig.properties"></properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
<!--<databaseIdProvider type=""></databaseIdProvider>-->
<mappers>
<mapper resource="com/hpg/mybatis/Mapper/EmployeeMapper.xml"/>
</mappers>
</configuration>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void InterfaceTest() throws IOException {
String resource = "mybatis-config.xml";

InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

//获取sqlSession实例 能够执行已经映射的sql语句
SqlSession sqlSession = sqlSessionFactory.openSession();

try {
//获取接口的实现类对象
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = mapper.getEmpById(1);
System.out.println(employee);
}finally {
sqlSession.close();
}

}

image-20210306154118099

这一部分之后与 Spring 整合的时候会进一步优化,由 Spring 进行托管

Settings 标签

image-20210306154654358

以最后一个为例:数据库一个属性叫 A_xxx,但你 sql 语句写了一个 select xxx,如果不设置驼峰参数的话,就读不了,设置了后就能自动进行映射,用于读取属性;

TypeAliases 标签

  • 别名处理器,能为 java 类型起别名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<typeAliases>
<!-- 1、typeAlias:为某个java类型起别名
type:指定要起别名的类型全类名;默认别名:就是类名小写;employee
alias:指定新的别名
-->
<typeAlias type="com.hpg.mybatis.pojo.Employee" alias="emp"/>

<!-- 2、package:为某个包下的所有类批量起别名
name:指定包名(为当前包以及下面所有的后代包的每一个类都起一个默认别名:(类名小写),)
-->
<package name="com.hpg.mybatis.pojo"/>

<!-- 3、批量起别名的情况下,可能会造成类名重复从而重复,因此可以使用@Alias注解为某个类型指定新的别名 -->
</typeAliases>

但一般还是写全路径名,方便后期排查;0

typeHandlers 标签

类型处理器:使 java 对象的属性映射成数据库对应的变量属性(比如:java String → 数据库 varchar)

自定义类型处理器:

image-20210306155957578

plugins 标签

image-20210306160103908

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed):执行器,作用是在 sql 执行增删改之前改变一些 mybatis 行为,达到自定义效果
  • ParameterHandler (getParameterObject, setParameters):参数处理器,预编译的时候设置以及获取参数
  • ResultSetHandler (handleResultSets, handleOutputParameters):结果集处理器,作用就是把得到的结果形成一个结果集并且封装成javabean 对象
  • StatementHandler (prepare, parameterize, batch, update, query):SQL 语句处理器

environments 标签

一个 environment 标签内需要有 transactionManager 和 dataSource 标签

环境标签的 id:代表当前环境的唯一标识

比如一个环境用来测试,而另一个环境专门用来开发;

  • 事务管理器 transactionManager

    • type:表示事务管理器的事务类型(JDBC/MANAGE)

      • JDBC(JdbcTransactionFactory)
        <!--code16-->
    • 自定义事务管理器:实现 TransactionFactory 接口.type 指定为全类名

  • 数据源 dataSource

    • type:数据源类型

      • UNPOOLED(UnpooledDataSourceFactory)
        <!--code17-->
      • JNDI(JndiDataSourceFactory)
        <!--code18-->
        

例子:

在全局配置文件中加入

1
2
3
4
5
6
7
8
9
10
11
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value= "db2"/>
<property name="Oracle Server" value="oracle"/>
<property name="MySQL" value= "mysql"/>
<property name="PostgreSQL" value="postgresql"/>
<property name="Derby" value= "derby"/>
<property name="H2" value= "h2"/>
<property name="HSQL" value= "hsqldb"/>

</databaseIdProvider>

对应的 sql 标签中添加属性 databaseId:

1
2
3
4
5
6
7
8
<update id="updateByMap" databaseId="mysql">
update sys_user
set
<foreach collection="_parameter" item="val" index="key" separator=",">
${key} = #{val}
</foreach>
where id = #{id}
</update>

这样就可以做到 从不同数据库中查了;

mappers 标签

每一个 mappers 标签的作用:将 sql 映射注册到了全局配置中

  • resource:引用类路径下的 sql 映射文件

  • url:引用网络路径 或者 磁盘路径下的 sql 映射文件(file://xxxxx/xxx/xx.xml)

  • class:引用接口

    如果要使用这个进行映射的话,要满足

    • 若有 sql 映射文件,则要和接口同名,且同一目录(包)下

      ![image-20210306164452142](

      https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210306164452142.png)

      1
      2
      <mapper resource = "xxx/xxx/abc.xml"/>
      <mapper class = "xxx.xxx.abc"/>
    • 没有 sql 映射文件,根据注解进行配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      package com.hpg.mybatis.dao;

      import com.hpg.mybatis.pojo.Employee;
      import org.apache.ibatis.annotations.Select;

      public interface EmployeeMapperAnnotation {

      @Select("select * from tbl_employee where id =#{id}")
      public Employee getEmpById(Integer id);
      }

      在全局配置文件中配置:
      1
      <mapper class="com.hpg.mybatis.dao.EmployeeMapperAnnotation"/>
      编写测试类:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      @Test
      public void AnnotationTest() throws IOException {
      String resource = "mybatis-config.xml";

      InputStream inputStream = Resources.getResourceAsStream(resource);
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

      //获取sqlSession实例 能够执行已经映射的sql语句
      SqlSession openSession = sqlSessionFactory.openSession();
      try{
      EmployeeMapperAnnotation mapper = openSession.getMapper(EmployeeMapperAnnotation.class);
      Employee empById = mapper.getEmpById(1);
      System.out.println(empById);
      }finally{
      openSession.close();
      }
      }
      结果: ![image-20210306164353447](

      https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210306164353447.png)

Mybatis 映射文件

image-20210306164710236

CRUD

目录结构

image-20210306173836777

别忘了需要提交openSession.commit()

当然了,如果设置了sqlSessionFactory.openSession(true)就可以进行自动提交啦

insert

1
2
3
4
5
6
<!--添加方法-->
<insert id="addEmp" parameterType="com.hpg.pojo.Employee">
insert into tbl_employee(last_name,email,gender)
values(#{lastName},#{email},#{gender})
</insert>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void CRUD_Test() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//获取sqlSession实例 能够执行已经映射的sql语句
SqlSession sqlSession = sqlSessionFactory.openSession();

try{
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
mapper.addEmp(new Employee(2, "jerry", "123@qq.com", "1"));
sqlSession.commit();

}finally {
sqlSession.close();
}

}

测试结果:

  • 获取自增主键的值:
    mysql 支持自增主键,自增主键值的获取,mybatis 也是利用 statement.getGenreatedKeys();
    useGeneratedKeys=”true”;使用自增主键获取主键值策略
    keyProperty;指定对应的主键属性,也就是 mybatis 获取到主键值以后,将这个值封装给 javaBean 的哪个属性
1
2
3
4
5
<insert id="addEmp" parameterType="com.atguigu.mybatis.bean.Employee"
useGeneratedKeys="true" keyProperty="id" databaseId="mysql">
insert into tbl_employee(last_name,email,gender)
values(#{lastName},#{email},#{gender})
</insert>

update

把一号员工也改成二号员工数据:

1
2
3
4
5
6
<!-- public void updateEmp(Employee employee);  -->
<update id="updateEmp">
update tbl_employee
set last_name=#{lastName},email=#{email},gender=#{gender}
where id=#{id}
</update>
1
mapper.updateEmp(new Employee(1, "jerry", "123@qq.com", "1"));

测试结果:

image-20210306174416906

delete

把二号员工删掉

1
2
3
4
<!-- public void deleteEmpById(Integer id); -->
<delete id="deleteEmpById">
delete from tbl_employee where id=#{id}
</delete>
1
mapper.deleteEmpById(2);

image-20210306205210832

select

image-20210307171355026

Select 获取 List

1
2
3
4
5
<!-- public List<Employee> getEmpsByLastNameLike(String lastName); -->
<!--resultType:如果返回的是一个集合,要写集合中元素的类型 -->
<select id="getEmpsByLastNameLike" resultType="com.hpg.pojo.Employee">
select * from tbl_employee where last_name like #{lastName}
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void Select_Test() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
List<Employee> list = mapper.getEmpsByLastNameLike("%e%");
for(Employee employee : list) {
System.out.println(employee.toString());
}
}finally {
openSession.close();
}
}

image-20210307171340880

(为啥 lastName 是 null 呢?好像是因为 Mybatis 找不到的原因 这个问题可以在下一节解决)

image-20210307171230053

Select 获取 Map

1
2
3
4
5
    //多条记录封装一个map:Map<String,Employee>:键是这条记录的主键,值是记录封装后的javaBean
//@MapKey:告诉mybatis封装这个map的时候使用哪个属性作为map的key
//要注意Map的键值对类型,不要写错
@MapKey("lastName")
public Map<String, Employee> getEmpByLastNameLikeReturnMap(String lastName);
1
2
3
4
5
<!--public Map<Integer, Employee> getEmpByLastNameLikeReturnMap(String lastName);  -->
<select id="getEmpByLastNameLikeReturnMap" resultType="com.hpg.pojo.Employee">
select * from tbl_employee where last_name like #{lastName}
</select>

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void Select_Map_Test() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Map<String, Employee> map = mapper.getEmpByLastNameLikeReturnMap("%e%");
System.out.println(map);

}finally {
openSession.close();
}
}

image-20210307172722264

image-20210307172729162

自定义 resultMap(重点)

image-20210307185043006

还记得不记得上面有个问题,取到的值为 null 吗?这个问题可以用ResultMap进行解决

  • 自定义某个 javaBean 的封装规则:
  1. type:自定义规则的 Java 类型
  2. id:取的名字,方便引用
  • 指定主键类的封装规则
  1. column:指定哪一列
  2. property:指定对应的 javaBean 属性

要使用的时候,只需要把原来的resultType替换成resultMap并且 id 填上我们自己取得 ResultMap 的 id 即可;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    <!--自定义某个javaBean的封装规则
type:自定义规则的Java类型
id:唯一id方便引用
-->
<resultMap type="com.hpg.pojo.Employee" id="MySimpleEmp">
<!--指定主键列的封装规则:
column:指定哪一列
property:指定对应的javaBean属性-->

<!-- id定义主键会底层有优化(Mybatis就知道这是一个主键了);result定义主键就不会有优化-->
<id column="id" property="id"/>
<!-- result列 定义普通列封装规则 -->
<result column="last_name" property="lastName"/>
<!-- 其他不指定的列会自动封装;但我们只要写resultMap就把全部的映射规则都写上。 -->
<result column="email" property="email"/>
<result column="gender" property="gender"/>
</resultMap>

<!-- resultMap:自定义结果集映射规则; -->
<!-- public Employee getEmpById(Integer id); -->
<select id="getEmpById" resultMap="MySimpleEmp">
select * from tbl_employee where id=#{id}
</select>

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void OwnMapTest() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapperPlus mapper = openSession.getMapper(EmployeeMapperPlus.class);
Employee empID = mapper.getEmpById(1);
System.out.println(empID);
} finally {
openSession.close();
}
}

image-20210307193615796

联表查询

现在员工有一个属性叫部门 Department,现在希望查询员工的时候把这个信息也显示出来

(其实就是结合 Mysql 的外键约束进行联表查询)

在 Employee 类中添加

1
2
3
4
5
6
7
8
9
private Department dept;

public Department getDept() {
return dept;
}

public void setDept(Department dept) {
this.dept = dept;
}

设置一个 Department 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.hpg.pojo;

import java.util.List;

public class Department {
private Integer id;
private String departmentName;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getDepartmentName() {
return departmentName;
}

public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}

@Override
public String toString() {
return "Department{" +
"id=" + id +
", departmentName='" + departmentName + '\'' +
'}';
}
}

进行联合查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  <!--
联合查询:级联属性封装结果集
-->
<resultMap type="com.hpg.pojo.Employee" id="MyDifEmp">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="did" property="dept.id"/>
<result column="dept_name" property="dept.departmentName"/>
</resultMap>

<!-- public Employee getEmpAndDept(Integer id);-->
<select id="getEmpAndDept" resultMap="MyDifEmp">
SELECT e.id id,e.last_name last_name,e.gender gender,e.d_id d_id,e.email email,
d.id did,d.dept_name dept_name FROM tbl_employee e,tbl_dept d
WHERE e.d_id=d.id AND e.id=#{id}
</select>

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void Join_Select_Test() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapperPlus mapper = openSession.getMapper(EmployeeMapperPlus.class);
Employee emp = mapper.getEmpAndDept(1);
System.out.println(emp);
System.out.println(emp.getDept().toString());
} finally {
openSession.close();
}
}

image-20210307201536083

association 标签方法进行联合查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
   <!--  public Employee getEmpAndDept(Integer id);-->
<select id="getEmpAndDept" resultMap="MyDifEmp2">
SELECT e.id id,e.last_name last_name,e.gender gender,e.d_id d_id,e.email email,
d.id did,d.dept_name dept_name FROM tbl_employee e,tbl_dept d
WHERE e.d_id=d.id AND e.id=#{id}
</select>

<!--
使用association定义关联的单个对象的封装规则;
-->
<resultMap type="com.hpg.pojo.Employee" id="MyDifEmp2">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<!-- association可以指定联合的javaBean对象
property="dept":指定哪个属性是联合的对象 这个是原类中取得id
javaType:指定这个属性对象的类型[不能省略]
-->
<association property="dept" javaType="com.hpg.pojo.Department">
<!--这里的property属性虽然也是id但是是Department中的id 不会与上面的冲突-->
<!--然而这里的列名column就不能取id了 就会与上面Employee的列名id重复了-->
<id column="did" property="id"/>
<result column="dept_name" property="departmentName"/>
</association>
</resultMap>

还是之前的测试代码,结果:

image-20210307203659476

association 实现分步查询
  • association 定义关联对象的封装语法
    • property:关联的对象,填入属性名(id)
    • select:表明使用哪个方法,填入对应的方法
    • column:指定将数据库中哪一列的值传入这个方法

运行的流程:使用select指定的方法 传入 column 指定的这列参数的值,查出来对象,并封装给 property 指定的属性

测试:

创建一个 department 接口 带有方法:

1
2
3
4
5
6
7
8
9
package com.hpg.dao;

import com.hpg.pojo.Department;

public interface DepartmentMapper {

public Department getDeptById(Integer id);

}

配置好 Department 的 mapper 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hpg.dao.DepartmentMapper">

<!--public Department getDeptById(Integer id); -->
<select id="getDeptById" resultType="com.hpg.pojo.Department">
select id,dept_name departmentName from tbl_dept where id=#{id}
</select>


</mapper>

在 EmployeeMapperPlus.xml 中进行分步查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
   <!-- 使用association进行分步查询:
1、先按照员工id查询员工信息
2、根据查询员工信息中的d_id值去部门表查出部门信息
3、部门设置到员工中;
-->

<!-- id last_name email gender d_id -->
<resultMap type="com.hpg.pojo.Employee" id="MyEmpByStep">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<!-- association定义关联对象的封装规则
select:表明当前属性是调用select指定的方法查出的结果
column:指定将哪一列的值传给这个方法

流程:使用select指定的方法(传入column指定的这列参数的值)查出对象,并封装给property指定的属性
-->
<association property="dept"
select="com.hpg.dao.DepartmentMapper.getDeptById"
column="d_id">
</association>
</resultMap>


<!-- public Employee getEmpByIdStep(Integer id);-->
<select id="getEmpByIdStep" resultMap="MyEmpByStep">
select * from tbl_employee where id=#{id}
</select>

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void Step_Association_Test() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapperPlus mapper = openSession.getMapper(EmployeeMapperPlus.class);
Employee emp = mapper.getEmpByIdStep(1);
System.out.println(emp);
System.out.println(emp.getDept().toString());
} finally {
openSession.close();
}
}

image-20210307213722020

懒加载/延迟查询

懒加载针对级联使用的,懒加载的目的是减少内存的浪费和减轻系统负担。

懒加载是一种按需加载,也就是只用调用到关联的数据的时候,才会与数据库进行交互(执行相应 sql)

以上述代码为例,我们知道 Employee 中有一个 Department 属性;

但我有时候不需要特定的部门的员工,只需要干净的员工信息即可,这就可以使用懒加载来只加载 Employee 表的信息,而不加载 Department 表的信息了

使用方法:

在全局配置文件中:

这是用于打印 sql 的设置,添加在全局配置中

1
<setting name="logImpl" value="STDOUT_LOGGING" />

同时需要引入依赖:

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.6</version>
</dependency>

开启方式:

1
2
3
4
5
6
<settings>
<!--延迟加载/懒加载-->
<!--显示的指定每个我们需要更改的配置的值,即使他是默认的。防止版本更新带来的问题 -->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
    @Test
public void LazyLoadTest() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapperPlus mapper = openSession.getMapper(EmployeeMapperPlus.class);
Employee emp = mapper.getEmpByIdStep(1);
System.out.println(emp.getLastName());
// System.out.println(emp.getDept().toString());
} finally {
openSession.close();
}
}

在关闭配置的时候,我们只需要进行查询员工的 lastName,但依旧执行了多余的 sql:

image-20210308201325432

开启之后:

image-20210308201338105


collection 标签实现关联集合查询

首先需要明白 collection 和 association 标签区别:

他们都能进行多表的查询,但是 association 是【关联】,collection 是【集合】

什么意思呢?

  • association 用于一对一多对一 一个人对应一个部门,但部门可以有多人,希望查一个人顺带部门的时候,就用association
  • collection 是用于一对多,一个部门可容纳多人,希望查一个部门的时候,顺带把部门中的人查出来就用 collection

修改一下数据库:

image-20210308203757038

DepartmentMapper 接口中新增方法

1
public Department getDeptByIdPlus(Integer id);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
        <!--
public class Department {
private Integer id;
private String departmentName;
private List<Employee> emps;
did dept_name || eid last_name email gender
-->

<!--嵌套结果集的方式,使用collection标签定义关联的集合类型的属性封装规则 -->
<resultMap type="com.hpg.pojo.Department" id="MyDept">
<id column="did" property="id"/>
<result column="dept_name" property="departmentName"/>
<!--
collection:定义【关联集合类型】的属性的封装规则
ofType:指定集合里面元素的类型
-->
<collection property="emps" ofType="com.hpg.pojo.Employee">
<!-- 定义这个集合中元素的封装规则 -->
<id column="eid" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
</collection>
</resultMap>
<!-- public Department getDeptByIdPlus(Integer id); -->
<select id="getDeptByIdPlus" resultMap="MyDept">
SELECT d.id did,d.dept_name dept_name,
e.id eid,e.last_name last_name,e.email email,e.gender gender
FROM tbl_dept d
LEFT JOIN tbl_employee e
ON d.id=e.d_id
WHERE d.id=#{id}
</select>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void collection_Test() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
DepartmentMapper mapper = openSession.getMapper(DepartmentMapper.class);
Department department = mapper.getDeptByIdPlus(1);
System.out.println(department);
System.out.println(department.getEmps());

} finally {
openSession.close();
}
}

结果

image-20210308203937320

collection 实现分步查询
1
public Department getDeptByIdStep(Integer id);
1
public Employee getEmpByIdStep(Integer id);

EmployeeMapperPlus.xml

首先查询出来特定部门的员工数据

1
2
3
4
<!-- public List<Employee> getEmpsByDeptId(Integer deptId); -->
<select id="getEmpsByDeptId" resultType="com.hpg.pojo.Employee">
select * from tbl_employee where d_id=#{deptId}
</select>
1
2
3
4
5
6
7
8
9
10
11
12
<!-- collection:分段查询 -->
<resultMap type="com.hpg.pojo.Department" id="MyDeptStep">
<id column="id" property="id"/>
<id column="dept_name" property="departmentName"/>
<collection property="emps"
select="com.hpg.dao.EmployeeMapperPlus.getEmpsByDeptId"
column="id"></collection>
</resultMap>
<!-- public Department getDeptByIdStep(Integer id); -->
<select id="getDeptByIdStep" resultMap="MyDeptStep">
select id,dept_name from tbl_dept where id=#{id}
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void collection_Step_Test() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
DepartmentMapper mapper = openSession.getMapper(DepartmentMapper.class);
Department dept = mapper.getDeptByIdStep(1);
System.out.println(dept);
System.out.println(dept.getEmps());

} finally {
openSession.close();
}
}

image-20210308205257235

这里的 Employee 查询查询部分 没有配置 resultMap 因此 lastName 是 null;

collection 标签 extra
  • 多列的值传递过去:
    将多列的值封装 map 传递;使用:column=”{key1=column1,key2=column2}”

    比如在上一个例子中:

    1
    2
    3
    <collection property="emps"
    select="com.hpg.dao.EmployeeMapperPlus.getEmpsByDeptId"
    column="id"></collection>

    处的 id 也可以写成:{deptId = id}其中 deptId 的由来是 EmployeeMaaperPlus 中的 sql 语句:

    1
    2
    3
    4
    <!-- public List<Employee> getEmpsByDeptId(Integer deptId); -->
    <select id="getEmpsByDeptId" resultType="com.hpg.pojo.Employee">
    select * from tbl_employee where d_id=#{deptId}
    </select>
  • 延迟加载

    fetchType=”lazy”:表示使用延迟加载;

    • lazy:延迟
    • er:立即

discriminator 鉴别器

Mybatis可以使用discriminator判断某一列的值,然后根据某一列的值改变封装行为

现在模拟一个场景:

  • 如果查出的是女生:就把部门信息查询出来,否则不查询;
  • 如果是男生,把 last_name 这一列的值赋值给 email;

修改数据库:gender 0 代表女生, 1 代表男生

image-20210308212312476

1
<discriminator javaType="" column=""></discriminator>

javaType 为鉴别的列值对应的属性 , column 为列名,case 代表不同的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    <resultMap type="com.hpg.pojo.Employee" id="MyEmpDis">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<!--
column:指定判定的列名
javaType:列值对应的java类型 -->
<discriminator javaType="string" column="gender">
<!--女生 resultType:指定封装的结果类型;不能缺少。/resultMap-->
<case value="0" resultType="com.hpg.pojo.Employee">
<association property="dept"
select="com.hpg.dao.DepartmentMapper.getDeptById"
column="d_id">
</association>
</case>
<!--男生 ;如果是男生,把last_name这一列的值赋值给email; -->
<case value="1" resultType="com.hpg.pojo.Employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="last_name" property="email"/>
<result column="gender" property="gender"/>
</case>
</discriminator>
</resultMap>


<!-- public Employee getEmpByIdStep(Integer id);-->
<select id="getEmpByIdStep" resultMap="MyEmpDis">
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void Dis_Test() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapperPlus mapper = openSession.getMapper(EmployeeMapperPlus.class);
Employee emp = mapper.getEmpByIdStep(1);
System.out.println(emp);
System.out.println(emp.getDept());

} finally {
openSession.close();
}
}

测试:

男生情况(gender=1):email=lastName,且不显示部门信息

image-20210308212556124

女生情况(gender=0):email 不变,显示部门信息

image-20210308212629729

Mybatis 参数处理

  • 单个参数:mybatis 不会做特殊处理,参数名可以随便写(当作占位符)
    #{参数名/任意名}:取出参数值。
  • 多个参数:mybatis 会做特殊处理,多个参数会被封装成 一个map
    key:param1…paramN,或者参数的索引也可以
    value:表示传入的参数值
    #{}就是从 map 中获取指定的 key 的值;

例子:

假如接口有一个方法:

1
public Employee getEmpByIdAndLastName(Integer id,String lastName);

并编写对应的 sql 映射:

1
2
3
4
<!--  public Employee getEmpByIdAndLastName(Integer id,String lastName);-->
<select id="getEmpByIdAndLastName" resultType="com.hpg.pojo.Employee">
select * from tbl_employee where id = #{id} and last_name=#{lastName}
</select>

测试

1
2
Employee emp = mapper.getEmpByIdAndLastName(1, "jerry");
System.out.println(emp);

执行:

1
2
3
4
5
6
7
异常:
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
操作:
方法:public Employee getEmpByIdAndLastName(Integer id,String lastName);
取值:#{id},#{lastName}

修改:

1
2
3
4
<!--  public Employee getEmpByIdAndLastName(Integer id,String lastName);-->
<select id="getEmpByIdAndLastName" resultType="com.hpg.pojo.Employee">
select * from tbl_employee where id = #{param1} and last_name=#{param2}
</select>

结果:

image-20210306211827961

然而如果参数过多的话,占位符都写 param1,param2 会显得很臃肿,因此有了*【命名参数】 *

  • 【命名参数】

    形如:

    1
    public Employee getEmpByIdAndLastName(@Param("id")Integer id,@Param("lastName")String lastName);
    • 作用:明确指定封装参数时 map 的 key;

    • 形式:@Param(“id”)
      多个参数会被封装成 一个 map,
      key:使用@Param 注解指定的值
      value:参数值
      #{指定的 key}取出对应的参数值

      如果用这种形式的话,我们的 xml 文件就可以写成:

      1
      2
      3
      4
      <!--  public Employee getEmpByIdAndLastName(Integer id,String lastName);-->
      <select id="getEmpByIdAndLastName" resultType="com.hpg.pojo.Employee">
      select * from tbl_employee where id = #{id} and last_name=#{lastName}
      </select>

    再测试:

    image-20210306213238180

  • POJO:
    如果多个参数正好是我们业务逻辑的数据模型,我们就可以直接传入 pojo;
    #{属性名}:取出传入的 pojo 的属性值

  • Map:
    如果多个参数不是业务模型中的数据,没有对应的 pojo,不经常使用,为了方便,我们也可以传入 map
    #{key}:取出 map 中对应的值

    1
    2
    3
    4
    <!--public Employee getEmpByMap(Map<String, Object> map);-->
    <select id="getEmpByMap" resultType="com.hpg.pojo.Employee">
    select * from tbl_employee where id=#{id} and last_name=#{lastName}
    </select>
    1
    2
    3
    4
    5
    6
    Map<String, Object> map = new HashMap<>();
    map.put("id", 1);
    map.put("lastName", "jerry");
    Employee emp = mapper.getEmpByMap(map);
    sqlSession.commit();
    System.out.println(emp);

    image-20210306214655409

参数处理总结

public Employee getEmp(@Param("id")Integer id,String lastName);

  • id → #{id/param1}

  • lastName → #{param2}

public Employee getEmp(Integer id,@Param("e")Employee emp);

  • id → #{param1}

  • lastName → #{param2.lastName / e.lastName}

    因为 param2 才代表 employee

##特别注意:

如果是 Collection(List、Set)类型或者是数组,也会特殊处理,是把传入的 list 或者数组封装在 map 中。

  • 此时的 key:
    • Collection(collection),
    • 如果是 List 还 key(list)
    • 数组(array)
    • public Employee getEmpById(List<Integer> ids);为例
      现在取值:取出第一个 id 的值: #{list[0]},就不能用#{param[0]}或者是#{ids[0]}

参数传递源码分析

我们知道 mapper 是以动态代理为原理执行 sql 的,下面看看整个运行流程

测试:

1
2
3
SqlSession openSession = sqlSessionFactory.openSession();
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee employee = mapper.getEmpByIdAndLastName(1, "Tom");

第三行处进行断点调试,先来到一个名字叫 mapperProxy.class 动态代理的 InvocationHandler

1
public class MapperProxy<T> implements InvocationHandler, Serializable {}

看看这个类中的 invoke 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//有一个判断,当前方法声明的类是在object里面声明的, 直接放行.
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//否则,把method包装成一mapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

我们发现这里的mapperMethod.execute(sqlSession, args);方法中的参数传入的是 1 和 Tom

image-20210306221301585

进入 execute 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//执行之前,先判断是什么类型的,对应的走增删改查的方法
//每次调用之前,resulte就是返回值,通过convertArgsToSqlCommandParam(args)会把你传过来的参数转化为sql能用的参数
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {//没有返回值
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {//多个
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {//map
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {//游标
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);//返回单个对象
//底层调用的还是sqlSession
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}

在 convertArgsToSqlCommandParam(args)方法中 参数确实是传过来了

image-20210306221955848

在这个方法中:

1
2
3
public Object convertArgsToSqlCommandParam(Object[] args) {
return paramNameResolver.getNamedParams(args);
}

它调用的又是

1
paramNameResolver.getNamedParams(args);

再 step in 进入这个方法,下面这个方法将我们传入的参数封装成了 param1,和 param2

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* <p>
* A single non-special parameter is returned without a name.<br />
* Multiple parameters are named using the naming rule.<br />
* In addition to the default names, this method also adds the generic names (param1, param2,
* ...).
* </p>
*/
public Object getNamedParams(Object[] args) {
//上手先获取了一个names.size(),而这个names里面是有值的:{0=id,1=lastName}, key就是0,1 value就是id和lastName. 这里我们就能看出来是调用的哪个mapper接口, 见下图. 那name是如何确定的,我们见图下面的分析
final int paramCount = names.size();
...........
}

这行代码的第一段执行了一个final int ParamCount = names.size(); 也就是先把参数的数量确认为了 names 的大小。

那么这个names又是什么呢?是ParamNameResolver类下的一个属性:private final SortedMap<Integer, String> names;也就是说,本质是一个 map;

names值的确定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public ParamNameResolver(Configuration config, Method method) {
//先拿到所有的参数
final Class<?>[] paramTypes = method.getParameterTypes();
//以及参数的注解
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations 开始标注参数的索引
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {// 判断一下:如果当前参数的注解是param
hasParamAnnotation = true;//标记:拥有了注解值
name = ((Param) annotation).value();//拿到param注解的value值
break;
}
}
if (name == null) {
// @Param was not specified.
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);//Jdk1.8
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());//没标注解,name就是map的size
}
}
map.put(paramIndex, name);//map每确定一个参数,就会增大一下
}
//最终确定了names的值
names = Collections.unmodifiableSortedMap(map);
}

总的来说 names 的确定流程:

  1. 获取每个标注了@Param 注解的参数的值,并且把这些值赋值给 name(一个字符串)
  2. 接着每次解析一个参数,形成一个{key(参数索引),value(name 的值}放到 map 中
    1. 如果加了@Param 注解,name 的值就是注解值
    2. 没有标注@Param 注解的话
      1. 如果配置了 useActualParamName(jdk1.8) 那么 name=参数名
      2. 否则,name=map.size() 相当于元素索引

举个例子 来理解:对于public Employee getEmpByIdAndLastName(@Param("id")Integer id, @Param("lastName")String lastName);来说

由于 id,lastName 这两个属性前加了注解@Param,因此这俩的 name 值分别为:id 和 lastName,因此确定完 names 应该是:{0 : id, 1 : lastName}

假如再有另一个属性没加@Param 注解,那最终的 names 应该是{0 : id, 1 : lastName, 2 : 2} (key=2,value=2)


下面回到getNamedParams方法继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Object getNamedParams(Object[] args) {
final int ParamCount = names.size();
//如果参数为空直接返回
if (args == null || ParamCount == 0) {
return null;
//如果只有一个元素并且是没有@Param注解,就直接调用arg[0],也就是单元素直接返回
} else if (!hasParamAnnotation && ParamCount == 1) {
return args[names.firstkey()];
//多个元素或者有@Param标注
} else {
final Map<String, Object> Param = new ParamMap<Object>();
int i = 0;
//遍历names集合(该集合的确定在上文讲述过了):names的values作为key;names集合的key作为取值的参考arg[0]:args[1,"Tom"]
//比如:{id=args[0]:1,lastName=args[1]:Tom}
for (Map.Entry<Integer, String> entry : names.entrySet()) {
Param.put(entry.getValue(), args[entry.getkey()]);
//额外的将每一个参数也保存在map中,使用心得key:Param1...ParamN
//因此,有Param注解也已使用#{指定的key}或者#{Param1}
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);

if (!names.containsValue(genericParamName)) {
Param.put(genericParamName, args[entry.getkey()]);
}
i++;
}
return Param;
}
}

关于 names 集合遍历理解:

  • 首先我们有了一个 names 集合:{0:id, 1:lastName}和一个 args(之前传入的参数列表):【1,“Tom”】
  • 接着新建了一个 map 我们称为 Param,这个新 map 的 key 我们使用 names 集合中的 value,而 value 我们得参数列表的值;举个例子就是,现在 map 应该为:{id:args[0] = 1, lastName : args[1] = “Tom”}、
  • 同时啊,我们还贴心的将 Param1,Param2….ParamN 也做为了 key 与参数列表中的各个参数进行了映射封装进了 map 中,这样子以后不仅可以通过#{key}去获取值了,#{Param}也能取到值

参数值的获取(#{}与${})

主要探究#{}${}的区别:

例子:

1
select * from tbl_employee where id=${id} and last_name=#{lastName}

控制台:Preparing: select * from tbl_employee where id=2 and last_name=?

我们发现使用了$的 id 能打印出来,而使用了#的属性就不能获取到;

  • #{}:是以预编译的形式,将参数设置到 sql 语句中;PreparedStatement;防止 sql 注入

  • ${}:取出的值直接拼装在 sql 语句中;会有安全问题;

    但我们仍有一些场景是能够去使用的:比如原生 jdbc 不支持占位符的地方我们就可以使用${}进行取值

    按照年份分表拆分:select _ from ${year}
    排序:select _ from tbl_employee order by ${f_name} ${order}

#{}取参数的一些规则

  • 参数位置支持的属性:

    javaType、 jdbcType、 mode(存储过程)、 numericScale、
    resultMap、 typeHandler、 jdbcTypeName、 expression(未来准备支持的功能);
  • jdbcType 通常需要在某种特定的条件下被设置:

在我们数据为 null 的时候,有些数据库可能不能识别 mybatis 对 null 的默认处理,比如 Oracle(报错):JdbcType OTHER:无效的类型;因为 mybatis 对所有的 null 都映射的是原生 Jdbc 的 OTHER 类型,oracle 不能正确处理;
由于全局配置中:jdbcTypeForNull=OTHER;oracle 不支持;

两种办法进行解决:

  1. #{email,jdbcType=OTHER};
  2. jdbcTypeForNull=NULL

动态 SQL

if

1
2
//携带了哪个字段查询条件就带上这个字段的值
public List<Employee> getEmpsByConditionIf(Employee employee);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    <!--
• if:判断
• choose (when, otherwise):分支选择;带了break的swtich-case
如果带了id就用id查,如果带了lastName就用lastName查;只会进入其中一个
• trim 字符串截取(where(封装查询条件), set(封装修改条件))
• foreach 遍历集合
-->
<!-- 查询员工,要求,携带了哪个字段查询条件就带上这个字段的值 -->
<!-- public List<Employee> getEmpsByConditionIf(Employee employee); -->
<select id="getEmpsByConditionIf" resultType="com.hpg.pojo.Employee">
select * from tbl_employee
where
<!-- test:判断表达式(OGNL)
OGNL参照PPT或者官方文档。
c:if test
从参数中取值进行判断

遇见特殊符号应该去写转义字符:(转义字符可以在w3school中查询 html标签中)
&&:&amp;&amp; 表示 and
-->
<if test="id!=null">
id=#{id}
</if>
/*&amp;&amp; = and*/
<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
and last_name like #{lastName}
</if>
<if test="email!=null and email.trim()!=&quot;&quot;">
and email=#{email}
</if>
<!-- ognl会进行字符串与数字的转换判断 "0"==0 -->
<if test="gender==0 or gender==1">
and gender=#{gender}
</if>

</select>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void DynamciIFTest() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapperDynamicSQL mapper = openSession.getMapper(EmployeeMapperDynamicSQL.class);
List<Employee> list = mapper.getEmpsByConditionIf(new Employee(1, "%e%", "123@qq.com", "1"));
System.out.println(list);

} finally {
openSession.close();
}
}

image-20210308222402866

image-20210308222419630

where

由于查询时候如果某些条件没带会出现SQL 拼接问题

因此有以下解决方案

  1. 在 where 后添加1=1,以后的 sql 条件语句使用and xxx

  2. 另一种方法是使用where 标签去将所有的查询条件包括在内

    也就是通过where 标签if 标签配合使用来取代 where 条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<select id="getEmpsByConditionIf" resultType="com.hpg.pojo.Employee">
select * from tbl_employee
<!-- where -->
<where>
<!-- test:判断表达式(OGNL)
OGNL参照PPT或者官方文档。
c:if test
从参数中取值进行判断

遇见特殊符号应该去写转义字符:(转义字符可以在w3school中查询 html标签中)
&&:&amp;&amp; 表示 and
-->
<if test="id!=null">
id=#{id}
</if>
/*&amp;&amp; = and*/
<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
and last_name like #{lastName}
</if>
<if test="email!=null and email.trim()!=&quot;&quot;">
and email=#{email}
</if>
<!-- ognl会进行字符串与数字的转换判断 "0"==0 -->
<if test="gender==0 or gender==1">
and gender=#{gender}
</if>
</where>
</select>

trim

  • trim 标签是一个格式化的标签,可以完成 set 或者是 where 标记的功能

image-20210309195726679

trim 标签的四个属性如上:

  1. prefix:前缀,该属性的值将会在拼接后的整个字符串作为前缀出现
  2. prefixOverrides:前缀覆盖,会去掉字符串前面多余的字符
  3. suffix:后缀,该属性的值将会在拼接后的整个字符串作为后缀出现
  4. suffixOverrides:后缀覆盖,会去掉字符串后面多余的字符(可以用来去除多余的【逗号,】)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
       <select id="getEmpsByConditionIf" resultType="com.hpg.pojo.Employee">
select * from tbl_employee
<!-- where -->
<where>
<if test="id!=null">
id=#{id}
</if>
/*&amp;&amp; = and*/
<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
and last_name like #{lastName}
</if>
<if test="email!=null and email.trim()!=&quot;&quot;">
and email=#{email}
</if>
<!-- ognl会进行字符串与数字的转换判断 "0"==0 -->
<if test="gender==0 or gender==1">
and gender=#{gender}
</if>
</where>
</select>

<!--对比-->

<select id="getEmpsByConditionTrim" resultType="com.hpg.pojo.Employee">

-->
<!-- 自定义字符串的截取规则 -->
<trim prefix="where" suffixOverrides="and">
<if test="id!=null">
id=#{id} and
</if>
<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
last_name like #{lastName} and
</if>
<if test="email!=null and email.trim()!=&quot;&quot;">
email=#{email} and
</if>
<!-- ognl会进行字符串与数字的转换判断 "0"==0 -->
<if test="gender==0 or gender==1">
gender=#{gender}
</if>
</trim>
</select>

choose

类似于 java 中的 switch case 进行分支选择

choose 标签中 的 case 呢 则用了when 标签,而 default 情况呢 则使用了otherwise 标签

下面模拟一个场景:

  • 假如我们想搜索的 id 不为空,那么执行的 sql 语句为:select * from tbl_employee WHERE id=?

    image-20210309202705115

  • 假如我们想搜索的 lastname 不为空,执行select * from tbl_employee WHERE last_name like ?

    image-20210309202637465

  • 假如我们想搜索的 email 不为空,则执行select * from tbl_employee WHERE email = ?

    image-20210309202749417

  • 都为空,则执行select * from tbl_employee WHERE gender = 0

    image-20210309202815040

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

<!-- public List<Employee> getEmpsByConditionChoose(Employee employee); -->
<select id="getEmpsByConditionChoose" resultType="com.hpg.pojo.Employee">
select * from tbl_employee
<where>
<!-- 如果带了id就用id查,如果带了lastName就用lastName查;只会进入其中一个 -->
<choose>
<when test="id!=null">
id=#{id}
</when>
<when test="lastName!=null">
last_name like #{lastName}
</when>
<when test="email!=null">
email = #{email}
</when>
<otherwise>
gender = 0
</otherwise>
</choose>
</where>
</select>

set

关于 set 这个标签,我们是在更新状态中使用,也就是实现动态更新的效果(满足某种条件去对应更新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!--set 动态更新-->
<!--public void updateEmp(Employee employee); -->
<update id="updateEmp">
<!-- Set标签的使用 -->
update tbl_employee
<set>
<if test="lastName!=null">
last_name=#{lastName},
</if>
<if test="email!=null">
email=#{email},
</if>
<if test="gender!=null">
gender=#{gender}
</if>
</set>
where id=#{id}
<!--
Trim:更新拼串
update tbl_employee
<trim prefix="set" suffixOverrides=",">
<if test="lastName!=null">
last_name=#{lastName},
</if>
<if test="email!=null">
email=#{email},
</if>
<if test="gender!=null">
gender=#{gender}
</if>
</trim>
where id=#{id} -->
</update>

foreach

属性 描述
collection 表示迭代集合的名称,可以使用@Param 注解指定,如下图所示 该参数为必选![image-20210309204419205](
https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210309204419205.png)
item 表示本次迭代获取的元素,若 collection 为 List、Set 或者数组,则表示其中的元素;若 collection 为 map,则代表 key-value 的 value,该参数为必选
open 表示该语句以什么开始,最常用的是左括弧’(’,注意:mybatis 会将该字符拼接到整体的 sql 语句之前,并且只拼接一次,该参数为可选项
close 表示该语句以什么结束,最常用的是右括弧’)’,注意:mybatis 会将该字符拼接到整体的 sql 语句之后,该参数为可选项
separator mybatis 会在每次迭代后给 sql 语句 append 上 separator 属性指定的字符,该参数为可选项
index 在 list、Set 和数组中,index 表示当前迭代的位置,在 map 中,index 代指是元素的 key,该参数是可选项。

foreach 查询

1
2
//查询员工id'在给定集合中的
public List<Employee> getEmpsByConditionForeach(@Param("ids")List<Integer> ids);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--public List<Employee> getEmpsByConditionForeach(List<Integer> ids);  -->
<select id="getEmpsByConditionForeach" resultType="com.hpg.pojo.Employee">
select * from tbl_employee
<!--
collection:指定要遍历的集合:
list类型的参数会特殊处理封装在map中,map的key就叫list
item:将当前遍历出的元素赋值给指定的变量
separator:每个元素之间的分隔符
open:遍历出所有结果拼接一个开始的字符
close:遍历出所有结果拼接一个结束的字符
index:索引。遍历list的时候是index就是索引,item就是当前值
遍历map的时候index表示的就是map的key,item就是map的值

#{变量名}就能取出变量的值也就是当前遍历出的元素
-->
<foreach collection="ids" item="item_id" separator=","
open="where id in(" close=")">
#{item_id}
</foreach>
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void DynamicForEachTest() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapperDynamicSQL mapper = openSession.getMapper(EmployeeMapperDynamicSQL.class);
List<Integer> list = Arrays.asList(1, 2, 3, 4);
List<Employee> TestList = mapper.getEmpsByConditionForeach(list);
for(Employee e : TestList) {
System.out.println(e);
}

} finally {
openSession.close();
}
}

数据库表中 id:1 2 5,测试结果为 2 条数据 正确

image-20210309205209258

image-20210309205142077

foreach 批量保存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  <!-- 批量保存 -->
<!--public void addEmps(@Param("emps")List<Employee> emps); -->
<!--MySQL下批量保存:可以foreach遍历 mysql支持values(),(),()语法-->
<insert id="addEmps">
insert into tbl_employee(last_name, email, gender, d_id)
values
<foreach collection="emps" item="emp" separator=",">
(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
</foreach>
</insert>

<!-- 这种方式需要数据库连接属性allowMultiQueries=true;
这种分号分隔多个sql可以用于其他的批量操作(删除,修改) -->
<!-- <insert id="addEmps">
<foreach collection="emps" item="emp" separator=";">
insert into tbl_employee(last_name,email,gender,d_id)
values(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
</foreach>
</insert> -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testBatchSave() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapperDynamicSQL mapper = openSession.getMapper(EmployeeMapperDynamicSQL.class);
List<Employee> emps = new ArrayList<>();
emps.add(new Employee(null, "smith0x1", "606@qq.com", "1",new Department(1)));
emps.add(new Employee(null, "allen0x1", "111@qq.com", "0",new Department(1)));
mapper.addEmps(emps);
openSession.commit();
}finally{
openSession.close();
}
}

image-20210309210642528

image-20210309210651516

两个内置参数

  • _parameter:表示真个参数
  • _databaseId:表示当前数据库的别名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- 两个内置参数:
不只是方法传递过来的参数可以被用来判断,取值。。。
mybatis默认还有两个内置参数:
_parameter:代表整个参数
单个参数:_parameter就是这个参数
多个参数:参数会被封装为一个map;_parameter就是代表这个map

_databaseId:如果配置了databaseIdProvider标签。
_databaseId就是代表当前数据库的别名oracle
-->

<!--public List<Employee> getEmpsTestInnerParameter(Employee employee); -->
<select id="getEmpsTestInnerParameter" resultType="com.hpg.pojo.Employee">
<if test="_databaseId=='mysql'">
select * from tbl_employee
<if test="_parameter!=null">
where last_name == #{lastName}
</if>
</if>
<if test="_databaseId=='oracle'">
select * from employees
<if test="_parameter!=null">
where last_name == #{_parameter.lastName}
</if>
</if>
</select>

bind

  • bind 标签中,value 对应传入实体类的某个字段,name 属性既给对应字段取的变量名。在 value 属性中可以使用字符串拼接等特殊处理。

以上述场景为例,如果要想使用 like 进行模糊查询:where last_name lile '%#{lastNmae}%' 这样写 是不可以的,需要将 lastName 绑定成一个变量传进去:<bind name="xxx" value="'%'+ id + '%'"/>

其中,不仅仅可以写成 % + id + % ,也可以写成_ + id + % ,看需求变化;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--public List<Employee> getEmpsTestInnerParameter(Employee employee);  -->
<select id="getEmpsTestInnerParameter" resultType="com.hpg.pojo.Employee">
<!-- bind:可以将OGNL表达式的值绑定到一个变量中,方便后来引用这个变量的值 -->
<bind name="_lastName" value="'%'+lastName+'%'"/>
<if test="_databaseId=='mysql'">
select * from tbl_employee
<if test="_parameter!=null">
where last_name like #{_lastName}
</if>
</if>
<if test="_databaseId=='oracle'">
select * from employees
<if test="_parameter!=null">
where last_name like #{_parameter.lastName}
</if>
</if>
</select>

sql 标签 - 抽取/重用

抽取可重用的 sql 片段。方便后面引用

  1. sql 抽取:经常将要查询的列名,或者插入用的列名抽取出来方便引用
  2. include 来引用已经抽取的 sql:
  3. include 还可以自定义一些 property,sql 标签内部就能使用自定义的属性
    1. nclude-property:取值的正确方式${prop},
    2. #{不能使用这种方式}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  <sql id="insertColumn">
<if test="_databaseId=='oracle'">
employee_id,last_name,email
</if>
<if test="_databaseId=='mysql'">
last_name,email,gender,d_id
</if>
</sql>


<insert id="addEmps">
insert into tbl_employee(
<include refid="insertColumn"></include>
)
values
<foreach collection="emps" item="emp" separator=",">
(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
</foreach>
</insert>

缓存(重点)

  • Mybatis 包含了一个时分强大的查询缓存特性,可以非常方便的配置和定制;缓存的作用呢,是可以极大的提升查询效率
  • Mybatis 系统默认定义了两级缓存(一级缓存和二级缓存)
    • 默认情况下,只有一级缓存(SqlSession 级别的缓存,AKA 本地缓存)开启
    • 二级缓存需要手动的去开启和配置,是基于namespace级别的缓存
    • 为了提高扩展石呢,MYbatis 定义了缓存接口 Cache,因此我们可以通过实现这个接口去自定义我们的二级缓存

image-20210310210359313

两级缓存

缓存分为:一级缓存和二级缓存

一级缓存

  • 一级缓存(本地缓存):与数据库同一次会话期间查询到的数据会放到本地缓存中,之后若需要获取相同数据,则可以直接从缓存去获取,没必要去查询数据库了

img

下面进行一个测试去体会一下:

对于同一个请求,我们执行两次,却发现,只执行了第一次的 sql;

第二次的数据是直接获取到的。是从哪里获取的呢?答案就是一级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testFirstLevelCache() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(1);
System.out.println(emp01);

Employee emp02 = mapper.getEmpById(1);
System.out.println(emp02);

System.out.println(emp01==emp02);

//openSession2.close();
}finally{
openSession.close();
}
}

image-20210309215628869

  • 那么我们不由得就会产生一个疑问了,什么场景下我们的一级缓存不再适用了呢?
  1. sqlSession 不同。

    1
    2
    3
    SqlSession openSession2 = sqlSessionFactory.openSession();
    EmployeeMapper mapper2 = openSession2.getMapper(EmployeeMapper.class);
    Employee empTest = mapper.getEmpById(1);
  2. 使用的 sqlSession 相同

    1. 但查询条件不同

      1
      Employee emp01 = mapper.getEmpById(2);
    2. 但两次查询之间执行了一次 CRU(增删改)操作(因为有可能会导致数据受到影响)

      1
      mapper.addEmp(new Employee(null, "testCache", "cache", "1"));
    3. 但手动清除过了一级缓存(缓存清空)

      1
      openSession.clearCache();
  • 一个一级缓存的生命周期有多长呢?
    • MyBatis 在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
    • 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
    • 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
    • SqlSession中执行了任何一个update 操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用

二级缓存

为什么要有二级缓存呢?肯定是一级缓存有一定的不足,需要一个补丁或者是进化版缓存去弥补啦;

  • 二级缓存(全局缓存):基于 namespace 级别的缓存,即一个 namespace 对应一个二级缓存

  • 工作机制

    • 一个会话,去查询一条数据,这个数据会被放在当前会话的一级缓存中
    • 会话关闭了,一级缓存的数据就会被保存到二级缓存中,这时候有一个新的会话去查询信息,就可以参照二级缓存去查了;
    • 一个 sqlSession 去开启两个 Mapper,两个 Mapper 又各自对应不同的对象,此时这两个对象是放在不同的二级缓存中的(因为一个 namespace 对应一个二级缓存)

img

二级缓存的使用

  • 开启方式:在全局配置文件中

    1
    2
    <!--显式的指定每个我们需要更改的配置的值,即使他是默认的。防止版本更新带来的问题  -->
    <setting name="cacheEnabled" value="true"/>
  • 使用方式:哪个 mapper 文件要使用,就在哪个 mapper 文件中添加

    1
    <cache></cache>
  • 关于cache标签的几个属性

    • eviction:缓存的回收策略

      • LRU - 最近最少使用的:移除最长时间不被使用的对象。
      • FIFO - 先进先出:按对象进入缓存的顺序来移除它们。
      • SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象。
      • WEAK - 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
    • flushIntercal:缓存刷新间隔。可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

    • readOnly:是否只读。属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。

      • true:只读;mybatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。

        mybatis 为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,速度快

      • false:非只读:mybatis 觉得获取的数据可能会被修改。

        mybatis 会利用序列化&反序列的技术克隆一份新的数据给你。安全,速度慢

    • size:引用数目,可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。

    • type:指定自定义的全类名,实现 Cache 接口即可;

  • 要想实现缓存,我们的 POJO 类(不是 Mapeer 类)就需要去实现序列化接口(Serializable)

    如果不实现的话:image-20210310204032410

下面进行下测试:

当不开启缓存的时候:发送了两次 sql

image-20210310204214966

当开启缓存的时候:只发送了一次 sql 同时可以发现一句话:Cache Hit Ratio 也就是缓存命中了 后面的 0.5 是什么意思呢?
代表着第二次查缓存命中了,即 1/2 = 0.5

1
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache>

image-20210310204140122

ps:需要注意的是,查出来的数据都会被默认先放在一级缓存中,只有当会话提交或者关闭之后,一级缓存中的数据才会转移到二级缓存

也就是说,假如上述的例子。第一个 sqlSession 是在建立第二个 sqlSession 之后才关闭的,就算开启了二级缓存,那么照样会发送 2 条 sql

缓存相关设置

  • cacheEnabled=true:false:关闭缓存(二级缓存关闭)(一级缓存一直可用的)

  • 在 mapper 配置中,每个select 标签都有一个useCache = "true".那么假如设置为 false 是设置一级缓存还是二级缓存为 false 呢?

    答案是:一级缓存还能正常使用,二级缓存被禁止了

  • 每个增删改标签中有 flushCache="true" (刷新缓存):表示每次增删改执行之后 就会清除缓存(一级二级缓存都会被清除)

    每个查询标签中flushCache="false"假如设置为 true,则每次查询之前都会清除缓存

  • sqlSession.clearCache()只清除当前 session 的一级缓存

  • localCacheScope:本地缓存作用域

    image-20210310205858366

    • 一级缓存 SESSION:当前会话的所有数据保存在会话缓存中;
    • STATEMENT:可以禁用一级缓存;

Mybatis 与 Spring 进行整合

MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapperSqlSession 并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException。 最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring。

(之后再补)

Mybatis 运行原理

  • MyBatis 层次图

image-20210310212828706

首先我们回顾一下运用 Mybatis 来执行 CRUD 的几个步骤:

  1. 获取 SqlSessionFacroty 对象

    1
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
  2. 获取 SqlSession 对象

    1
    SqlSession openSession = sqlSessionFactory.openSession();
  3. 获取接口的实现对象(接口的代理对象 MapperProxy)

    1
    EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
  4. 执行 CRUD 方法

    1
    Employee employee = mapper.getEmpById(1);

下面就根据这 4 步,来仔细看看源码,探究下每一步都发生了什么事情

SqlSessionFacroty 初始化

首先把一个配置文件转成流的形式,把这个流作为参数传到下列方法即可获取到一个 SqlSessionFactory

1
return new SqlSessionFactoryBuilder().build(inputStream);

那么其本质是怎么运作的呢?我们进入biuld方法

1
2
3
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}

发现其实呢 他调用的是自己的biuld方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
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.
}
}
}

看起来代码很多,但是真正执行的关键部分是

1
2
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
  1. 通过调用 XMLConfigBuilder(对 mybatsi 的配置文件进行解析的类)的 parse 方法
  2. 将上一步获取到的元素作为参数传入 biuld 方法,返回值

那么下面探究一下这个XMLConfigBuilder参数解析器类的parse()方法,我们 step in:

1
2
3
4
5
6
7
8
9
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//这里的/configuration 对应的是全局配置文件中的 /configuration标签
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

它的核心代码(如下)又做了什么呢?

1
2
  //这里的/configuration 对应的是全局配置文件中的 /configuration标签
parseConfiguration(parser.evalNode("/configuration"));

其中parser.evalNode("/configuration")是获取了一个根结点

我们进入这个parseConfiguration 类看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

我们在这里发现了很多我们很眼熟的标签:properties,typeAliases,environments,mappers

这不由得让我们思考:这段代码与 xml 中的标签是否有关呢?答案是肯定的

  • 这段代码的作用是:解析每一个标签,把其中详细的信息保存在Configuration

那么又是怎么解析的呢?我们点进mapperElement(root.evalNode("mappers"));看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果使用package标签
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//如果不是的话(比如使用了mapper标签)
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//以 <mapper resource="com/hpg/mapper/EmployeeMapper.xml"/> 为例
//我们就会进入这个方法
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
//得到一个流
InputStream inputStream = Resources.getResourceAsStream(resource);
//用了一个XMLMApper的解析器 传进来
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//使用parse方法进行解析
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}

OK,上面我们说到了使用mapperParser.parse();方法进行解析,我们继续 Step in 再看看

1
2
3
4
5
6
7
8
9
10
11
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}

parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}

它的第一行:configurationElement(parser.evalNode("/mapper")); 又是干嘛的呢?我们继续 Step in’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void configurationElement(XNode context) {
try {
//获取命名空间namespace
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//set方法注入
builderAssistant.setCurrentNamespace(namespace);
//如果写了缓存 就把缓存set入节点
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}

这个方法的作用就是解析各种标签,我们以 buildStatementFromContext(context.evalNodes("select|insert|update|delete"));为例,探究一下是如何解析增删改查标签的

我们首先看context.evalNodes("select|insert|update|delete")部分:

1
2
3
public List<XNode> evalNodes(String expression) {
return xpathParser.evalNodes(node, expression);
}

这段代码的返回值是一个 List,也就是得到了一个增删改查所有标签的 LIst

接着通过buildStatementFromContext()方法

1
2
3
4
5
6
7
8
9
private void buildStatementFromContext(List<XNode> list) {
//首先判断全局配置中是否配置了DatabaseId
if (configuration.getDatabaseId() != null) {
//配置了 则把databaseid作为参数
buildStatementFromContext(list, configuration.getDatabaseId());
}
//否则参数为null
buildStatementFromContext(list, null);
}

不管 databaseId 是否为空,我们都需要调用buildStatementFromContext()这个方法 我们 Step in

1
2
3
4
5
6
7
8
9
10
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}

首先呢,创建了一个 XMLStatementBuilder 对象,这个对象的用途是将用户的CRUD配置解析成对应的MappedStatement对象.

关于MappedStatement对象:

MappedStatement

接着,通过这个对象调用statementParser.parseStatementNode();方法

将 mapper.xml 中的每一个元素信息解析出来并且保存在configuration类(全局配置)中

1
2
3
4
5
6
7
8
9
10
11
public void parseStatementNode() {

//....省略前面的代码。前面代码的作用就是将 XNode(结点)类型值 contex 的值取出来并且赋值给、
//id,sqlSource...等等标签元素

//再通过下列方法 传入参数去获取一个什么呢?我们进入方法看看
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

我们进入builderAssistant.addMappedStatement方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {

if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}

id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);

ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}

MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}

我们进入这个方法,看到前面的所有代码都是为了下面这三句代码服务的

1
2
3
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;

我们首先看看第一句,鼠标放在 satement 对象上,看看里面都有哪些信息

1
MappedStatement statement = statementBuilder.build();

从下图,我们可以发现,一个 MappedStatement 保存了一个增删改查标签的详细信息

image-20210311162130714

第二句:

1
configuration.addMappedStatement(statement);

将我们辛辛苦苦创建出来的 statement 对象,存到了 configuration 对象中;

还记得我们一开始从哪个方法进来的吗?

1
mapperElement(root.evalNode("mappers"));

对的,这个代码又是在哪里被执行的呢?parseConfiguration(parser.evalNode("/configuration"));

mapper 解析完了意味着这个方法也跑完了,最后我们就获取了一个 configuration

1
2
3
4
5
6
7
8
9
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//这里的/configuration 对应的是全局配置文件中的 /configuration标签
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

我们也可以来看看这个 configuration 到底保存了什么

image-20210311163720603

我们仅仅是拿了一个解析 CRUD 标签为例,但肯定是不止解析了 CRUD 标签的,由此我们可以推断:

  • 这个 configuration 类保存了所有配置文件的详细信息

在这个类中有两个重要属性:

image-20210311164432459

当然了,还没完,我们获取到了 configuration 后,按照流程走的话,调用的是:

1
2
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());

或者换句话说,其实调用的是build(configuration); 因为本质上传的参数就是我们的 configuration 嘛

其中这个 biuld 方法的返回值,是一个 DefaultSqlSessionFactory

1
2
3
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

这个 DefaultSqlSessionFactory 也就是我们最后的返回值了;

1
2
3
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}

至此,大功告成,我们明白了,原来 SqlSessionFactory 初始化这一步的最终结果就是获取了一个 DefaultSqlSessionFactory

  • 我们用一张图来总结一下流程

image-20210311165446964

获取 SqlSession 对象

我们知道,获取 SqlSession 的方法是调用了 SqlSession 类中的 openSession()方法

1
2
3
4
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

关于

1
openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);

第一个参数的获取configuration.getDefaultExecutorType() 如下 默认是 SIMPLE

image-20210311170245338

我们进入这个方法看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//通过transactionFactory这个类我们也可以知道,【tx】这个类应该与事务有点关系
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//接着通过newExecutor方法去获取一个Executor类对象
final Executor executor = configuration.newExecutor(tx, execType);
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();
}
}

Step in newExecutor(tx, execType); 看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

原来啊,这个方法是通过 Executor 在全局配置中的类型,去创建以下三种类型的执行器 Executor

  1. BatchExecutor:每执行一次 update 或 select 就开启一个 Statement 对象,用完立刻关闭 Statement 对象;
  2. ReuseExecutor:执行 update 或 select,以 SQL 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后不关闭 Statement 对象,而是放置于 Map 内供下一次使用。简言之,就是重复使用 Statement 对象;
  3. SimpleExecutor:执行 update(没有 select,jdbc 批处理不支持 select),将所有 SQL 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理,与 jdbc 批处理相同。
  • 这些执行器的特点,严格限制在 SqlSession 的生命周期范围内

同时,根据有没有配置二级缓存,去决定是否包装成一个CachingExecutor

接着执行很重要的一步:

1
executor = (Executor) interceptorChain.pluginAll(executor);

进入这个方法,我们发现其作用是使用每一个拦截器重新包装 executor 并且返回

1
2
3
4
5
6
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}

上述全部执行完后,执行完

1
final Executor executor = configuration.newExecutor(tx, execType);

后,执行的就应该是下列代码,new 了一个 DefaultSqlSession (包含了 configuration 和 execut);

1
return new DefaultSqlSession(configuration, executor, autoCommit);

并且返回这个 SqlSession,至此,我们成功获取到了 SqlSession 对象

image-20210311171747802

接口的代理对象 MapperProxy 生成

我们知道,Mapper 代理对象是由 SqlSession 生成,那么我们进入 DefaultSqlSession 查看其中的 getMapper 方法

1
2
3
4
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}

我们进入configuration.getMapper(type, this);方法

1
2
3
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}

他是通过 mapperRegistry(上文其实有提及过)的 getMapper 方法获取代理对象的,我们继续 Step in

1
2
3
4
5
6
7
8
9
10
11
12
13
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//通过这第一行代码,通过传入type(接口类型)生成了一个代理对象的工厂
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//通过这个工程的newInstance方法,传入sqlSession,获得一个
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
  1. 首先生成一个代理对象的工厂对象
  2. 通过这个工厂的方法去进行下一步
1
2
3
4
5
6
public T newInstance(SqlSession sqlSession) {
//生成了一个MapperProxy 这个代理实现了InvocationHandler的接口
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
//接着又生成了什么呢?
return newInstance(mapperProxy);
}

我们 step in 这重载的 newInstance 方法看看

1
2
3
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

终于,我们守得云开见月明,发现其本质就是调用了 jdk 中的 newProxyInstance 动态代理方法,为我们生成了一个MapperProxy的代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {
Objects.requireNonNull(h);

final Class<?> caller = System.getSecurityManager() == null
? null
: Reflection.getCallerClass();

/*
* Look up or generate the designated proxy class and its constructor.
*/
Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);

return newProxyInstance(caller, cons, h);
}

至此,返回一个接口的代理对象,结束;

image-20210311194212629

CRUD 的执行(以查询为例)

以下列代码为例,我们继续 Step in

1
Employee employee = mapper.getEmpById(1);

首先,我们进入的是 MapperProxy 的 invoke 方法,其中将传入的方法包装成了一个 MapperMthod,通过这个类的 execute 方法去执行我们调用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//method 是我们要走的方法getEmpById
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//在这里 把我们要走的method包装成了MapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
//再调用包装后的MapperMethod的execute方法 此时会传入sqlSession
return mapperMethod.execute(sqlSession, args);
}

下面我们继续 step in 这个 execute 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//根据我们的标签的不同 我们进入不同的方法
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
//在这里我们是选择 于是进入这个方法
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
//如果返回多个对象的话
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
//如果返回一个map的话
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
//如果返回一个cursor游标的话
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
//其余 我们这里返回一个对象 因此走这个方法
//下列方法是不是很眼熟?没错 前面的参数解析部分我们也调用了这个方法
//其本质就是将我们所传进来的参数,转换成sql中能用的参数
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}

首先进行了方法的选择,之后将我们的参数解析包装成了 sql 中的参数,之后,调用 sqlSession 的 selectOne 方法(因为我们这里是查询单个嘛,如果是查询多个就是executeForMany了,视情况而定;

1
result = sqlSession.selectOne(command.getName(), param);

我们 step in 这个selectOne方法

1
2
3
4
5
6
7
8
9
10
11
12
@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.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;
}
}

t 首先通过:List<T> list = this.selectList(statement, parameter); 创建了一个 list,然后根据 list 的大小进相应的操作;那这个 selectList 又进行了什么操作呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//通过全局配置中的getMappedStatement获得了 关于增删改查的全部详细信息
MappedStatement ms = configuration.getMappedStatement(statement);
//调用执行器executor的 query方法
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();
}
}
  1. 通过全局配置中的 getMappedStatement 获得了 关于增删改查的全部详细信息

  2. 接着调用执行器 executor 的方法,其中传入的参数 ms 我们知道是一个 MappedStatement 对象,那后面的wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER又是什么呢?我们不妨看看

    1. private Object wrapCollection(final Object object) {
        if (object instanceof Collection) {
          StrictMap<Object> map = new StrictMap<>();
          map.put("collection", object);
          if (object instanceof List) {
            map.put("list", object);
          }
          return map;
        } else if (object != null && object.getClass().isArray()) {
          StrictMap<Object> map = new StrictMap<>();
          map.put("array", object);
          return map;
        }
        return object;
      }
      <!--code154-->
      

首先通过 mapperstatement 获取了 boundSql,这个类含有 sql 语句、参数映射等信息 总的来说 这是一个包含了 sql 详细信息的类

1
2
3
4
5
6
7
//sql语句
private final String sql;
//参数映射
private final List<ParameterMapping> parameterMappings;
private final Object parameterObject;
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;

image-20210311204437669

接着第二步CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql); 得到了一个缓存

image-20210311204742284

获取完boundSqlCacheKey后,执行query方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

最终调用了Executorquery方法;

image-20210311205120391

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
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++;
//首先从本地缓存中通过key获取list
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
//list不为空 即缓存中有存
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
//否则就调用真正的query方法
} else {
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;
}
  1. 首先从本地缓存中通过 key 拿到 list;第一次调用的话,key 应该是 null,因此无法从缓存中获取

    image-20210311205400264

  2. 若缓存中没有 list,则调用真正的 query 方法: list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {

    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
    localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
    }
    return list;
    }

    在这个方法中通过 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);创建 list,且通过 localCache.putObject(key, list); 将 list 放入本地缓存中

下面进入 doQery 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
//首先拿到一个全局配置
Configuration configuration = ms.getConfiguration();
//调用了newStatementHandler方法获得了Mybatsi的四大组件之一
//这个组件负责操作Statement对象与数据库进行交流
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
  1. 第一步:创建 StatementHandler

    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

    1
    2
    3
    4
    5
    6
    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    //statementHandler在此处被创建出来
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
    }

    先走第一步:StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    //根据mapperstatement中存储的statement的类型进行切换(type的设置在mapper文件中写sql语句可选择配)
    switch (ms.getStatementType()) {
    //type = STATEMENT 非预编译形式
    case STATEMENT:
    delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    break;
    //type = PREPARED 预编译形式 这是默认的
    case PREPARED:
    delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    break;
    //type = CALLABLE
    case CALLABLE:
    delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    break;
    default:
    throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

    }
    1. 会走预编译形式 的执行器的构造方法

      1
      2
      3
      public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
      super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
      }

    然后走第二步:statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);

    包装拦截器 之前也有讲过

    1
    2
    3
    4
    5
    6
    public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
    }
    return target;
    }
  2. 第二步:stmt = prepareStatement(handler, ms.getStatementLog()); 通过这个方法,创建出了原生的Statement 对象;继续 step in:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    //获取连接
    Connection connection = getConnection(statementLog);
    //通过handler进行参数预编译 设置到statemtnt对象中
    stmt = handler.prepare(connection, transaction.getTimeout());
    //调用ParameterHandler去设置参数
    handler.parameterize(stmt);
    return stmt;
    }
    1. 进入 parameterize 方法
    1
    2
    3
    4
    5
     //  private final StatementHandler delegate;
    @Override
    public void parameterize(Statement statement) throws SQLException {
    delegate.parameterize(statement);
    }

    本质上调用了StatementHandlerparameterize方法

    即:

    1
    2
    3
    4
    5
    6
    7
    //PreparedStatementHandler extends BaseStatementHandler
    //BaseStatementHandler implements StatementHandler
    @Override
    public void parameterize(Statement statement) throws SQLException {
    // protected final ParameterHandler parameterHandler; 调用的是ParameterHandler的方法
    parameterHandler.setParameters((PreparedStatement) statement);
    }

    其中有人会好奇,这个 ParameterHandler 是什么时候创建的呢?答案是在 BaseStatementHandler 被创建时就创建了

    1
    2
    3
    4
    5
    6
    7
    8
    protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    .....

    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);

    }

    我们来看看这个 ParameterHandler 他是一个接口,有着我们想要的 setParameters 即 设置参数方法

    1
    2
    3
    4
    5
    6
    7
    8
    public interface ParameterHandler {

    Object getParameterObject();

    void setParameters(PreparedStatement ps)
    throws SQLException;

    }

    这个方法是由public class DefaultParameterHandler implements ParameterHandler类 去实现的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    @Override
    public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
    for (int i = 0; i < parameterMappings.size(); i++) {
    ParameterMapping parameterMapping = parameterMappings.get(i);
    if (parameterMapping.getMode() != ParameterMode.OUT) {
    Object value;
    String propertyName = parameterMapping.getProperty();
    if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
    value = boundSql.getAdditionalParameter(propertyName);
    } else if (parameterObject == null) {
    value = null;
    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
    value = parameterObject;
    } else {
    MetaObject metaObject = configuration.newMetaObject(parameterObject);
    value = metaObject.getValue(propertyName);
    }
    //获得类型处理器
    TypeHandler typeHandler = parameterMapping.getTypeHandler();
    JdbcType jdbcType = parameterMapping.getJdbcType();
    if (value == null && jdbcType == null) {
    jdbcType = configuration.getJdbcTypeForNull();
    }
    try {
    //调用类型处理器的setParameter方法 把预编译语句 下标 值 类型 传进去
    typeHandler.setParameter(ps, i + 1, value, jdbcType);
    } catch (TypeException | SQLException e) {
    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
    }
    }
    }
    }
    }
  3. 第三步:return handler.query(stmt, resultHandler);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    //将语句转成预编译语句
    PreparedStatement ps = (PreparedStatement) statement;
    //执行
    ps.execute();
    //使用resultSetHandler去封装处理结果
    return resultSetHandler.handleResultSets(ps);
    }
    1. execute()方法:(看不看得懂是一回事,先放上来…)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      public boolean execute() throws SQLException {
      synchronized (checkClosed().getConnectionMutex()) {

      MySQLConnection locallyScopedConn = this.connection;

      if (!this.doPingInstead && !checkReadOnlySafeStatement()) {
      throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"),
      SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
      }

      ResultSetInternalMethods rs = null;

      this.lastQueryIsOnDupKeyUpdate = false;

      if (this.retrieveGeneratedKeys) {
      this.lastQueryIsOnDupKeyUpdate = containsOnDuplicateKeyUpdateInSQL();
      }

      this.batchedGeneratedKeys = null;

      resetCancelledState();

      implicitlyCloseAllOpenResults();

      clearWarnings();

      if (this.doPingInstead) {
      doPingInstead();

      return true;
      }

      setupStreamingTimeout(locallyScopedConn);

      Buffer sendPacket = fillSendPacket();

      String oldCatalog = null;

      if (!locallyScopedConn.getCatalog().equals(this.currentCatalog)) {
      oldCatalog = locallyScopedConn.getCatalog();
      locallyScopedConn.setCatalog(this.currentCatalog);
      }

      //
      // Check if we have cached metadata for this query...
      //
      CachedResultSetMetaData cachedMetadata = null;
      if (locallyScopedConn.getCacheResultSetMetadata()) {
      cachedMetadata = locallyScopedConn.getCachedMetaData(this.originalSql);
      }

      Field[] metadataFromCache = null;

      if (cachedMetadata != null) {
      metadataFromCache = cachedMetadata.fields;
      }

      boolean oldInfoMsgState = false;

      if (this.retrieveGeneratedKeys) {
      oldInfoMsgState = locallyScopedConn.isReadInfoMsgEnabled();
      locallyScopedConn.setReadInfoMsgEnabled(true);
      }

      //
      // Only apply max_rows to selects
      //
      locallyScopedConn.setSessionMaxRows(this.firstCharOfStmt == 'S' ? this.maxRows : -1);

      rs = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(), (this.firstCharOfStmt == 'S'), metadataFromCache, false);

      if (cachedMetadata != null) {
      locallyScopedConn.initializeResultsMetadataFromCache(this.originalSql, cachedMetadata, rs);
      } else {
      if (rs.reallyResult() && locallyScopedConn.getCacheResultSetMetadata()) {
      locallyScopedConn.initializeResultsMetadataFromCache(this.originalSql, null /* will be created */, rs);
      }
      }

      if (this.retrieveGeneratedKeys) {
      locallyScopedConn.setReadInfoMsgEnabled(oldInfoMsgState);
      rs.setFirstCharOfQuery(this.firstCharOfStmt);
      }

      if (oldCatalog != null) {
      locallyScopedConn.setCatalog(oldCatalog);
      }

      if (rs != null) {
      this.lastInsertId = rs.getUpdateID();

      this.results = rs;
      }

      return ((rs != null) && rs.reallyResult());
      }
      }
    2. resultSetHandler.handleResultSets(ps);方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      @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;
      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);
      }

      总的来说就是查出数据,使用 ResultSetHandler 去处理结果;然后通过 TypeHandler 去获取结果值

总的来说:

  1. 根据具体传入的参数,动态地生成需要执行的 SQL 语句,用 BoundSql 对象表示
  2. 为当前的查询创建一个缓存 Key
  3. 缓存中没有值,直接从数据库中读取数据
  4. 执行查询,返回 List 结果,然后 将查询的结果放入缓存之中
  5. 根据既有的参数,创建 StatementHandler 对象来执行查询操作
  6. 将创建 Statement 传递给 StatementHandler 对象,调用 parameterize()方法赋值
  7. 调用 StatementHandler.query()方法,返回 List 结果集

image-20210311220722327

查询流程总结

image-20210311220745262

  • 查询流程

image-20210312202645885

我们的对象调用过程:代理对象DefaultSqlSessionExecutorStatementHandler

关于StatementHandler的类结构:(短暂过一下,不做细究)

img

  • RoutingStatementHandler:路由处理器,这个相当于一个静态代理,根据 MappedStatement.statementType 创建对应的对处理器;
  • SimpleStatementHandler:不需要预编译的简单处理器;
  • PreparedStatementHandler:预编译的 SQL 处理器;
  • CallableStatementHandler:主要用于存储过程的调度;

以及ResultSetHandler的类继承结构:

img

在我们的查询流程中,我们主要使用的是PreparedStatementHandlerResultSetHandler

  1. 通过前者进行设置参数预编译
  2. 后者进行结果集的一定处理

这两个类在处理过程中,都使用了typeHandler类分别进行参数和结果集的处理:

  1. DefaultParameterHandler
     typeHandler.setParameter(ps, i + 1, value, jdbcType)
    <!--code173-->
    

因此,我们就知道了TypeHandler的作用了:进行数据库类型javaBean 类型的映射

当然了,所有的类的方法,追根溯源都依靠的是 JDBC 去执行,换句话说,都依靠的是Statement和PreparedStatement去执行的;

全局总结

思维导图:

image-20210312210632202

Mybatis 插件

插件原理

要自己编写插件之前肯定得明白原理,即插件是什么,是怎么运作的,我们往下看;

首先,抛开插件,我们要明白,在四大对象(Executor、ParameterHandler、ResultSetHandler、StatementHandler)被创建的的时候,都调用了一个方法:interceptorChain.pluginAll();

1
2
3
4
5
6
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}

这段代码的作用,就是遍历/获取到所有的Interceptor(拦截器),之后通过interceptor.plugin(target)返回 target 包装后的对象

因此!到这里,我们可以思考一下,假如我们也能实现一个拦截器,那我们在创建四大对象的时候,相当于外面套了一层我们做的衣服/功能,这样的话,岂不是就可以让 Mybatis 做一些我们 想让他做的事情了吗?这就是插件的原理:利用动态代理去给目标对象创建一个代理对象,用以实现特殊功能;

插件编写测试

如我们上述所言,若我们能实现一个拦截器,就达到了插件的效果了,接下来就动手来做一做

首先,插件编写需要以下几步

  1. 编写 Interceptor 的实现类
  2. 使用@Intercept 注解去完成插件的签名
  3. 将写好的插件注册到全局配置文件中

需要实现拦截器,就需要实现Interceptor接口

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

default Object plugin(Object target) {
return Plugin.wrap(target, this);
}

default void setProperties(Properties properties) {
// NOP
}

}

测试代码

  • 自定义插件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.hpg.dao;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;

import java.util.Properties;


/* 完成插件的签名: 告诉Mybatis当前使用的插件去拦截哪个对象的哪个方法
*
* */

@Intercepts(
{
@Signature(type= StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
})
public class MyFirstPlugin implements Interceptor {
/*
* intercept(拦截): 拦截目标对象的目标方法的执行
* */
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyFirstPlugin 影响的方法" + invocation.getMethod());
//执行目标方法
Object proceed = invocation.proceed();
//return返回值
return proceed;
}

/*plugin:包装, 本质就是为目标对象去创建一个代理对象
*
* */

@Override
public Object plugin(Object target) {
//借助Plugin的wrap方法 去使用Interceptor去包装目标对象
System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target);
//这里对应的this 是 MyFirstPlugin
Object wrap = Plugin.wrap(target, this);
return wrap;
}


/* setProperties: 将插件注册时 的property属性设置进来
*
* */
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的信息:"+properties);
}
}

全局配置:

1
2
3
4
5
6
7
8
9
<!--注意:插件的注册不能写在settings的前面-->
<!--plugins:注册插件 -->
<plugins>
<plugin interceptor="com.hpg.dao.MyFirstPlugin">
<property name="username" value="root"/>
<property name="password" value="123456"/>
</plugin>
<plugin interceptor="com.hpg.dao.MyFirstPlugin"></plugin>
</plugins>

执行结果:

image-20210312221445363

  • 关键信息

    尽管我们的标签里面设置了只对StatementHandler对象进行包装,但是我们也会生成四个包装对象:

    即分别对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 进行了包装。这是我们需要注意的;

    1
    2
    3
    4
    5
    6
    7
    8
    MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.CachingExecutor@6973b51b
    MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.CachingExecutor@6973b51b
    MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@399f45b1
    MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@399f45b1
    MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@35cabb2a
    MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@35cabb2a
    MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@7e07db1f
    MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@7e07db1f

在执行完 sql 之后:
我们的插件也对设置参数方法进行了影响/拦截,此时就只会对我们想要包装的对象的方法进行拦截了,就不会像上面生成代理对象这一步时一样,对四大对象都生成代理对象。

1
2
MyFirstPlugin 影响的方法public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
MyFirstPlugin 影响的方法public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException

关于插件一些细节

假如我们在写一个插件,与第一个插件共同运作时,会怎么样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Intercepts(
{
@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
})
public class MySecondPlugin implements Interceptor{

@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MySecondPlugin...intercept:"+invocation.getMethod());
return invocation.proceed();
}

@Override
public Object plugin(Object target) {
// TODO Auto-generated method stub
System.out.println("MySecondPlugin...plugin:"+target);
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}

}

image-20210313160743141

我们发现 在产生代理对象时候是先进行 first 插件,再进行 second 插件

但是在拦截方法的时候,是先进行 second 插件,再进行 first 插件;

因此我们可以得出结论

  1. 插件会产生目标对象的代理对象,多个插件就会产生多层代理对象

    image-20210313160901039

  2. 创建动态代理的时候,是按照插件配置顺序层层去创建代理对象。然而执行目标方法时,是按照逆向顺序执行

    image-20210313160944715

开发插件

有了上述例子,我们可以试着去推断开发插件的思路了:

从 invocation 参数中获取执行方法的对象/方法/方法参数 → 获取该对象的元数据 → 获取想要修改的参数 → 修改该参数

下面我们改造一下第一个插件的intercept方法 我们输入的员工 id 为 1,我们修改成 11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public Object intercept(Invocation invocation) throws Throwable {
// TODO Auto-generated method stub
System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
//动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询3号员工
Object target = invocation.getTarget();
System.out.println("当前拦截到的对象:"+target);
//拿到:StatementHandler==>ParameterHandler===>parameterObject
//拿到target的元数据
MetaObject metaObject = SystemMetaObject.forObject(target);
Object value = metaObject.getValue("parameterHandler.parameterObject");
System.out.println("sql语句用的参数是:"+value);
//修改完sql语句要用的参数
metaObject.setValue("parameterHandler.parameterObject", 11);
//执行目标方法
Object proceed = invocation.proceed();
//返回执行后的返回值
return proceed;
}

结果:

image-20210313162533937

插件实战 - PageHelper 分页插件

PageHelper 失效问题在于版本没有对应:https://blog.csdn.net/sinat_34104446/article/details/92679046

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<plugins>
<!-- PageHelper4版本插件配置 -->
<plugin interceptor="com.github.pagehelper.PageHelper"/>
</plugins>
<!-- PageHelper4版本依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.6</version>
</dependency>

---------------------------------------------------------------------

<plugins>
<!-- PageHelper5版本配置 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>
<!-- PageHelper5版本依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.6</version>
</dependency>

数据库信息:

image-20210313163849060

外部依赖:

pom.xml:

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.6</version>
</dependency>

全局配置:

1
2
3
4
<plugins>
<!-- PageHelper4版本插件配置 -->
<plugin interceptor="com.github.pagehelper.PageHelper"/>
</plugins>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Test
public void InterfaceTest() throws IOException {
// 1、获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2、获取sqlSession对象
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Page<Object> page = PageHelper.startPage(1, 5);

List<Employee> emps = mapper.getEmps();
//传入要连续显示多少页 navigatePages 分页导航
PageInfo<Employee> info = new PageInfo<>(emps, 5);
for (Employee employee : emps) {
System.out.println(employee);
}
/*System.out.println("当前页码:"+page.getPageNum());
System.out.println("总记录数:"+page.getTotal());
System.out.println("每页的记录数:"+page.getPageSize());
System.out.println("总页码:"+page.getPages());*/
///xxx
System.out.println("当前页码:"+info.getPageNum());
System.out.println("总记录数:"+info.getTotal());
System.out.println("每页的记录数:"+info.getPageSize());
System.out.println("总页码:"+info.getPages());
System.out.println("是否第一页:"+info.isIsFirstPage());
System.out.println("连续显示的页码:");
int[] nums = info.getNavigatepageNums();
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}

} finally {
openSession.close();
}

}

image-20210313165433423