八股文之mybatis

文章目录

1、mybatis是什么

img

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

MyBatis架构图主要分三层。基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。数据处理层:负责具体的 SQL查找、SQL解析、SQL执行和执行结果映射处理 等。它主要的目的是根据调用的请求完成一次数据库操作。API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

2、orm是什么

对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术,通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中

3、传统JDBC开发存在的问题

频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能

sql语句定义、参数设置、结果集处理存在硬编码

结果集处理存在重复代码,处理麻烦。

执行流程

img

注册驱动,在static代码块中注册,这里用Class.forName方法来检查驱动类的方式

//加载驱动
Class.forName("oracle.jdbc.driver.OracleDriver");

获取数据的链接,根据url 用户名和密码

//获取连接
        Connection conn=DriverManager.getConnection(
                "jdbc:oracle:thin:@localhost:1521:XE",
                "SCOTT",
                "TIGER"
        );

创建SQL的语句

//准备sql
String sql="select * from dept";

执行语句

//封装处理快
Statement statement=conn.createStatement();
//发送sql获取结果
ResultSet result=statement.executeQuery(sql);

如果有结果集处理结果集

//处理结果
while(result.next()){
	int deptno=result.getInt(1);
    String dname=result.getString(2);
	String loc=result.getString(3);
	System.out.println("deptno="+deptno+",dname="+dname+",loc="+loc);
}

释放资源

//关闭资源
result.close();
conn.close();
statement.close();

完整代码

public class Test01 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        //加载驱动
        Class.forName("oracle.jdbc.driver.OracleDriver");
        //获取连接
        Connection conn=DriverManager.getConnection(
                "jdbc:oracle:thin:@localhost:1521:XE",
                "SCOTT",
                "TIGER"
        );
        //准备sql
        String sql="select * from dept";
        //封装处理快
        Statement statement=conn.createStatement();
        //发送sql获取结果
        ResultSet result=statement.executeQuery(sql);
        //处理结果
        while(result.next()){
           int deptno=result.getInt(1);
           String dname=result.getString(2);
           String loc=result.getString(3);
            System.out.println("deptno="+deptno+",dname="+dname+",loc="+loc);
        }
        //关闭资源
        result.close();
        conn.close();
        statement.close();
    }
}

4、mybatis适用场景

  • MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。
  • 对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。

5、#{}和${}区别

  • #{}是预编译处理,MyBatis 在处理 #{ }时,它会将sql中的#{ }替换为 ?号,然后调用JDBC 中 PreparedStatement 的 set 方法来赋值,传入字符串后会自动在值两边加上单引号,使用占位符的方式提高效率,可以防止 sql 注入问题
  • ${ } 表示拼接 sql 串,将接收到参数的内容不加任何修饰拼接在 sql 中,可能引发 sql 注入问题(一般很少使用)

6、模糊查询like如何使用

方案一:CONCAT(’%’,#{question},’%’) 使用CONCAT()函数,推荐

<select id="getCourseInfo" parameterType="string" resultType="com.geekmice.staging.entity.CourseDO">
    select score_prize as scorePrize,stu_no as stuNo from score
    <where>
        <if test=" stuNo != '' and stuNo != null  ">
            stu_no like CONCAT('%', #{stuNo}, '%')
        </if>
    </where>
</select>

方案二:使用bind标签

<select id="getCourseInfo" parameterType="string" resultType="com.geekmice.staging.entity.CourseDO">
    <bind name="pattern" value="'%' + stuNo + '%'"/>
    select score_prize as scorePrize,stu_no as stuNo from score
    <where>
        <if test=" stuNo != '' and stuNo != null  ">
            stu_no like #{pattern}
        </if>
    </where>
</select>

7、mapper中如何传递多个参数

顺序传参法
public User selectUser(String name, int deptId);

<select id="selectUser" resultMap="UserResultMap">
    select * from user
    where user_name = #{0} and dept_id = #{1}
</select>

#{}里面的数字代表传入参数的顺序。

这种方法不建议使用,sql层表达不直观,且一旦顺序调整容易出错。

@param注解传参

数据层DAO

/**
  * @param stuNo
  * @param teacherNo
  * @return
  * @description 顺序传参法
  */
CourseDO getCourse(@Param("courseNO") String courseNo, @Param("teacherNO") String teacherNo);

映射文件xml

<select id="getCourse" parameterType="java.lang.String" resultType="com.geekmice.staging.entity.CourseDO">
    select course_no as courseNo,teacher_no as teacherNo
    ,course_name as courseName
    from course
    <where>
        <if test="courseNO != null and courseNO != '' ">and course_no =#{courseNO}</if>
        <if test="teacherNO != null and teacherNO != '' ">and teacher_no =#{teacherNO}</if>
    </where>
</select>

#{}里面的名称对应的是注解@Param括号里面修饰的名称。

这种方法在参数不多的情况还是比较直观的,推荐使用。

注意:则在获取参数时,如果是要进行非null的判断,则不可在if后直接那变量名进行判空,因为mybatis会默认变量名为_parameter,否则会报no getter/setter错误。正确的写法如下:

<if test="_parameter != null and _parameter != ''">
    and stu_no =#{stuNo,jdbcType}
</if>

上面情况是针对于string类型参数直接传入,如果不想在判断时使用,mybatis默认的变量名,则需要在dao层后台传入时加上@Param

public interface CourseDAO {
    List<CourseDO> getCourseInfo(@Param("stuNo") String stuNo);
}
map传参

数据层DAO

CourseDO getCourseSecond(Map<String,Object> map);

映射文件xml

<select id="getCourseSecond" parameterType="java.util.Map" resultType="com.geekmice.staging.entity.CourseDO">
    select course_no as courseNo,teacher_no as teacherNo
    ,course_name as courseName
    from course
    <where>
        <if test="courseNo != null and courseNo != '' ">and course_no =#{courseNo}</if>
        <if test="teacherNo != null and teacherNo != '' ">and teacher_no =#{teacherNo}</if>
    </where>
</select>

#{}里面的名称对应的是Map里面的key名称。

这种方法适合传递多个参数,且参数易变能灵活传递的情况。

测试类Test

import com.geekmice.staging.BaoProjectApiApplication;
import com.geekmice.staging.dao.CourseDAO;
import com.geekmice.staging.entity.CourseDO;
import org.apache.commons.lang3.*;
import org.apache.commons.lang3.math.NumberUtils;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.runner.RunWith;
import org.springframework.test.annotation.Commit;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.List;
import java.util.Random;

@SpringBootTest(classes = BaoProjectApiApplication.class)
public class BaoTest {

    // 日志管理器
    private Logger logger = LoggerFactory.getLogger(BaoTest.class);

    @Autowired
    private CourseDAO courseDAO;

    @Test
    public void t1() {
       CourseDO itemSecond = courseDAO.getCourseSecond(map);
       logger.info("itemSecond {}",itemSecond);
    }

}
javabean 传参

数据层DAO

CourseDO getCourseThird(CourseDO item);

映射文件xml

<select id="getCourseThird" parameterType="com.geekmice.staging.entity.CourseDO" resultType="com.geekmice.staging.entity.CourseDO">
    select course_no as courseNo,teacher_no as teacherNo
    ,course_name as courseName
    from course
    <where>
        <if test="courseNo != null and courseNo != '' ">and course_no =#{courseNo}</if>
        <if test="teacherNo != null and teacherNo != '' ">and teacher_no =#{teacherNo}</if>
    </where>
</select>

测试类Test

import com.geekmice.staging.BaoProjectApiApplication;
import com.geekmice.staging.dao.CourseDAO;
import com.geekmice.staging.entity.CourseDO;
import org.apache.commons.lang3.*;
import org.apache.commons.lang3.math.NumberUtils;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.runner.RunWith;
import org.springframework.test.annotation.Commit;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.List;
import java.util.Random;

@SpringBootTest(classes = BaoProjectApiApplication.class)
public class BaoTest {

    // 日志管理器
    private Logger logger = LoggerFactory.getLogger(BaoTest.class);

    @Autowired
    private CourseDAO courseDAO;

    @Test
    public void t1() {
        CourseDO newTiem = new CourseDO();
        newTiem.setCourseNo("0001");
        newTiem.setTeacherNo("0001");
        CourseDO courseThird = courseDAO.getCourseThird(newTiem);
        logger.info("courseThird {}",courseThird);
    }

}

#{}里面的名称对应的是User类里面的成员属性。

这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。

8、如何获取生成的主键

对于支持主键自增的数据库(MySQL)

useGeneratedKeys

<insert id="save" parameterType="com.geekmice.staging.entity.EmpDO" useGeneratedKeys="true" keyProperty="id">
    insert into
    emp
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="empNo != null and empNo != ''">empno,</if>
        <if test="name !=null and name != ''">name,</if>
        <if test="age!=null and age != ''">age,</if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
        <if test="empNo !=null and empNo != ''">#{empNo,jdbcType=VARCHAR},</if>
        <if test="name !=null and name != ''">#{name,jdbcType=VARCHAR},</if>
        <if test="age !=null and age != ''">#{age,jdbcType=INT},</if>
    </trim>
</insert>

selectkey方式

<insert id="saveSelectKey" parameterType="com.geekmice.staging.entity.EmpDO">
    <selectKey keyProperty="id" resultType="Integer" keyColumn="id" order="BEFORE">
        SELECT LAST_INSERT_ID() as id
    </selectKey>
    insert into
    emp
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="empNo != null and empNo != ''">empno,</if>
        <if test="name !=null and name != ''">name,</if>
        <if test="age!=null and age != ''">age,</if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
        <if test="empNo !=null and empNo != ''">#{empNo,jdbcType=VARCHAR},</if>
        <if test="name !=null and name != ''">#{name,jdbcType=VARCHAR},</if>
        <if test="age !=null and age != ''">#{age,jdbcType=INT},</if>
    </trim>
</insert>

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

通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。

<select id="getOrder" parameterType="int" resultType="com.jourwon.pojo.Order">
       select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
</select>

通过<resultMap>来映射字段名和实体类属性名的一一对应的关系。

<resultMap id="BaseResultMap" type="com.jack.springbootmybatis.pojo.Student" >
    <!--   stu_no数据库表字段,stuNo简单java对象属性,jdbcType表示数据库对应字段类型 -->
    <id column="stu_no" property="stuNo" jdbcType="VARCHAR" />
    <result column="stu_name" property="stuName" jdbcType="VARCHAR" />
    <result column="stu_born_date" property="stuBornDate" jdbcType="DATE" />
    <result column="stu_sex" property="stuSex" jdbcType="CHAR" />
</resultMap>

<sql id="Base_Column_List" >
    stu_no, stu_name, stu_born_date, stu_sex
</sql>

<select id="selectSelective" resultMap="BaseResultMap" parameterType="com.jack.springbootmybatis.pojo.Student">
    select
    <include refid="Base_Column_List" />
    from student
    <where>
      <if test="stuNo != null and stuNo != ''">
        stu_no=${stuNo}
      </if>
      <if test="stuName != null and stuName != ''">
        stu_name=${stuName}
      </if>
      <if test="stuBornDate != null and stuBornDate != ''">
        stu_born_date=${stuBornDate}
      </if>
      <if test="stuSex != null and stuSex != ''">
        stu_sex=${stuSex}
      </if>
    </where>
  </select>

通过resulttype起别名映射

<select id="getCourseInfo" parameterType="string" resultType="com.geekmice.staging.entity.CourseDO">
    <!-- 
     score_prize 别名 scorePrize
     stu_no 别名 stuNo
	CourseDO需要保证属性包含scorePrize,stuNo
     -->
    select score_prize as scorePrize,stu_no as stuNo from score
    <where>
        <if test=" stuNo != '' and stuNo != null  ">
            stu_no = #{stuNo}
        </if>
    </where>
</select>

10、Mapper 编写有哪几种方式?

使用 mapper 扫描器

1、mapper.xml 文件编写

mapper.xml 中的 namespace 为 mapper 接口的地址
mapper 接口中的方法名和 mapper.xml 中的定义的 statement id 保持一致;
如果将 mapper.xml mapper 接口的名称保持一致则不用在 sqlMapConfig.xml中进行配置。

2、定义 mapper 接口:

注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录

4、使用扫描器后从 spring 容器中获取 mapper 的实现对象
@MapperScan("com.geekmice.dao.*")

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

通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;

通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。

当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。

12、使用MyBatis的mapper接口调用时有哪些要求?

1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同。

2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同。

3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同。

4、Mapper.xml文件中的namespace即是mapper接口的类路径。

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

Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个、、、标签,都会被解析为一个MappedStatement对象。

Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。

Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。

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

不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。

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

15、resultMapresultType区别

MyBatis中在查询进行select映射的时候,返回类型可以用resultType,也可以用resultMapresultType是直接表示返回类型的,而resultMap则是对外部ResultMap的引用,但是resultTyperesultMap不能同时存在。
MyBatis进行查询映射时,其实查询出来的每一个属性都是放在一个对应的Map里面的,其中键是属性名,值则是其对应的值。

对于resultType而言

1、如果数据库表的字段名和实体bean对象的属性名一样时,那么也可以直接使用resultType返回结果。

2、MyBatis在执行sql语句时,会把查询出来的字段名和resultType定义实体bean对象的属性进行一一对应,然后再把查询到的值放到实体bean对象的属性中,完成赋值操作。但如果不一样,则会查询出空值,可以通过SQL中as关键字起别名进行字段映射。

bean对象

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class EmpDO {
    private Integer id;
    private String empNo;
    private String name;
    private Integer age;
    private String testType;
}

数据层DAO

public interface EmpDAO {
    List<EmpDO> getEmpById(EmpDO item);
}

xml映射文件

<!--验证resultType-->
<select id="getEmpById" resultType="com.geekmice.staging.entity.EmpDO"
        parameterType="com.geekmice.staging.entity.EmpDO">
    select id,empno,name,age,test_type as testType from emp
    <where>
        <if test="empNo != null and empNo != ''">empno = #{empNo,jdbcType=VARCHAR}</if>
    </where>
</select>

image-20221113002627851

对于resultMap而言

resultMap要配置一下,将数据库表的字段名和实体bean对象类的属性名一一对应关系,这样的话就算你的数据库的字段名和你的实体类的属性名不一样也没有关系,都会给你对应的映射出来,所以resultMap要更强大一些。

关联查询,一对一,一对多场景,更加灵活

一对一

<!-- 订单查询关联用户的resultMap 将整个查询的结果映射到cn.itcast.mybatis.po.Orders中   -->
    <resultMap type="cn.itcast.mybatis.po.Orders" id="OrdersUserResultMap">
        <!-- 配置映射的订单信息 -->
        <!-- id:指定查询列中的唯 一标识,订单信息的中的唯 一标识,如果有多个列组成唯一标识,配置多个id
            column:订单信息的唯 一标识 列
            property:订单信息的唯 一标识 列所映射到Orders中哪个属性
          -->
        <id column="id" property="id"/>
        <result column="user_id" property="userId"/>
        <result column="number" property="number"/>
        <result column="createtime" property="createtime"/>
        <result column="note" property=note/>
        
        <!-- 配置映射的关联的用户信息 -->
        <!-- association:用于映射关联查询单个对象的信息
        property:要将关联查询的用户信息映射到Orders中哪个属性
         -->
        <association property="user"  javaType="cn.itcast.mybatis.po.User">
            <!-- id:关联查询用户的唯 一标识
            column:指定唯 一标识用户信息的列
            javaType:映射到user的哪个属性
             -->
            <id column="user_id" property="id"/>
            <result column="username" property="username"/>
            <result column="sex" property="sex"/>
            <result column="address" property="address"/>
        
        </association>
    </resultMap>

一对多

-- 订单及订单明细的resultMap
    使用extends继承,不用在中配置订单信息和用户信息的映射
     -->
    <resultMap type="cn.itcast.mybatis.po.Orders" id="OrdersAndOrderDetailResultMap" extends="OrdersUserResultMap">
        <!-- 订单信息 -->
        <!-- 用户信息 -->
        <!-- 使用extends继承,不用在中配置订单信息和用户信息的映射 -->
        
        
        <!-- 订单明细信息
        一个订单关联查询出了多条明细,要使用collection进行映射
        collection:对关联查询到多条记录映射到集合对象中
        property:将关联查询到多条记录映射到cn.itcast.mybatis.po.Orders哪个属性
        ofType:指定映射到list集合属性中pojo的类型
         -->
         <collection property="orderdetails" ofType="cn.itcast.mybatis.po.Orderdetail">
             <!-- id:订单明细唯 一标识
             property:要将订单明细的唯 一标识 映射到cn.itcast.mybatis.po.Orderdetail的哪个属性
               -->
             <id column="orderdetail_id" property="id"/>
             <result column="items_id" property="itemsId"/>
             <result column="items_num" property="itemsNum"/>
             <result column="orders_id" property="ordersId"/>
         </collection>
        
    
    </resultMap>

16、@requestParam,@PathVariable,@requestbody区别

1.1、@Pathvariable

通过占位符的方式获取入参,前端示例:url:http://localhost:8080/system/student/${stuSno}
也即是从路径里面去获取变量
后端:

    /**
     * @param stuSno 学号
     * @return 学生信息
     * @description 根据主键获取学生信息
     */
    @GetMapping("/selectByPrimaryKey/{stuSno}")
    public Student selectByPrimaryKey(@PathVariable String stuSno) {
        return studentService.selectByPrimaryKey(stuSno);
    }

这种情况是方法参数名称和需要绑定的url中变量名称一致时
若是若方法参数名称和需要绑定的url中变量名称不一致时
后端:

/**
 * @param stuSno 学号
 * @return 学生信息
 * @description 根据主键获取学生信息
 */
@GetMapping("/selectByPrimaryKey/{stuSno}")
public Student selectByPrimaryKey(@PathVariable("stuSno") String sno) {
    return studentService.selectByPrimaryKey(stuSno);
}

注意:前端传参的URL于后端@RequestMapping的URL必须相同且参数位置一一对应,否则前端会找不到后端地址

1.2、@RequestParam
  1. 作用
    将请求参数绑定在控制层(controller)方法参数【springmvc注解】
  2. 语法
@RequestParam(value="参数名",required="true/false",default="")

在这里插入图片描述

value:表示前端传过来的值名称,如果你不设置,那就默认使用服务端使用的参数名称(stuSno)
不设置:
前端:在这里插入图片描述

http://localhost:8081/student/selectByPrimaryKey1?stuSno=0001

后端:
在这里插入图片描述

public Student selectByPrimaryKey1(@RequestParam String stuSno) {

设置
前端:
在这里插入图片描述

http://localhost:8081/student/selectByPrimaryKey1?sno=0001

后端:
在这里插入图片描述
此时@requestParam中value=“sno” value可以省略 直接输入“sno”,类似于@RequestMapping

    public Student selectByPrimaryKey1(@RequestParam("sno") String stuSno) {

这时候前端传sno并非stuSno,需要在@requestParam中value设置sno
required:是否包含该参数,默认为true,表示该请求路径中必须包含该参数,如果不包含就报错
在这里插入图片描述

2021-08-15 02:26:30.495  WARN 4736 --- [nio-8081-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'sno' for method parameter type String is not present]

defaultValue:默认参数值,如果设置了该值,required=true将失效,自动变为false,如果没有传该参数,使用默认值;比如说此时 后端直接写成 defaultValue=“0001”
在这里插入图片描述在这里插入图片描述

  1. 示例说明
    后端controller
    /**
     * @param stuSno 学号
     * @return 学生信息
     * @description 根据主键获取学生信息
     */
    @GetMapping("/selectByPrimaryKey")
    public Student selectByPrimaryKey1(@RequestParam(value = "sno",defaultValue = "0001",required = false) String stuSno) {
        return studentService.selectByPrimaryKey(stuSno);
    }

前端暂时使用 postman
在这里插入图片描述

1.3、@RequestBody

@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的)

  1. @RequestBody直接以String接收前端传过来的json数据
    后端代码
    /**
     * @param stuSno 学号
     * @return 学生信息
     * @description 根据主键获取学生信息
     */
    @GetMapping("/selectByPrimaryKey3")
    public Student selectByPrimaryKey2(@RequestBody String jsonString) {
        // 使用fastjson解析json格式字符串为json对象
        JSONObject jsonObject = JSONObject.parseObject(jsonString);
        // 获取学号
        String stuSno = jsonObject.getString("stuSno");
        return studentService.selectByPrimaryKey(stuSno);
    }

前端postman
在这里插入图片描述
需要通过fastjson转换json字符串为json对象从而获取相应的值,否则报错
在这里插入图片描述

  1. @RequestBody以简单对象接收前端传过来的json数据
    实体类
package com.geekmice.springbootrequestparam.pojo;


import com.fasterxml.jackson.annotation.JsonFormat;

import java.io.Serializable;
import java.util.Date;

public class Student implements Serializable {

    private String stuSno;
    private String stuName;
    private String stuBorn;
    private String stuSex;

    public String getStuSno() {
        return stuSno;
    }

    public void setStuSno(String stuSno) {
        this.stuSno = stuSno;
    }

    public String getStuName() {
        return stuName;
    }

    public void setStuName(String stuName) {
        this.stuName = stuName;
    }

    public String getStuBorn() {
        return stuBorn;
    }

    public void setStuBorn(String stuBorn) {
        this.stuBorn = stuBorn;
    }

    public String getStuSex() {
        return stuSex;
    }

    public void setStuSex(String stuSex) {
        this.stuSex = stuSex;
    }

    @Override
    public String toString() {
        return "Student{" +
                "stuSno='" + stuSno + '\'' +
                ", stuName='" + stuName + '\'' +
                ", stuBorn='" + stuBorn + '\'' +
                ", stuSex='" + stuSex + '\'' +
                '}';
    }
}

dao层

    /**
     * @param student 学生信息
     * @return 返回学生信息
     * @description 根据学生对象获取学生信息
     */
    List<Student> selectByPrimaryKeySelective(Student student);

xml

<!--获取学生信息-->
    <select id="selectByPrimaryKeySelective" resultType="student" parameterType="student">
        select
        <include refid="Base_Column_List"/>
        from student
        <where>
            <if test="stuSno != '' and stuSno != null">
                stu_sno = #{stuSno}
            </if>
            <if test="stuName != '' and stuName != null">
                and stu_name = #{stuName}
            </if>
            <if test="stuBorn != '' and stuBorn != null">
                and stu_born = #{stuBorn}
            </if>
            <if test="stuSex != '' and stuSex != null">
                and stu_sex = #{stuSex}
            </if>
        </where>
    </select>

service

    /**
     * @description 根据学生对象获取学生信息
     * @param student 学生信息
     * @return 返回学生信息
     */
    List<Student> selectByPrimaryKeySelective(Student student);
    @Override
    public List<Student> selectByPrimaryKeySelective(Student student) {
        return studentDao.selectByPrimaryKeySelective(student);
    }

controller

    /**
     * @param student 学生对象
     * @return 获取对应学生信息
     * @description 用户选择获取对应的学生信息
     */
    @PostMapping("/selectByPrimaryKeySelective")
    public List<Student> selectByPrimaryKeySelective(@RequestBody Student student) {
        return studentService.selectByPrimaryKeySelective(student);
    }

postman效果
在这里插入图片描述

  1. @RequestBody以复杂对象接收前端传过来的json数据
    复杂对象:Tim
package com.geekmice.springbootrequestparam.pojo;

import java.util.List;

/**
 * @author pmb
 * @create 2021-08-15-4:34
 */
public class Tim {
    // 团队id
    private Integer id;

    // 团队名字
    private String timName;

    // 获得荣誉
    private List<String> honors;

    // 团队成员
    private List<Student> studentList;

    public Integer getId() {
        return id;
    }

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

    public String getTimName() {
        return timName;
    }

    public void setTimName(String timName) {
        this.timName = timName;
    }

    public List<String> getHonors() {
        return honors;
    }

    public void setHonors(List<String> honors) {
        this.honors = honors;
    }

    public List<Student> getStudentList() {
        return studentList;
    }

    public void setStudentList(List<Student> studentList) {
        this.studentList = studentList;
    }

    @Override
    public String toString() {
        StringBuffer stringHonor = new StringBuffer("荣誉开始:。。。");
        for (String str : honors) {
            stringHonor.append(str);
            stringHonor.append("+");
        }
        StringBuffer stringTim = new StringBuffer("团队成员开始:。。。");
        for (Student student : studentList) {
            stringTim.append(student);
            stringTim.append("-");
        }
        stringHonor.append(stringTim);
        return stringHonor.toString();
    }
}

在这里插入图片描述
postman
在这里插入图片描述

  1. @RequestBody与简单的@RequestParam()同时使用

controller
在这里插入图片描述

postman在这里插入图片描述

  1. @RequestBody与复杂的@RequestParam()同时使用
    controller
    在这里插入图片描述

postman
在这里插入图片描述

  1. @RequestBody接收请求体中的json数据;不加注解接收URL中的数据并组装为对象
    在这里插入图片描述

在这里插入图片描述

1.4、不同之处&应用场景

我认为在单个参数提交 API 获取信息的时候,直接放在 URL 地址里,也就是使用 URI 模板的方式是非常方便的,而不使用 @PathVariable 还需要从 request 里提取指定参数,多一步操作,所以如果提取的是多个参数,而且是多个不同类型的参数,我觉得应该使用其他方式,也就是 @RequestParam

在这里插入图片描述

在这里插入图片描述

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

映射方式有两种

1、当列名和封装查询结果的类的属性名一一对应时

时MyBatis 有自动映射功能,将查询的记录封装到resultType 指定的类的对象中去

<mapper namespace="com.hanT.dao.UserDao">
    <!--id对应接口中的方法名,resultType为返回结果的类型,要用类的全限定名-->
    <select id="getUserList" resultType="com.hanT.pojo.User">
        select * from mybatis.user
    </select>
</mapper>

2、当列名和封装查询结果的类的属性名不对应时

使用resultMap 标签,在标签中配置属性名和列名的映射关系

<resultMap type="cn.com.mybatis.pojo.User" id="UserResult">
    <result property="username" column="name"/>
</resultMap>
<select id="findUserById" parameterType="java.lang.Long" resultMap="UserResult">
    select id,name,email from t_user where id=#{id}
</select>

如何返回

有了列名与属性名的映射关系后,Mybatis 通过反射创建对象(resultType有类的全路径),同时使用反射给对象的属性逐一赋值并返回, 那些找不到映射关系的属性,是无法完成赋值的。

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

where

主要是用来简化sql语句中where条件判断的,能智能的处理 and or 条件

where 1=1 

select * from t_blog where content = #{content},而不是select * from t_blog where and content = #{content},因为MyBatis会智能的把首个and or给忽略。

<!--selectDeptListById:根据主键查询部门信息-->
<select id="selectDeptListById" parameterType="com.geekmice.staging.staging.entity.DeptDO"
        resultType="com.geekmice.staging.staging.entity.DeptDO">
    select
    <include refid="all_columns"/>
    from test_dept
    where 1=1
    <if test="deptId != null and deptId != '' "> and dept_id = #{deptId}</if>
</select>

where 1=1的应用,不是什么高级的应用,也不是所谓的智能化的构造,仅仅只是为了满足多条件查询页面中不确定的各种因素而采用的一种构造一条正确能运行的动态SQL语句的一种方法。

where 1<>1 当我们只需要获取表的字段(结构)信息,而不需要理会实际保存的记录时,使用where 1<>1,系统仅会读取结构信息,而不会将具体的表记录读入内存中,节省了系统开销。

思考1:where 1=1 是否影响索引,使得索引失效

EXPLAIN SELECT * FROM test_dept WHERE 1=1 AND dept_id = 100;

image-20221121002132447

思考2:where 1<>1

SELECT * FROM test_dept WHERE 1<>1;
# 只是返回表结构
foreach

可以在SQL语句中进行迭代一个集合,item,index,collection,open,separator,close。

(1)item表示集合中每一个元素进行迭代时的别名。

(2)index指定一个名字,用于表示在迭代过程中,每次迭代到位置。

(3)open表示该语句以什么开始。

(4)separator表示在每次进行迭代之间以什么符号作为分隔符。

(5)close表示以什么结束。

(1)如果传入的是单参数参数类型是一个List的时候,collection属性值为list

(2)如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array

(3)如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key。

如果参数是数组的话

dao

/**
     * @param ids
     * @return
     * @description 批量根据主键查询部门信息
     */
List<DeptDO> selectDeptList(Integer[] ids);

xml

<select id="selectDeptList" parameterType="java.util.List" resultType="com.geekmice.staging.staging.entity.DeptDO">
    select
    <include refid="all_columns"></include>
    from test_dept
    <where>
        dept_id in
        <foreach collection="array" index="index" item="item" open="(" separator="," close=")">
            #{item}
        </foreach>
    </where>
</select>

image-20221121003854742

dao入参如果是list

    /**
     * @param ids
     * @return
     * @description 批量根据主键查询部门信息
     */
    List<DeptDO> selectDeptInfo(List<Integer> ids);

xml

    <select id="selectDeptInfo" parameterType="java.util.List" resultType="com.geekmice.staging.staging.entity.DeptDO">
        select
        <include refid="all_columns"></include>
        from test_dept
        <where>
            dept_id in
            <foreach collection="list" index="index" item="item" open="(" separator="," close=")">
                #{item}
            </foreach>
        </where>
    </select>
if

简单的条件判断,以往我们使用其他类型框架或者直接使用JDBC的时候, 如果我们要达到同样的选择效果的时候,我们就需要拼SQL语句,这是极其麻烦的,比起来,上述的动态SQL就要简单多了。

<select id="selectSelective" resultMap="BaseResultMap" parameterType="com.jack.springbootmybatis.pojo.Student">
    select
    <include refid="Base_Column_List" />
    from student
    <where>
      <if test="stuNo != null and stuNo != ''">
        stu_no=${stuNo}
      </if>
      <if test="stuName != null and stuName != ''">
        stu_name=${stuName}
      </if>
      <if test="stuBornDate != null and stuBornDate != ''">
        stu_born_date=${stuBornDate}
      </if>
      <if test="stuSex != null and stuSex != ''">
        stu_sex=${stuSex}
      </if>
    </where>
  </select>
choose

类似于java 语言中的 switchwhen元素表示当when中的条件满足的时候就输出其中的内容,跟JAVA中的switch效果差不多的是按照条件的顺序,当when中有条件满足的时候,就会跳出choose,即所有的whenotherwise条件中,只有一个会输出,当所有的我很条件都不满足的时候就输出otherwise中的内容

 <select id="selectSelectiveSex" resultMap="BaseResultMap" parameterType="com.jack.springbootmybatis.pojo.Student">
    select
    <include refid="Base_Column_List" />
    from student
    <where>
      <choose>
        <when test="stuSex != null ">
            stu_sex = #{stuSex}
        </when> 
        <when test="stuName != null ">
            stu_name = #{stuName}
        </when>
        <otherwise>
          stu_sex = "中"
        </otherwise>
      </choose>
    </where>
  </select>
set

主要用于更新时,set元素主要是用在更新操作的时候,它的主要功能和where元素其实是差不多的,主要是在包含的语句前输出一个set,然后如果包含的语句是以逗号结束的话将会把该逗号忽略,如果set包含的内容为空的话则会出错。有了set元素我们就可以动态的更新那些修改了的字段。

<update id="dynamicSetTest" parameterType="Blog">
    update t_blog
    <set>
        <if test="title != null">
            title = #{title},
        </if>
        <if test="content != null">
            content = #{content},
        </if>
        <if test="owner != null">
            owner = #{owner}
        </if>
    </set>
    where id = #{id}
</update>

19、Mybatis映射文件中,如果A标签通过include引用了B标签的内容,请问,B标签能否定义在A标签的后面,还是说必须定义在A标签的前面?

虽然说mybatis解析xml文件时按顺序解析,但是b标签的位置可以在任何地方。原理:mybatis解析a标签时,发现引用了b标签,未解析到b标签,此时会把a标签标记为未解析状态,继续解析下面内容,把剩下解析完之后,再解析标记为未解析的标签,此时已解析到b标签,a标签也就顺利解析完成。

基本使用

<select id="selectAll" resultType="com.geekmice.entity.User">
    select <include refid="all_column"/>
    from user
</select>

<sql id="all_column">
	id,
    name,
    age,
    sex
</sql>

进阶使用

<select id="selectDeptInfo" resultType="com.geekmice.staging.staging.entity.DeptDO">
    select
    <include refid="param_column">
        <property name="dept" value="t1"/>
    </include>
    from test_dept as t1
</select>

<sql id="all_columns">
    dept_id as deptId,
    dept_name as deptName
</sql>

<sql id="param_column">
    ${dept}.dept_id as deptId,
    ${dept}.dept_name as deptName
</sql>

propertyname为${}中的参数,value为数据表名,若如上存在别名,则用别名。

总之,就是把一块内容封装起来,不用每次都写,用的时候直接拿来用就可,此处需注意sql标签中的参数后逗号的问题,若引入两个内容块,则第一个最后的参数后需加逗号,以免造成SQL语句拼接错误

20、MyBatis——》转义字符(大于,小于,大于等于,小于等于)

符号小于小于等于大于大于等于单引号双引号
原符号<<=>>=&"
替换符号<<=>>=&'"

image-20211213161642675

21、MyBatis实现一对一,一对多有几种方式,怎么操作的?

准备工作

-- 员工表
CREATE TABLE `test_emp` (
  `emp_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '员工ID',
  `emp_name` varchar(255) NOT NULL COMMENT '名称',
  `emp_age` int(11) DEFAULT NULL COMMENT '年龄',
  `dept_id` int(11) NOT NULL COMMENT '部门ID',
  PRIMARY KEY (`emp_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 部门表
CREATE TABLE `test_dept` (
  `dept_id` int(11) NOT NULL COMMENT '部门ID',
  `dept_name` varchar(255) DEFAULT NULL COMMENT '部门名称',
  PRIMARY KEY (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test_emp(emp_name,emp_age,dept_id) VALUES
-- ('小红',18,100),
-- ('小明',18,101),
-- ('小花',18,102)
('小江',21,102)
;
INSERT INTO test_dept(dept_id,dept_name) VALUES
(100,'商务部'),
(101,'技术部'),
(103,'营销部')
;

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

思路二:嵌套查询是先查一个表,根据这个表里面的结果的外键id,去再另外一个表里面查询数据,也是通过配置association,collection,但另外一个表的查询通过select节点配置。

22、简述Mybatis的插件运行原理,以及如何编写一个插件

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

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

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

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

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

24、mybatis一个接口,一个xml文件,执行SQL语句如何实现

MyBatis 会先解析这些 XML 文件,通过 XML 文件里面的命名空间 (namespace)跟 DAO 建立关系;然后 XML 中的每段 SQL 会有一个id 跟 DAO 中的接口进行关联。

25、核心api

1、SqlSessionFactoryBuilder【全局的对象】

每一个MyBatis的应用程序的入口是:SqlSessionFactoryBuilder。它的作用是通过XML配置文件创建Configuration对象(当然也可以在程序中自行创建),然后通过build方法创建SqlSessionFactory对象。

2、SqlSessionFactory【全局的对象】

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory是由SqlSessionFactoryBuilder 从 XML 配置文件或通过Java的方式构建出 的实例,主要功能是创建SqlSession(会话)对象;SqlSessionFactory对象一个必要的属性是Configuration对象;SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,建议使用单例模式或者静态单例模式。一个SqlSessionFactory对应配置文件中的一个环境(environment),如果你要使用多个数据库就配置多个环境分别对应一个SqlSessionFactory。

3、 SqlSession

作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。SqlSession通过调用api的Statement ID找到对应的MappedStatement对象。SqlSession是一个接口,它有2个实现类,分别是DefaultSqlSession(默认使用)以及SqlSessionManager;默认使用DefaultSqlSession,它有两个必须配置的属性:Configuration和Executor,SqlSession通过内部存放的执行器(Executor)来对数据进行CRUD。由于不是线程安全的,所以SqlSession对象的作用域需限制方法内;每一次操作完数据库后都要调用close对其进行关闭,官方建议通过try-finally来保证总是关闭SqlSession。

4、 Executor

MyBatis执行器,是MyBatis 调度的核心。Executor对象在创建Configuration对象的时候创建,并且缓存在Configuration对象里。Executor:负责SQL语句的生成,调用StatementHandler访问数据库,查询缓存的维护;Executor(负责动态SQL的生成和查询缓存的维护)将MappedStatement对象进行解析,sql参数转化、动态sql拼接,生成jdbc Statement对象;**Executor(执行器)接口有两个实现类,其中BaseExecutor有三个继承类分别是BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statements),SimpleExecutor(普通的执行器)。

5、 StatementHandler

封装了JDBC Statement操作,负责对JDBCstatement的操作,如设置参数、将Statement结果集转换成List集合,是真正访问数据库的地方,并调用ResultSetHandler处理查询结果。

6、ResultSetHandler

ParameterHandler 负责将用户传递的参数转换成JDBC Statement 所需要的参数ResultSetHandler负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;处理查询结果。TypeHandler负责java数据类型和jdbc数据类型之间的映射和转换7、 MappedStatement :

MappedStatement就是用来存放我们SQL映射文件中的信息包括sql语句,输入参数,输出参数等等。一个SQL节点对应一个MappedStatement对象。借助MappedStatement中的结果映射关系,将返回结果转化成HashMap、JavaBean等存储结构并返回。

8、 SqlSource

负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回

BoundSql表示动态生成的SQL语句以及相应的参数信息ConfigurationMyBatis所有的配置信息都维持在Configuration对象之中

26、对于日期判断非空不能使用如下操作

startDate != ‘’

<if test="startDate != null || startDate != '' "></if>

27、返回对象数据时候,对象字段必须为包装类型

28、单双引号问题

注意单引号在外面,双引号在里面;

正常情况下用字符串放在单引号里面没有问题,但如果是用==来做判断单个字符时,单引号必须改为双引号,否则会被转化为字符char格式与字符串String格式进行比较,自然就无法匹配通过。

<if test="filterMobile !=null ">
    <if test='filterMobile=="1"'>
        and length(t_user_arch.mobile_tel) = 11
    </if>
    <if test='filterMobile=="0"'>
        and length(t_user_arch.mobile_tel)  &lt;&gt; 11
    </if>
</if>

29、处理日期作为参数传递

4.1 当不指定jdbcType时,日期会自动转化会MySQL的timestamp
4.2 指定jdbcType=TIMESTAMP,日期会自动转化会MySQL的timestamp
4.3 指定jdbcType=DATE,那么MyBatis会将传入参数截取为2018-07-24(Date)

当指定jdbcType=DATE的时候,MyBatis会自动截取掉时间,如果MySQL的日期字段类型是datetime或者timestamp一定不要这么写

java.util.Date实际上是能够表示MySQL的三种字段类型

3.1 date
3.2 datetime
3.3 timestamp

30、对于where标签和 where 1=1

,还会存在SQL 注入的风险,where标签自动去除 and,or关键字,以及有返回值情况才会使用where。

31、mybatis实现批量操作

1、foreach标签

<!-- 这种方式需要数据库连接属性allowMutiQueries=true的支持
 如jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true -->  
<insert id="addEmpsBatch">
    <foreach collection="emps" item="emp" separator=";">                                 
        INSERT INTO emp(ename,gender,email,did)
        VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})
    </foreach>
</insert>

2、ExecutorType.BATCH

Mybatis内置的ExecutorType一共有三种,默认为SIMPLE,该模式下它为每个语句的执行创建一个新的预处理语句,并单条提交sql。

BATCH模式会重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优;
注意: batch模式也有自己的问题,比如在Insert操作时,在事务没有提交之前,是没有办法获取到自增的id,这在某型情形下是不符合业务要求的。

//批量保存方法测试
@Test  
public void testBatch() throws IOException{
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    //可以执行批量操作的sqlSession
    SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);

    //批量保存执行前时间
    long start = System.currentTimeMillis();
    try {
        EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
        for (int i = 0; i < 1000; i++) {
            mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1"));
        }

        openSession.commit();
        long end = System.currentTimeMillis();
        //批量保存执行后的时间
        System.out.println("执行时长" + (end - start));
        //批量 预编译sql一次==》设置参数==》10000次==》执行1次   677
        //非批量  (预编译=设置参数=执行 )==》10000次   1121

    } finally {
        openSession.close();
    }
}

32、一级缓存

一级缓存命中场景

运行时参数设置

  • 同一个回话sqlsession
  • sql语句,参数一致
  • 相同的statementid
  • rowbounds相同(分页)

操作与配置

  • 未手动清空缓存
  • 未配置flushCache=true
  • 未执行update
  • 缓存作用域不是statement
一级缓存源码解析

image-20220920072201170

每个SqlSession中持有了Executor,每个Executor中有一个LocalCache,当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,则直接返回结果给用户,如果没有名字,则去查询数据库,结果写入Local Cache,最后返回结果给用户

在我们一级缓存的源码主要是对BaseExecutor内部实现的学习。
BaseExecutor:BaseExecutor是一个实现了Executor接口的抽象类,里面定义了若干个抽象方法,执行的时候,是把具体的操作委托给子类进行执行。

protected abstract int doUpdate(MappedStatement var1, Object var2) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement var1, Object var2,
                                       RowBounds var3, ResultHandler var4, BoundSql var5) throws SQLException;

我们开始说一级缓存的Local Cache的查询和写入是在Executor内部完成的,看到BaseExecutor的代码后发现Local Cache 是BaseExecutor内部的一个成员变量。看如下代码:

  protected ConcurrentLinkedQueue<BaseExecutor.DeferredLoad> deferredLoads;
  protected PerpetualCache localCache;

Cache:Mybatis中的Cache接口,提供了与缓存相关的基本操作。如下代码:

public interface Cache {
    String getId();

    int getSize();

    void putObject(Object var1, Object var2);

    Object getObject(Object var1);

    Object removeObject(Object var1);

    void clear();

    ReadWriteLock getReadWriteLock();
}

Cache也有若干个实现类,使用装饰器模式互相组装,提供丰富的操控缓存的能力,部分如下:

在这里插入图片描述

BaseExecutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,即内部持有HashMap,对一级缓存的操作实则是对HashMap的操作,如下代码:

 private String id;
 private Map<Object, Object> cache = new HashMap();

为执行和数据库的交互,首先我们要初始化SqlSession,通过DefaultSqlSessionFactory开启SqlSession:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    ............
    final Executor executor = configuration.newExecutor(tx, execType);     
    return new DefaultSqlSession(configuration, executor, autoCommit);
}

在初始化SqlSession时,会使用Configuration类创建一个全新的Executor,作为DefaultSqlession构造函数的参数,创建Executor代码如下:

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);
    }
    // 尤其可以注意这里,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);                      
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

SqlSession创建完毕后,根据Statment的不同类型,会进入SqlSession的不同方法中,如果是Select语句的话,最后会执行到SqlSession的SelectList,代码如下:

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}

SqlSession把具体的查询职责委托给了Executor,如果只开启了一级缓存的话,首先会进入BaseExecutor的query方法,代码如下:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

上述代码中,会先根据传入的参数生成CacheKey,进入该方法查看CacheKey是如何生成的,代码如下:

CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//后面是update了sql中带的参数
cacheKey.update(value);

在上述的代码中,将MappedStatement的id,SQL的offset、SQL的limit,SQL本身以及SQL中的参数传入了CacheKEey这个类,最终构成CacheKey,下面是这个类的内部结构:

private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;

private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;

public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<Object>();
}

首先是成员变量和构造函数,有一个初始的hashcode和乘数,同时维护了一个内部的updateList。在Cachekey的update方法中,会进行一个hashcode和checksum的计算,同事把传入的参数添加进updatelist,如下代码:

public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    
    updateList.add(object);
}

同时重写了CacheKey的equals方法,代码如下:

@Override
public boolean equals(Object object) {
    .............
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
}

除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,就可以认为时CacheKey相等。只要下面五个值相同,级可以认为时相同的SQL。

StatementId+Offset+Limmit+Sql+Params

BaseExecutor的query方法继续往下走,代码如下:

list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
    // 这个主要是处理存储过程用的。
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。
在query方法执行的最后,会判断一级缓存级别是不是statement级别的,如果是的话,就清空缓存,这也是statement级别的一级缓存无法共享localCache的原因,代

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
}

最后,我们确认如果是insert/delete/update方法,缓存就会被刷新。
SqlSession的insert方法和delete方法,都会统一走update的流程,

@Override
public int insert(String statement, Object parameter) {
    return update(statement, parameter);
  }
   @Override
  public int delete(String statement) {
    return update(statement, null);
}

update方法也是委托给了Executor执行。BaseExecutor的执行方法如下所示:

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
}

每次执行update前都会清空localCache,到此一级缓存的工作流程结束。

一级缓存失效

img
1、Mybatis一级缓存的生命周期和SqlSession一致。
2、Mybatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
3、Mybatis的一级缓存最大范围SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

总结

每个SqlSession中持有了Executor,每个Executor中有一个LocalCache,当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,则直接返回结果给用户,如果没有名字,则去查询数据库,结果写入Local Cache,最后返回结果给用户

33、二级缓存

image-20220924212210764

责任链模式

命中条件

1、回话提交

2、sql语句,参数相同

3、相同的statementid

4、rowbounds相同

二级缓存为何只有SqlSession关闭或者事务提交时才生效

image-20220924232942759

注意二级缓存是从 MappedStatement 中获取的。由于 MappedStatement 存在于全局配置中,可以多个 CachingExecutor 获取到,这样就会出现线程安全问题;跨线程访问,多个回话同时访问,会话一与会话二原本是两条隔离的事务,但由于二级缓存的存在导致彼此可见性会发生脏读。若会话二的修改直接填充到二级缓存,会话一查询时缓存中存在即直接返回数据,此时会话二回滚会话一读到的数据就是脏数据。

执行流程

image-20220924235811805

作用范围

一级缓存作用域是SqlSession级别,所以它存储的SqlSession中的BaseExecutor之中,但是二级缓存目的就是要实现作用范围更广,那肯定是要实现跨会话共享的,在MyBatis中二级缓存的作用域是namespace,也就是作用范围是同一个命名空间,所以很显然二级缓存是需要存储在SqlSession之外的,那么二级缓存应该存储在哪里合适呢?

在MyBatis中为了实现二级缓存,专门用了一个装饰器来维护,这就是我们上一篇文章介绍Executor时还留下的没有介绍的一个对象:CachingExecutor。

如何开启

二级缓存配置有三个地方

1、mybatis_config.xml中有一个全局配置属性,这个不用配置也可以,因为默认是true

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

2、mapper映射文件配置缓存标签

<cache/>
或者
<cache-ref namespace="com.geekmice.dao.UserMapper"></cache-ref>

3、在查询语句标签配置useCache

<select id="getUserById" resultMap="JobResultMap" useCache="true">
	select * from user
</select>

注意:第一点默认开启,也就是说只要配置第二点就可以打开二级缓存,而第三点是我们需要针对于某一条语句配置二级缓存使用的

原理分析

上面我们提到二级缓存是通过CachingExecutor对象来实现的,那么就让我们先来看看这个对象:

public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
    
  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
}

我们看到CachingExecutor中只有2个属性,第1个属性不用说了,因为CachingExecutor本身就是Executor的包装器,所以属性TransactionalCacheManager肯定就是用来管理二级缓存的,我们再进去看看TransactionalCacheManager对象是如何管理缓存的:

image-20220925003834370

TransactionalCacheManager内部非常简单,也是维护了一个HashMap来存储缓存。
HashMap中的value是一个TransactionalCache对象,继承了Cache。

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  // 二级缓存对象
  private final Cache delegate;
  // 表示提交事务清除缓存标志
  private boolean clearOnCommit;
  // 这里记录临时缓存,当commit的时候将其添加二级缓存
  private final Map<Object, Object> entriesToAddOnCommit;
  // 未命中缓存的key值  
  private final Set<Object> entriesMissedInCache;
}

注意上面有一个属性是临时存储二级缓存的,为什么要有这个属性,我们下面会解释。

二级缓存的创建和使用
我们在读取mybatis-config全局配置文件的时候会根据我们配置的Executor类型来创建对应的三种Executor中的一种,然后如果我们开启了二级缓存之后,只要开启(全局配置文件中配置为true)就会使用CachingExecutor来对我们的三种基本Executor进行包装,即使Mapper.xml映射文件没有开启也会进行包装。

接下来我们看看CachingExecutor中的query方法:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 创建一级缓存key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
    // 获取二级缓存
    Cache cache = ms.getCache();
    // 全局配置文件默认开启,而加入Mapper.xml映射文件没有开启二级缓存,这里就是null值
    if (cache != null) {
        // select标签是否配置flushCache属性
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            // 确保没有输出参数,因为二级缓存不能缓存输出类型参数
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            // 获取二级缓存
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 如果二级缓存没有获取到,则回去执行原来基础Executor中query方法,也就是去读一级缓存
                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);
}

上面方法大致经过如下流程:

1、创建一级缓存的CacheKey
2、获取二级缓存
3、如果没有获取到二级缓存则执行被包装的Executor对象中的query方法,此时会走一级缓存中的流程。
4、查询到结果之后将结果进行缓存。
需要注意的是在事务提交之前,并不会真正存储到二级缓存,而是先存储到一个临时属性,等事务提交之后才会真正存储到二级缓存。这么做的目的就是防止脏读。因为假如你在一个事务中修改了数据,然后去查询,这时候直接缓存了,那么假如事务回滚了呢?所以这里会先临时存储一下。
所以我们看一下commit方法:

  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit(); // 这里会存储临时缓存的值真正刷新到二级缓存内
  }
如何进行包装

在这里插入图片描述

1、PerpetualCache:第一层缓存,这个是缓存的唯一实现类,肯定需要。
2、LruCache:二级缓存淘汰机制之一。因为我们配置的默认机制,而默认就是LRU算法淘汰机制。淘汰机制总共有4中,我们可以自己进行手动配置。
3、SerializedCache:序列化缓存。这就是为什么开启了默认二级缓存我们的结果集对象需要实现序列化接口。
4、LoggingCache:日志缓存。
5、SynchronizedCache:同步缓存机制。这个是为了保证多线程机制下的线程安全性。

总结

当我们的sql执行的时候,先去二级缓存namespace中查看是否存在缓存,
然后如果二级缓存不存在,查看当前sqlSession中一级缓存中是否存在,
最后一、二级缓存中都不存在的话那么就去数据库查询,
接着会将查询出来的结果保存在我们的一级缓存当中,
当前会话(SqlSession)结束,就会将一级缓存中的数据,同步到我们的二级缓存

34、插件原理

四大插件彼此关系

image-20220925010444421

基于插件实现自动分页
需求考虑

易用性:不需要额外配置,参数中带有Page即可,page尽可能简单

不对使用场景作假设:不限制用户使用方式,比如接口调用还是会话调用,不能影响缓存业务

友好性:不符合分页情况,友好提示

环境搭建

mybatis配置文件

<?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"/>

    <settings>
        <!--二级缓存-->
        <setting name="cacheEnabled" value="true"/>
        <!--打印日志-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <plugins>
        <plugin interceptor="com.geekmice.mvnmybatis.utils.PageInterceptor"/>
    </plugins>

    <!-- 7.environments数据库环境配置 -->
    <!-- 和Spring整合后environments配置将被废除 -->
    <environments default="development">
        <environment id="development">
            <!-- 使用JDBC事务管理 -->
            <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>

    <!-- 8.加载映射文件 -->
    <mappers>
        <mapper resource="mapper/UserDao.xml"/>
    </mappers>
</configuration>

数据库连接信息资源文件

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://192.*.244.8:3306/school?useSSL=false&useUnicode=true&characterEncoding=utf-8
jdbc.username=root
jdbc.password=root

mybatis映射文件对应xml

<?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.geekmice.mvnmybatis.dao.UserDAO">
    <select id="getUserByPage" resultType="com.geekmice.mvnmybatis.bo.UserDO">
        select
        user_name as userName,
        birthday,
        sex,
        address,
        id
        from user
    </select>
</mapper>

数据层DAO

package com.geekmice.mvnmybatis.dao;

import com.geekmice.mvnmybatis.bo.UserDO;
import com.geekmice.mvnmybatis.utils.Page;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface UserDAO {
    /**
     * @param page
     * @return
     * @Description 带有分页查询
     */
    List<UserDO> getUserByPage(Page page);
}

相关的实体类DO

package com.geekmice.mvnmybatis.bo;


import java.io.Serializable;
import java.util.Date;

public class UserDO implements Serializable {
    private static final long serialVersionUID = 6320941908222932112L ;

    private String userName;
    private Date birthday;
    private String sex;
    private String address;
    private String id;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getId() {
        return id;
    }

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

    @Override
    public String toString() {
        return "UserBO{" +
                "userName='" + userName + '\'' +
                ", birthday=" + birthday +
                ", sex='" + sex + '\'' +
                ", address='" + address + '\'' +
                ", id='" + id + '\'' +
                '}';
    }
}

分页对象Page

package com.geekmice.mvnmybatis.utils;

import com.fasterxml.jackson.annotation.JsonProperty;

import java.io.Serializable;

public class Page implements Serializable {
    /**
     * @Description 每页显示数量
     */
    @JsonProperty("per_page")
    private int pageSize;
    /**
     * @Description 当前页码
     */
    @JsonProperty("current_page")
    private int curPage;
    /**
     * @Description 总页数
     */
    @JsonProperty("total_pages")
    private int pages;
    /**
     * @Description 总记录数
     */
    private int total;
    /**
     * @Description 当前页数量
     */
    private int count;

    /**
     * @Description 自动统计分页总数
     */
    private boolean autoCount;

    /**
     * @Description 默认无参构造器,初始化各值
     */
    public Page() {
        this.pageSize = 10;
        this.curPage = 1;
        this.pages = 0;
        this.total = 0;
        this.count = 0;
        this.autoCount = true;
    }

    public Page(Page page) {
        this.pageSize = page.pageSize;
        this.curPage = page.curPage;
        this.pages = page.pages;
        this.total = page.total;
        this.count = page.count;
        this.autoCount = page.autoCount;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public int getCurPage() {
        return curPage;
    }

    public void setCurPage(int curPage) {
        this.curPage = curPage;
    }

    public int getPages() {
        return pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    public int getTotal() {
        return total;
    }

    public void setTotal(int total) {
        this.total = total;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public boolean isAutoCount() {
        return autoCount;
    }

    public void setAutoCount(boolean autoCount) {
        this.autoCount = autoCount;
    }

    public void calculate(int total) {
        this.setTotal(total);
        this.pages = (total / pageSize) + ((total % pageSize) > 0 ? 1 : 0);
        // 如果当前页码超出总页数,自动更改为最后一页
        //this.curPage = this.curPage > pages ? this.pages : this.curPage;
        if (curPage > pages) {
            throw new IllegalStateException("超出查询范围");
        }
    }

    /**
     * @return 分页起始位置和偏移量数组
     * @Description 获取分页起始位置和偏移量
     */
    public int[] paginate() {
        // 数量为零时,直接从0开始
        return new int[]{total > 0 ? (curPage - 1) * pageSize : 0, pageSize};
    }
}

分页拦截器Interceptor

package com.geekmice.mvnmybatis.utils;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 分页插件拦截处理
        useMetaObject(invocation);
        return invocation.proceed();
    }

    private void useMetaObject(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        // 调用MetaObject 反射类处理
        // 分离代理对象链
        while (metaObject.hasGetter("h")) {
            Object obj = metaObject.getValue("h");
            metaObject = SystemMetaObject.forObject(obj);
        }
        while (metaObject.hasGetter("target")) {
            Object obj = metaObject.getValue("target");
            metaObject = SystemMetaObject.forObject(obj);
        }
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        // 存在分页标识
        Page page = getPage(boundSql);
        if (Objects.nonNull(page)) {
            int total = getTotalSize(statementHandler, (Connection) invocation.getArgs()[0]);
            if (total <= 0) {
                // 返回数量小于零,查询一个简单的sql,不去执行明细查询 【基于反射,重新设置boundSql】
                metaObject.setValue("delegate.boundSql.sql", "select * from (select 0 as id) as temp where  id>0");
                metaObject.setValue("delegate.boundSql.parameterMappings", Collections.emptyList());
                metaObject.setValue("delegate.boundSql.parameterObject", null);
            } else {
                page.calculate(total);
                String sql = boundSql.getSql() + " limit " + (page.getCurPage() - 1) * page.getPageSize() + ", " + page.getPageSize();
                metaObject.setValue("delegate.boundSql.sql", sql);
            }
        }
    }

    /***
     * 获取分页的对象
     * @param boundSql 执行sql对象
     * @return 分页对象
     */
    private Page getPage(BoundSql boundSql) {
        Object obj = boundSql.getParameterObject();
        if (Objects.isNull(obj)) {
            return null;
        }
        Page page = null;
        if (obj instanceof Page) {
            page = (Page) obj;
        } else if (obj instanceof Map) {
            // 如果Dao中有多个参数,则分页的注解参数名必须是page
            try {
                page = (Page) ((Map) obj).get("page");
            } catch (Exception e) {
                return null;
            }
        }
        // 不存在分页对象,则忽略下面的分页逻辑
        if (Objects.nonNull(page) && page.isAutoCount()) {
            return page;
        }
        return null;
    }

    /***
     * @Description 获取统计sql
     * @param originalSql 原始sql
     * @return 返回统计加工的sql
     */
    private String getCountSql(String originalSql) {
        // 统一转换为小写
        originalSql = originalSql.trim().toLowerCase();
        // 判断是否存在 limit 标识
        boolean limitExist = originalSql.contains("limit");
        if (limitExist) {
            originalSql = originalSql.substring(0, originalSql.indexOf("limit"));
        }
        boolean distinctExist = originalSql.contains("distinct");
        boolean groupExist = originalSql.contains("group by");
        if (distinctExist || groupExist) {
            return "select count(1) from (" + originalSql + ") temp_count";
        }
        // 去掉 order by
        boolean orderExist = originalSql.contains("order by");
        if (orderExist) {
            originalSql = originalSql.substring(0, originalSql.indexOf("order by"));
        }
        // todo   left join还可以考虑优化
        int indexFrom = originalSql.indexOf("from");
        return "select count(*)  " + originalSql.substring(indexFrom);
    }

    /**
     * @Description 查询总记录数
     *
     * @param statementHandler mybatis sql 对象
     * @param conn             链接信息
     */
    private int getTotalSize(StatementHandler statementHandler, Connection conn) {
        ParameterHandler parameterHandler = statementHandler.getParameterHandler();
        String countSql = getCountSql(statementHandler.getBoundSql().getSql());
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = (PreparedStatement) conn.prepareStatement(countSql);
            parameterHandler.setParameters(pstmt);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                // 设置总记录数
                return rs.getInt(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (pstmt != null) {
                    pstmt.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return 0;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

相关的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.geekmice</groupId>
    <artifactId>mvnmybatis</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.9</version>
        </dependency>
        <!--        @JsonProperty 此注解用于属性上,作用是把该属性的名称序列化为另外一个名称,如把trueName属性序列化为name,@JsonProperty("name")。-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.9</version>
        </dependency>
    </dependencies>
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>


</project>

单元测试

package com.geekmice.mvnmybatis.test;

import com.geekmice.mvnmybatis.bo.UserDO;
import com.geekmice.mvnmybatis.dao.UserDAO;
import com.geekmice.mvnmybatis.utils.MybatisUtils;
import com.geekmice.mvnmybatis.utils.Page;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;

import java.io.IOException;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.List;


public class PluginTest {
    private static Configuration configuration;
    private static SqlSessionFactory factory;

    static {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        try {
            factory = factoryBuilder.build(Resources.getResourceAsStream("mybatis_config.xml"));
            configuration = factory.getConfiguration();
            configuration.setLazyLoadTriggerMethods(new HashSet<>());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void t1() {
        UserDAO mapper = factory.openSession().getMapper(UserDAO.class);
        Page page = new Page();
        page.setAutoCount(true);
        page.setCurPage(1);
        page.setPageSize(3);
        List<UserDO> userByPage = mapper.getUserByPage(page);
        if (page.getTotal() == 0) {
            System.out.println("空空如也");
        } else {
            System.out.println(userByPage);
        }
    }
}

image-20220925143058080

底层实现

在项目启动的时候判断组件是否有被拦截,如果没有直接返回原对象。
如果有被拦截,返回动态代理的对象(Plugin)。
执行到的组件的中的方法时,如果不是代理对象,直接执行原方法 如果是代理对象,执行Plugin的invoke()方法。

在这里插入图片描述

然插件需要在配置文件中进行配置,那么肯定就需要进行解析,我们看看插件式如何被解析的。我们进入XMLConfigBuilder类看看

  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 获取拦截器
        String interceptor = child.getStringAttribute("interceptor");
        // 获取properties属性
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
       // 将属性添加到Interceptor对象
        interceptorInstance.setProperties(properties);
        // 添加到配置类的InterceptorChain属性,InterceptorChain类维护了一个List<Interceptor>
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

解析出来之后会将插件存入InterceptorChain对象的list属性

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
}

看到InterceptorChain我们是不是可以联想到,MyBatis的插件就是通过责任链模式实现的

拦截executor对象,我们知道,SqlSession对象是通过openSession()方法返回的,而Executor又是属于SqlSession内部对象,所以让我们跟随openSession方法去看一下Executor对象的初始化过程

在这里插入图片描述

可以看到,当初始化完成Executor之后,会调用interceptorChain的pluginAll方法,pluginAll方法本身非常简单,就是把我们存到list中的插件进行循环,并调用Interceptor对象的plugin方法:

在这里插入图片描述

/**
 * @author Clinton Begin
 */
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
  }

}
到这里我们是不是发现很熟悉,没错,这就是我们上面示例中重写的方法,而plugin方法是接口中的一个默认方法。

这个方法是关键

在这里插入图片描述

可以看到这个方法的逻辑也很简单,但是需要注意的是MyBatis插件是通过JDK动态代理来实现的,而JDK动态代理的条件就是被代理对象必须要有接口,这一点和Spring中不太一样,Spring中是如果有接口就采用JDK动态代理,没有接口就是用CGLIB动态代理。

正因为MyBatis的插件只使用了JDK动态代理,所以我们上面才强调了一定要实现Interceptor接口。
而代理之后汇之星Plugin的invoke方法,我们最后再来看看invoke方法

在这里插入图片描述

而最终执行的intercept方法,就是我们上面示例中重写的方法。

35、mybatis工作流程

image-20220925002812145

image-20220924210720131

(1)读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
(2)加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
(3)构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
(4)创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
(5)Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
(6)MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
(7)输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
(8)输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。

36、Mapper.xml 映射文件解析全流程

  private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    Class<?> typeClass = resolveClass(type);
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
    resultMappings.addAll(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
      if ("constructor".equals(resultChild.getName())) {
        processConstructorElement(resultChild, typeClass, resultMappings);
      } else if ("discriminator".equals(resultChild.getName())) {
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } else {
        List<ResultFlag> flags = new ArrayList<ResultFlag>();
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    // 创建 ResultMapResolver 对象
    // ResultMapResolver会根据上面解析到的ResultMappings集合以及<resultMap>标签的属性构造 ResultMap 对象
    // 并将其添加到 Configuration.resultMaps 集合(StrictMap 类型)中
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }

37、mybatis-config.xml 解析全流程

下面我们正式开始介绍 MyBatis 的初始化过程(工作过程、工作原理)。

MyBatis 初始化的第一个步骤就是加载和解析 mybatis-config.xml 这个全局配置文件,入口是 XMLConfigBuilder 这个 Builder 对象,它由 SqlSessionFactoryBuilder.build() 方法创建。XMLConfigBuilder 会解析 mybatis-config.xml 配置文件得到对应的 Configuration 全局配置对象,然后 SqlSessionFactoryBuilder 会根据得到的 Configuration 全局配置对象创建一个 DefaultSqlSessionFactory 对象返回给上层使用。

在 SqlSessionFactoryBuilder.build() 方法中也可以看到,XMLConfigBuilder.parse() 方法触发了 mybatis-config.xml 配置文件的解析,其中的 parseConfiguration() 方法定义了解析 mybatis-config.xml 配置文件的完整流程,核心步骤如下:

解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签。

image-20220927123440513

 @Test
 public void t1() throws IOException {
     // 加载mybatis配置文件,目的是为了构建SqlSessionFactory
     InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
     // 创建SqlSessionFactory对象
     // 开始解析mybatis配置文件
     SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream); 
     // 通过SqlSessionFactory工厂对象创建SqlSession对象
     SqlSession sqlSession = factory.openSession();
     // 通过SqlSession获取代理对象
     UserMapper mapper = sqlSession.getMapper(UserMapper.class);
     HdzPageHelper.startPage(12, 1);
     List<UserDO> userDoList = mapper.listUser();
     for (UserDO item : userDoList) {
         System.out.println(item);
     }

 }
// 调用build方法
public SqlSessionFactory build(InputStream inputStream) {
 return build(inputStream, null, null);
}

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.
   }
 }
}
public Configuration parse() {
 if (parsed) {
   throw new BuilderException("Each XMLConfigBuilder can only be used once.");
 }
 parsed = true;
 parseConfiguration(parser.evalNode("/configuration"));
 return configuration;
}
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);
 }
}



这里创建的 XMLConfigBuilder 对象的核心功能就是解析 mybatis-config.xml 配置文件。XMLConfigBuilder 有一部分能力继承自 BaseBuilder 抽象类,具体继承关系如下图所示:

configuration(Configuration 类型):MyBatis 的初始化过程就是围绕 Configuration 对象展开的,我们可以认为 Configuration 是一个单例对象,MyBatis 初始化解析到的全部配置信息都会记录到 Configuration 对象中。
typeAliasRegistry(TypeAliasRegistry 类型):别名注册中心。
typeHandlerRegistry(TypeHandlerRegistry 类型):TypeHandler 注册中心。除了定义别名之外,我们在 mybatis-config.xml 配置文件中,还可以使用 标签添加自定义 TypeHandler 实现,实现数据库类型与 Java 类型的自定义转换,这些自定义的 TypeHandler 都会记录在这个 TypeHandlerRegistry 对象中。

resolveAlias() 方法 :解析别名,核心逻辑是在中实现的,主要依赖于 TypeAliasRegistry 对象
resolveTypeHandler() 方法:解析 TypeHandler,主要依赖于 TypeHandlerRegistry 对象。

public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;

protected Class<?> resolveAlias(String alias) {
 return typeAliasRegistry.resolveAlias(alias);
}

protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, String typeHandlerAlias) {
 if (typeHandlerAlias == null) {
   return null;
 }
 Class<?> type = resolveClass(typeHandlerAlias);
 if (type != null && !TypeHandler.class.isAssignableFrom(type)) {
   throw new BuilderException("Type " + type.getName() + " is not a valid TypeHandler because it does not implement TypeHandler interface");
 }
 @SuppressWarnings( "unchecked" ) // already verified it is a TypeHandler
 Class<? extends TypeHandler<?>> typeHandlerType = (Class<? extends TypeHandler<?>>) type;
 return resolveTypeHandler(javaType, typeHandlerType);
}

protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
 if (typeHandlerType == null) {
   return null;
 }
 // javaType ignored for injected handlers see issue #746 for full detail
 TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
 if (handler == null) {
   // not in registry, create a new one
   handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType);
 }
 return handler;
}
}

我们回来看 XMLConfigBuilder 这个 Builder 实现类,看看它是如何解析 mybatis-config.xml 配置文件的。

parsed(boolean 类型):状态标识字段,记录当前 XMLConfigBuilder 对象是否已经成功解析完 mybatis-config.xml 配置文件。
parser(XPathParser 类型):XPathParser 对象是一个 XML 解析器,这里的 parser 对象就是用来解析 mybatis-config.xml 配置文件的。
environment(String 类型): 标签定义的环境名称。
localReflectorFactory(ReflectorFactory 类型):ReflectorFactory 接口的核心功能是实现对 Reflector 对象的创建和缓存。

public class XMLConfigBuilder extends BaseBuilder {
private boolean parsed;
private final XPathParser parser;
private String environment;
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();

public Configuration parse() {
 if (parsed) {
   throw new BuilderException("Each XMLConfigBuilder can only be used once.");
 }
 parsed = true;
 parseConfiguration(parser.evalNode("/configuration"));
 return configuration;
}

private void parseConfiguration(XNode root) {
 try {
   //issue #117 read properties first
   propertiesElement(root.evalNode("properties"));
   Properties settings = settingsAsProperties(root.evalNode("settings"));
   loadCustomVfs(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);
 }
}
}

我们可以看到 parseConfiguration(XNode root) 就硬核处理,简单介绍几个常用的

处理标签:从 标签中解析出来的 KV 信息会被记录到一个 Properties 对象(也就是 Configuration 全局配置对象的 variables 字段),在后续解析其他标签的时候,MyBatis 会使用这个 Properties 对象中记录的 KV 信息替换匹配的占位符。

处理标签:是否使用二级缓存、是否开启懒加载功能等,这些都是通过 mybatis-config.xml 配置文件中的 标签进行配置的。XMLConfigBuilder.settingsAsProperties() 方法的核心逻辑就是解析 标签,并将解析得到的配置信息记录到 Configuration 这个全局配置对象的同名属性中。

处理标签: 标签中会指定 Mapper.xml 映射文件的位置,通过解析 标签,MyBatis 就能够知道去哪里加载这些 Mapper.xml 文件了。mapperElement() 方法就是 XMLConfigBuilder 处理 标签的具体实现,其中会初始化 XMLMapperBuilder 对象来加载各个 Mapper.xml 映射文件。同时,还会扫描 Mapper 映射文件相应的 Mapper 接口,处理其中的注解并将 Mapper 接口注册到 MapperRegistry 中。

38、出现Mapped Statements collection already contains value for 的报错

1、mapper中存在id重复的值。

2、mapper中的parameterType或resultType为空。

3、在使用@Select等注解的情况下,方法名即为mapper的id,重载的方法会报这个错。

4、mapper复制 忘了改namespace指向的类,所以两个mapper指向同一个mapper,所以报了这个错。

39、Mapped Statements collection does not contain value for 解决方法

1、mapper文件扫描

2、daoimpl文件指定的方法位置xml正确

3、斜杠分隔不是逗号

image-20221119184920565

4、接收和返回的对象是否一致

5、dao接口方法名和xml方法名是否一致

6、xml命名空间是否一致

  • 6
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值