MyBatis学习总结 + 【手写MyBatis底层机制核心】

MyBatis笔记

MyBatis介绍

MyBatis 是一个持久层框架

  1. 前身是ibatis, 在ibatis3.x 时,更名为MyBatis
  2. MyBatis 在java 和sql 之间提供更灵活的映射方案
  3. mybatis 可以将对数据表的操作(sql,方法)等等直接剥离,写到xml 配置文件,实现和java
    代码的解耦
  4. mybatis 通过SQL 操作DB, 建库建表的工作需要程序员完成

相关文档

MyBatis 中文手册、

https://mybatis.org/mybatis-3/zh/index.html
https://mybatis.net.cn/

为什么需要MyBatis?

  1. 传统jdbc连接数据库需要自己编写,不统一标准
  2. 程序不是OOP的方式编写的
  3. SQL语句是写在程序中,属于硬编码,没有解耦
  4. mybatis 可以将对数据表的操作(sql,方法)等等直接剥离,写到xml 配置文件,实现和java
    代码的解耦
  5. MyBatis 是一个持久层框架所以它统一
  6. MyBatis 是OOP方式操作数据库

MyBatis案例

实体类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Monk {
    private Integer id;
    private String nickname;
    private String skill;
    private String grade;
    private Double salary;
    private String birthTime;
    private Date entry;
}

mapper接口

  1. 只是一个接口
  2. 该接口用于声明操作monster表的方法
  3. 这些方法可以通过注解或者xml文件实现
public interface MonkMapper {

    void addMonk(Monk monk);
}

mapper.xml

  1. 这是一个mapper xml 文件
  2. 该文件可以去实现对应的接口的方法
  3. namespace 指定该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.code_study.mapper.MonkMapper">

    <!--    配置addMonk
        1. id="addMonk" 就是接口的方法名
        2. parameterType="com.code_study.entity.Monk" 放入的形参
        3. parameterType="com.code_study.entity.Monk" 可以简写
        4. (#{nickname},#{skill},#{grade},#{salary},#{birthTime},#{entry})是从传入的monk对象属性来的
        5. #{nickname}对应 monk对象的  private String nickname;
	-->
    
    <insert id="addMonk" parameterType="Monk">
        INSERT INTO `monk`
            (`nickname`,`skill`,`grade`,`salary`,`birthTime`,`entry`)
        VALUES(#{nickname},#{skill},#{grade},#{salary},#{birthTime},#{entry})
    </insert>

</mapper>

原生的API和注解的方式

Mybatis原生的API调用

添加

 @Test
    public void myBatisNativeCrud(){
        Monk monk = new Monk();

        monk.setBirthTime("1999-9-9 10:11:02");
        Calendar instance = Calendar.getInstance();

        instance.set(Calendar.YEAR,2020);
        instance.set(Calendar.MONTH,Calendar.OCTOBER);
        instance.set(Calendar.DAY_OF_MONTH,15);
        Date time = instance.getTime();
        monk.setEntry(time);
        monk.setGrade("大宗师");
        monk.setNickname("法海");
        monk.setSalary(200.5);
        monk.setSkill("大威天龙");

        int insert = sqlSession.insert("com.code_study.mapper.MonkMapper.addMonk", monk);
        System.out.println("insert~~" + insert);
        System.out.println("操作成功");
   }

删除

  	@Test
    public void myBatisNativeCrud(){
 		int delete = sqlSession.delete("com.code_study.mapper.MonkMapper.deleteMonk", 6);
        System.out.println("delete~~" + delete);
        System.out.println("操作成功");
      }

修改

@Test
    public void myBatisNativeCrud(){
 		 Monk monk = new Monk();

        monk.setBirthTime("1999-9-9 10:11:02");
        Calendar instance = Calendar.getInstance();

        instance.set(Calendar.YEAR,2020);
        instance.set(Calendar.MONTH,Calendar.OCTOBER);
        instance.set(Calendar.DAY_OF_MONTH,15);
        Date time = instance.getTime();
        monk.setEntry(time);
        monk.setGrade("大宗师");
        monk.setNickname("法海");
        monk.setSalary(200.5);
        monk.setSkill("乾坤大挪移");
        monk.setId(8);
        int update = sqlSession.update("com.code_study.mapper.MonkMapper.updateMonk", monk);
        System.out.println("update~~" + update);
        System.out.println("操作成功");
      }

查询

@Test
    public void myBatisNativeCrud(){
		List<Monk> monks = 
            sqlSession.selectList("com.code_study.mapper.MonkMapper.findAllMonk");
        for (Monk monk : monks) {
            System.out.println("monk= "+ monk);
        }

        if (sqlSession != null){
            sqlSession.commit();
            sqlSession.close();

        }
    }

Mybatis注解的方式操作

添加

  /*
    useGeneratedKeys = true : 表示可以返回自增长的值
    keyProperty = "id"      : 表示 自增值对应对象的哪个属性
    keyColumn = "id"        : 表示 自增值对应表的哪个字段
    如果 keyProperty = "id" 和  keyColumn = "id"  一致,可以只保留 keyProperty = "id"
     */
    @Insert(value = "INSERT INTO `monk` " +
            "            (`nickname`,`skill`,`grade`,`salary`,`birthTime`,`entry`) " +
            "        VALUES(#{nickname},#{skill},#{grade},#{salary},#{birthTime},#{entry})")
    @Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
    void addMonk(Monk monk);

删除

  @Delete(value = "  DELETE FROM `monk` WHERE id = #{id}")
    void deleteMonk(Integer id);

修改

 @Update(value = " UPDATE `monk` " +
            "        SET `nickname`=#{nickname}, " +
            "            `skill`=#{skill}, " +
            "            `grade`=#{grade}, " +
            "            `salary`=#{salary}, " +
            "            `birthTime`=#{birthTime}, " +
            "            `entry`=#{entry} " +
            "         WHERE id = #{id}")
    void updateMonk(Monk monk);

查询单个

 //查询-根据id
    @Select(value = "  select * from `monk` where id = #{id}")
    Monk getMonkById(Integer id);

查询多个

//查询所有的Monster
@Select(value = "    select * from `monk`")
List<Monk> findAllMonk();

mybatis-config.xml配置文件详解

基本介绍

  • mybatis 的核心配置文件(mybatis-config.xml),比如配置jdbc 连接信息,注册mapper
    等等

properties 属性

  • 通过该属性,可以指定一个外部的jdbc.properties 文件,引入我们的jdbc 连接信息
  <!--引入 外部 jdbc.properties-->
    <properties resource="jdbc.properties"/>

settings 全局参数定义

  • 是MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。

typeAliases 别名处理器

  1. 别名是为Java 类型命名一个短名字。它只和XML 配置有关,用来减少类名重复的部分

  2. 如果指定了别名,我们的MappperXxxx.xml 文件就可以做相应的简化处理

  3. 注意指定别名后,还是可以使用全名的

  4. 可以直接指向包,这样包下的所有类都是取类名作为别名

<typeAliases>
    <!--<typeAlias type="com.code_study.entity.Monk" alias="Monk"/>-->
    <package name="com.code_study.entity"/>
 </typeAliases>

typeHandlers 类型处理器

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

  2. 用于java 类型和jdbc 类型映射

  3. Mybatis 的映射基本已经满足,不太需要重新定义

  4. 这个我们使用默认即可,也就是mybatis 会自动的将java 和jdbc 类型进行转换.

environments 环境

resource 注册Mapper 文件:
<!--配置需要管理的 Mapper.xml文件注意路径resource="" 是斜杠-->
<mappers>
    <mapper resource="com/code_study/mapper/MonsterMapper.xml"/>
</mappers>
class:接口注解实现
<mappers>
	<mapper class="com.code_study.mapper.MonkAnnotation"/>
</mappers>
url:外部路径,使用很少,不推荐
<mappers>
	<mapper url="file:///D:\yy\kk\yy\MOnsterMapper.xml" />
</mappers>
package 方式注册
<mappers>
	<package name="com.cody_study.mapper"/>
</mappers>

XxxMapper.xml SQL映射文件

基本介绍

​ MyBatis 的真正强大在于它的语句映射(在XxxMapper.xml 配置), 由于它的异常强大, 如
果拿它跟具有相同功能的JDBC 代码进行对比,你会立即发现省掉了将近95% 的代码。
MyBatis 致力于减少使用成本,让用户能更专注于SQL 代码。

SQL 映射文件常用的几个顶级元素

  1. cache – 该命名空间的缓存配置。
  2. cache-ref – 引用其它命名空间的缓存配置。
  3. resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  4. parameterType - 将会传入这条语句的参数的类全限定名或别名
  5. sql – 可被其它语句引用的可重用语句块。
  6. insert – 映射插入语句。
  7. update – 映射更新语句。
  8. delete – 映射删除语句。
  9. select – 映射查询语句。

parameterType(输入参数类型)

  1. 传入简单类型,比如按照id 查Monster
  2. 传入POJO 类型,查询时需要有多个筛选条件
  3. 当有多个条件时,传入的参数就是Pojo 类型的Java 对象
  4. 当传入的参数类是String 时,也可以使用${} 来接收参数
<!--形参是 String 用 ${ } 不再用 #{ }-->
<!--实现 public List<Monk> findMonsterByName(String name);-->
<select id="findMonsterByName" parameterType="String" resultType="Monk">
    SELECT * FROM  `monk` WHERE `nickname`  LIKE '%${name}%'
</select>
  • 传入的是List 时,resultType=“T”类型
<!--    实现   public List<Monster> findMonsterByNameORId(Monster monster); -->
<select id="findMonsterByNameORId" parameterType="Monk" resultType="Monk">
    SELECT * FROM `monk` WHERE `id` = #{id} OR `nickname` = #{nickname}
</select>

传入HashMap

  • HashMap 传入参数更加灵活,比如可以灵活的增加查询的属性,而不受限于Monster 这
    个Pojo 属性本身
  • parameterType="map"
<select id="findMonsterByIdAndSalary_PrameterHashMap" parameterType="map" resultType="Monk">
    SELECT * FROM  `monk` WHERE `id` > #{id} AND `salary` > #{salary}
</select>

传入和返回HashMap

  • parameterType="map"
  • resultType="map"
  <select id="findMonkByIdAndSalary_ParameterHashMap_ReturnHashMap"
            parameterType="map" resultType="map">
        SELECT * FROM  `monk` WHERE `id` > #{id} AND `salary` > #{salary}
    </select>

resultMap(结果集映射)

基本介绍

​ 当实体类的属性和表的字段名字不一致时,我们可以通过resultMap 进行映射,从而屏蔽
实体类属性名和表的字段名的不同.(作用)

案例

public interface UserMapper {

    //查询所有的User
    public List<User> findAllUser();

}
<!--
说明:
    resultMap:表示 定义一个 resultMap
    id="findAllUserMap" :表示 程序员指定的  resultMap  id,之后通过id可以使用它
    type="User": 表示需要返回的对象类型
    <result column="user_email" property="useremail"/> :表示 表字段 user_email 和 对象属性 useremail 之间的映射关系
-->
<!--查询所有的User-->
<!--public List<User> findAllUser();-->
<resultMap id="findAllUserMap" type="User">
    <result column="user_email" property="useremail"/>
    <result column="user_name" property="username"/>
</resultMap>
<!--使用的是resultMap属性而不是resultType或其他属性。
resultMap="findAllUserMap" 表示 使用我们定义的 resultMap , 通过id="findAllUserMap" 关联
-->

<select id="findAllUser" resultMap="findAllUserMap" >
   SELECT * FROM `user`
</select>

动态SQL语句

基本介绍

  1. 在一个实际的项目中,sql 语句往往是比较复杂的
  2. 为了满足更加复杂的业务需求,MyBatis 的设计者,提供了动态生成SQL 的功能。
  3. 动态SQL 是MyBatis 的强大特性之一
  4. 使用JDBC 或其它类似的框架,根据不同条件拼接SQL 语句非常麻烦,例如拼接时要
  5. 确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号等
  6. SQL 映射语句中的强大的动态SQL 语言, 可以很好的解决这个问题.
  7. 最重要的是解决一些普通的SQL无法处理的带有复杂逻辑的语句

动态SQL 常用标签

动态SQL 提供了如下几种常用的标签,类似我们Java 的控制语句:

  1. if [判断]
  2. where [拼接where 子句]
  3. choose/when/otherwise [类似java 的switch 语句, 注意是单分支]
  4. foreach [类似in ]
  5. trim [替换关键字/定制元素的功能]
  6. set [在update 的set 中,可以保证进入set 标签的属性被修改,而没有进入set 的,保
    持原来的值]

if [判断]

<!--    配置方法       public List<Monk> findMonkByAage(Integer age);-->
    <select id="findMonkById" parameterType="Integer" resultType="Monk">
        SELECT * FROM `monk` WHERE 1 = 1
        <if test="id >= 0">
            AND  id > #{id}
        </if>
    </select>

where [拼接where 子句]

<!--    如果入参是对象,test表达式中 直接使用属性名即可  例如: test="id>=0" -->
<!--    where标签 会在组织动态sql时,加上where,并去掉多余的 AND-->
    <select id="findMonkByIdAndName" parameterType="Monk" resultType="Monk">
        SELECT * FROM `monk`
        <where>

            <if test="id>=0">
                AND id > #{id}
            </if>

            <if test="nickname != null and nickname != ''">
                AND `nickname` = #{nic kname}
            </if>
        </where>
    </select>

choose/when/otherwise [类似java 的switch 语句, 注意是单分支]

<!--    配置方法         List<Monk> findMonsterByIdAndName_choose(Map<String, Object> map);-->
<select id="findMonkByIdOrName_choose" parameterType="map" resultType="Monk">
    select * from `monk`
    <choose >
        <when test="nickname != null and nickname !='' ">
            where nickname = #{nickname}
        </when>
        <when test="id > 0 ">
            where id = #{id}
        </when>
        <otherwise>
            where salary > 10000
        </otherwise>
    </choose>
</select>

foreach [类似in ]

   <select id="findMonkById_forEach" parameterType="map" resultType="Monk">
        SELECT * FROM `monk`
        <if test="ids != null and ids != ''">
            <where>
                id IN
                <foreach collection="ids" item="id" open="(" separator="," close=")">
                    #{id}
                </foreach>
            </where>
        </if>
    </select>

trim [替换关键字/定制元素的功能]

  <!--    配置方法        List<Monk> findMonkByName_Trim(Map<String, Object> map);-->
    <select id="findMonkByName_Trim" parameterType="map" resultType="Monk">
        SELECT * FROM `monk`
        <trim prefix="WHERE" prefixOverrides="AND|OR|ZY">

            <if test="id>=0">
                ZY id > #{id}
            </if>

            <if test="nickname != null and nickname != ''">
                AND `nickname` = #{nickname}
            </if>
        </trim>
    </select>

set [在update 的set 中,可以保证进入set 标签的属性被修改,而没有进入set 的,保
持原来的值]

<!--    配置方法         void updateMonster_set(Map<String, Object> map);-->
    <update id="updateMonster_set" parameterType="map">
        UPDATE `monk`
        <set>
            <if test="grade != null and grade != ''">
                `grade` = #{grade},
            </if>
            <if test="skill != null and skill != ''">
                `skill` = #{skill},
            </if>
            <if test="nickname != null and nickname != ''">
                `nickname` = #{nickname},
            </if>
            <if test="grade != null and grade != ''">
                `grade` = #{grade},
            </if>
            <if test="salary != null and salary != ''">
                `salary` = #{salary},
            </if>
            <if test="birthTime != null and birthTime != ''">
                `birthTime` = #{birthTime},
            </if>
            <if test="entry != null and entry != ''">
                `entry` = #{entry},
            </if>
        </set>
        WHERE id = #{id}
    </update>

映射关系一对一

基本介绍

  • 项目中1 对1 的关系是一个基本的映射关系,比如:Person(人) --- IDCard(身份证

映射方式

  1. 通过配置XxxMapper.xml 实现1 对1 [配置方式]

  2. 通过注解的方式实现1 对1 [注解方式]

映射方式一:配置Mapper.xml实现

  • 通过配置XxxMapper.xml 的方式,实现级联查询,通过person 可以获取到对应的idencard 信息

接口:

public interface PersonMapper {

    //通过Person 的id 获取到Person,包括这个Person 关联的IdenCard 对象
    Person getPersonById(Integer id);

    //通过Person 的id 获取到Person,包括这个Person 管理的IdenCard 对象,方式2
    Person getPersonById2(Integer id);

    //通过Person 的id 获取到Person
    Person getPerson(Integer id);

}
public interface IdenCardMapper {

    //根据id 获取到身份证序列号
    IdenCard getIdenCardById(Integer id);


    //通过IdenCard 的id 获取到Person
    IdenCard getIdenCardAndPerson(Integer id);
}

mapper.xml:

通过person 可以获取到对应的idencard 信息

方式一 :多表联查
<!-- 通过Person 的id 获取到Person,包括这个Person 关联的IdenCard 对象-->
<resultMap id="PersonResultMap" type="Person">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <!--
    property="card" 表示 person对象的card属性
    javaType="IdenCard" 表示 card属性 的类型
     column="id" 是这条语句
                          select *
                        from `person` join `idencard` on person.card_id = idencard.id
                        where person.id = #{id}
                 查询返回的字段
    -->
    <association property="card" javaType="IdenCard">
        <!--标记出作为id的结果 可以帮助提高整体性能-->
        <!--property="id"表示 person的属性 id  ;column="id" 表示 对应表的字段 id ,通常是主键-->
        <id property="id" column="id"/>
        <result property="card_sn" column="card_sn"/>
    </association>
</resultMap>

<select id="getPersonById" parameterType="Integer" resultMap="PersonResultMap">
    select *
    from `person` join `idencard` on person.card_id = idencard.id
    where person.id = #{id}
</select>
方式二:分解成单表操作
<resultMap id="PersonResultMap2" type="Person">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <association property="card" column="card_id"
                 select="com.code_study.mapper.IdenCardMapper.getIdenCardById"/>
<!--  column="card_id"是 下面 SELECT * FROM `person` where `id` = #{id} 返回的字段
card_id 会作为getIdenCardById 入参,执行
 -->
</resultMap>
<select id="getPersonById2" parameterType="Integer" resultMap="PersonResultMap2">
    SELECT * FROM `person` where `id` = #{id}
</select>

映射方式二:注解实现

public interface PersonMapperAnnotation {

    @Select(value = "SELECT * FROM `person` where `id` = #{id}")
    @Results({
            @Result(id = true,property = "id",column = "id"),
            @Result(property = "name",column = "name"),
            @Result(property = "card",column = "card_id",
                    one = @One(select = "com.code_study.mapper.IdenCardMapperAnnotation.getIdenCardById"))
    })
    Person getPersonById(Integer id);
}

映射关系多对一

基本介绍

  1. 项目中多对1 的关系是一个基本的映射关系, 多对1, 也可以理解成是1 对多.
  2. User --- Pet: 一个用户可以养多只宠物
  3. Dep ---Emp : 一个部门可以有多个员工

映射方式

  1. 通过配置XxxMapper.xml 实现1 对1 [配置方式]

  2. 通过注解的方式实现1 对1 [注解方式]

映射方式一:配置Mapper.xml实现

接口:

public interface UserMapper {
    //通过id 获取User 对象
    public User getUserById(Integer id);
}
public interface PetMapper {

    //通过User 的id 来获取pet 对象,可能有多个,因此使用List 接收
    public List<Pet> getPetByUserId(Integer userId);
    //通过pet 的id 获取Pet 对象
    public Pet getPetById(Integer id);

}

mapper.xml

<mapper namespace="com.code_study.mapper.PetMapper">

<!--通过User 的id 来获取pet 对象,可能有多个,因此使用List 接收
    public List<Pet> getPetByUserId(Integer userId);
-->
    <resultMap id="PetResultMap" type="Pet">
        <id property="id" column="id"/>
        <result property="nickname" column="nickname"/>
       <association property="user" column="user_id"
                    select="com.code_study.mapper.UserMapper.getUserById"/>
    </resultMap>
    <select id="getPetByUserId" parameterType="Integer" resultMap="PetResultMap">
        SELECT * FROM `mybatis_pet` WHERE user_id = #{userId}
    </select>



    <!--通过pet 的id 获取Pet 对象-->
    <!--public Pet getPetById(Integer id);-->
    <select id="getPetById" parameterType="Integer" resultMap="PetResultMap">
        select * from `mybatis_pet` where id = #{id}
    </select>
</mapper>
<mapper namespace="com.code_study.mapper.UserMapper">


<!--ofType="Pet" : 表示 返回的集合中存放的数据类型是 Pet-->
<resultMap id="PetsResultMap" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <collection property="pets" column="id"
                ofType="Pet" select="com.code_study.mapper.PetMapper.getPetByUserId" />
</resultMap>
<select id="getUserById" parameterType="Integer" resultMap="PetsResultMap">
    SELECT * from `mybatis_user` WHERE id = #{id}
</select>

</mapper>
说明:
  1. 多对一时,resultMap中 collection 标签表示集合
  2. 在 collection 标签中 属性 ofType 表示集合中存放的数据类型
  3. 双向多对一时,toString应该去掉,避免栈溢出
  4. resultMap中 select 标签表示复用这个目标方法,推荐使用,这样提高代码的复用性,将多表拆解成单表

映射方式二:注解实现

public interface UserMapperAnnotation {

    //通过id 获取User 对象

   
    @Select(value = "SELECT * from `mybatis_user` WHERE id = #{id}")
    @Results({
            @Result(id = true,property = "id",column = "id"),
            @Result(property = "name",column = "name"),
            @Result(property = "pets",column = "id",
                    many = @Many(select = "com.code_study.mapper.PetMapperAnnotation.getPetByUserId"))
    })
    public User getUserById(Integer id);
}
public interface PetMapperAnnotation {

    //通过User 的id 来获取pet 对象,可能有多个,因此使用List 接收
    @Select(value = "SELECT * FROM `mybatis_pet` WHERE user_id = #{userId}")
    @Results(id = "PetResultMap",value = {
            @Result(id = true,property = "id",column = "id"),
            @Result(property = "nickname",column = "nickname"),
            @Result(property = "user",column = "user_id",
                    one = @One(select = "com.code_study.mapper.UserMapperAnnotation.getUserById"))
    })
    List<Pet> getPetByUserId(Integer userId);


    //通过pet 的id 获取Pet 对象
    @Select(value = " select * from `mybatis_pet` where id = #{id}")
    @ResultMap(value = "PetResultMap")
    Pet getPetById(Integer id);
}
  • @Results(id = "PetResultMap"...)可以给@Results设置一个 id 用来复用代码
  • 在下面就进行了@Results 的复用
 //通过pet 的id 获取Pet 对象
   	@Select(value = " select * from `mybatis_pet` where id = #{id}")
 	@ResultMap(value = "PetResultMap")
    Pet getPetById(Integer id);

MyBatis缓存

基本介绍

  • MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。

相关文档

一级缓存

  1. 默认情况下,mybatis 是启用一级缓存的/本地缓存/local Cache,它是SqlSession 级别的。

  2. 同一个SqlSession 接口对象调用了相同的select 语句,会直接从缓存里面获取,而不是再
    去查询数据库

一级缓存原理图

一级缓存失效的情况

  1. 关闭sqlSession 会话后, 再次查询,会到数据库查询
  2. 当执行sqlSession.clearCache() 会使一级缓存失效
  3. 当对同一个monster 修改,该对象在一级缓存会失效

二级缓存

  1. 二级缓存和一级缓存都是为了提高检索效率的技术
  2. 最大的区别就是作用域的范围不一样,一级缓存的作用域是sqlSession 会话级别,在一次
    会话有效,而二级缓存作用域是全局范围,针对不同的会话都有效

二级缓存原理图

二级缓存配置

​ 1.在mybatis-config.xml中 全局性的开启缓存



​ 2.使用二级缓存时entity 类实现序列化接口(serializable),因为二级缓存可能使用到序列化技术

​ *大部分情况不需要这样配置,只是某些第三方缓存库需要序列化到磁盘!

​ 3.再对应的XxxMapper.xml中设置二级缓存的策略

<mapper namespace="com.code_study.mapper.MonsterMapper">
    <cache
            eviction="FIFO"
            flushInterval="60000"
            size="512"
            readOnly="true"/>
 
    <!--查询-根据id-->
    <select id="getMonsterById" resultType="Monster">
        SELECT * FROM `monster` WHERE id = #{id}
    </select>
</mapper>

注意事项

  • 表示创建了FIFO 的策略,每隔30 秒刷新一次,最多存放360 个对象而且返回的对象被认为是只读的。

  • eviction:缓存的回收策略

  • flushInterval:时间间隔,单位是毫秒,

  • size:引用数目,内存大就多配置点,要记住你缓存的对象数目和你运行环境的可用内存

  • 资源数目。默认值是1024

  • readOnly:true,只读

四大策略

  1. LRU – 最近最少使用:移除最长时间不被使用的对象。
  2. FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  3. SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  4. WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象

如何禁用二级缓存

方式一
<settings>
    <!--全局性的开启缓存-->
    <setting name="cacheEnabled" value="false"/>
</settings>
方式二

在XxxMapper.xml文件中不配做

方式三

直接在XxxMapper.xml文件对应的方法上 设置属性 useCache=false

   <!--查询所有的Monster-->
    <select id="findAllMonster" resultType="Monster" useCache="false">
        SELECT * FROM `monster`
    </select>

一级缓存和二级缓存的执行顺序

  • 缓存执行顺序是:二级缓存-->一级缓存-->数据库
  • 当我们关闭了一级缓存的时候,如果配置了二级缓存,那么一级缓存的数据会放入二级缓存中
  • 不会出现一级缓存和二级缓存中有同一个数据。因为二级缓存(数据)是在一级缓存关闭之后才有的

第三方缓存框架---EhCache

基本介绍

  1. EhCache 是一个纯Java 的缓存框架,具有快速、精干等特点
  2. MyBatis 有自己默认的二级缓存(前面我们已经使用过了),但是在实际项目中,往往使用
    的是更加专业的第三方缓存产品作为MyBatis 的二级缓存,EhCache 就是非常优秀的缓存
    产品

配置使用第三方缓存框架

添加依赖
<dependencies>
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache-core</artifactId>
        <version>2.6.11</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.25</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-ehcache</artifactId>
        <version>1.2.1</version>
    </dependency>
在XxxMapper.xml文件中启用EhCache
<mapper namespace="com.code_study.mapper.MonsterMapper">
 
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    
    <!--查询-根据id-->
    <select id="getMonsterById" resultType="Monster">
        SELECT * FROM `monster` WHERE id = #{id}
    </select>
</mapper>

如何理解EhCache 和MyBatis 缓存的关系

  1. MyBatis 提供了一个接口Cacheorg.apache.ibatis.cache.Cache

  2. 只要实现了该Cache 接口,就可以作为二级缓存产品和MyBatis 整合使用,Ehcache 就
    是实现了该接口

  3. MyBatis 默认情况(即一级缓存)是使用的PerpetualCache 类实现Cache 接口的,是核心类

  4. 当我们使用了Ehcahce 后,就是EhcacheCache 类实现Cache 接口的,是核心类.

  5. 缓存的本质就是Map<Object,Object>

手写MyBatis底层机制

读取配置文件,得到数据库连接

思路

  1. 引入必要的依赖
  2. 需要写一个自己的config.xml文件,在里面配置一些信息,driver,url ,password,username
  3. 需要编写Configuration类,对 自己的config.xml文件 进行解析,得到一个数据库连接

实现

  • 引入必要的依赖
<dependencies>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
  • 需要写一个自己的config.xml文件,在里面配置一些信息,driver,url ,password,username
<?xml version="1.0" encoding="UTF-8" ?>
<database>
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/hsp_mybatis?
        useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
    <property name="username" value="root"/>
    <property name="password" value="zy"/>
</database>
  • 需要编写Configuration类,对 自己的config.xml文件 进行解析,得到一个数据库连接
public class ZyConfiguration {

    //属性 类加载器
    private ClassLoader classLoader =
            ClassLoader.getSystemClassLoader();

    //读取xml文件信息并处理
    public Connection build(String resource) {
        Connection connection = null;
        
        //加载配置文件,获取对应的InputStream流
        InputStream resourceAsStream =
                classLoader.getResourceAsStream(resource);

        //解析xml文件
        SAXReader reader = new SAXReader();
        try {
            Document document = reader.read(resourceAsStream);
            Element root = document.getRootElement();

            //解析rootElement
            System.out.println("root= "+root);
            return evalDataSource(root);

        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }

    }


    //解析xml文件 并返回一个连接
    private Connection evalDataSource(Element node) {
        Iterator property = node.elementIterator("property");
        String driverClassName = null;
        String url = null;
        String username = null;
        String password = null;

        //遍历node子节点 获取属性值
        while(property.hasNext()){
            Element pro = (Element)property.next();
            String name = pro.attributeValue("name");
            String value = pro.attributeValue("value");

            //判断是否得到了name 和 value
            if (name == null || value == null){
                throw new RuntimeException("property 节点没有设置name 或 value属性");
            }
            switch (name){
                case "driverClassName":
                    driverClassName = value;
                    break;
                case "url":
                    url = value;
                    break;
                case "username":
                    username = value;
                    break;
                case "password":
                    password = value;
                    break;
                default:
                    throw new RuntimeException("属性名没有匹配到");
            }

        }
        Connection connection = null;
        try {
            Class.forName(driverClassName);
            connection = DriverManager.getConnection(url, username, password);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return connection;
    }

}

编写执行器,输入SQL语句,完成操作

思路

  1. 需要写一个实体类,对应monster表
  2. 编写接口executor
  3. 实现接口,编写自己的执行器
  4. 需要一个 自己的Configuration类 返回连接,通过连接对数据库进行操作

实现

  • 需要写一个实体类,对应monster表
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Monster {
    private Integer id;
    private Integer age;
    private String name;
    private String email;
    private Date birthday;
    private double salary;
    private Integer gender;
}
  • 编写接口executor
public interface Executor {
    public <T> T query(String statement,Object parameter);
}

  • 实现接口,编写自己的执行器
public class ZyExecutor implements Executor{

    private ZyConfiguration zyConfiguration = new ZyConfiguration();

    @Override
    public <T> T query(String sql, Object parameter) {
        Connection connection = getConnection();

        //查询返回的结果集
        ResultSet set = null;
        PreparedStatement pre = null;

        try {
            pre = connection.prepareStatement(sql);
            //设置参数,如果参数多,用数组处理
            pre.setString(1, parameter.toString());
            set = pre.executeQuery();

            //把set数据封装到对象 -- monster
            Monster monster = new Monster();//简化处理 认为返回的结果就是一个monster记录

            //遍历结果集
            while(set.next()){
                monster.setId(set.getInt("id"));
                monster.setName(set.getString("name"));
                monster.setEmail(set.getString("email"));
                monster.setAge(set.getInt("age"));
                monster.setGender(set.getInt("gender"));
                monster.setBirthday(set.getDate("birthday"));
                monster.setSalary(set.getDouble("salary"));
            }
            return (T)monster;

        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                if (set != null) {
                    set.close();
                }
                if (pre != null) {
                    pre.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        
    }

    public Connection getConnection(){//Configuration类 返回连接,通过连接对数据库进行操作
       return zyConfiguration.build("zy_mybatis.xml");
    }
}
  • 需要一个 自己的Configuration类 返回连接,通过连接对数据库进行操作

将Sqlsession封装到执行器

思路

  1. 需要写自己的Sqlsession类,它是搭建连接和执行器之间的桥梁,里面封装有 执行器 和 配置文件 以及 操作DB 的具体方法
  2. 写一个selectOne方法 ,SelectOne() 返回一条记录,一条记录对应一个Monster对象

实现

  • 需要写自己的Sqlsession类,它是搭建连接和执行器之间的桥梁,里面封装有 执行器 和 配置文件 以及 操作DB 的具体方法
public class ZySqlSession {//搭建连接和执行器之间的桥梁

    //执行器
    private Executor executor = new ZyExecutor();

    //配置
    private ZyConfiguration zyConfiguration = new ZyConfiguration();

    //操作DB 的具体方法
    //SelectOne 返回一条记录-对象
    public <T> T selectOne(String statement,Object parameter){
        return executor.query(statement,parameter);
    }
}
  • 写一个selectOne方法 ,SelectOne() 返回一条记录,一条记录对应一个Monster对象
//操作DB 的具体方法
    //SelectOne 返回一条记录-对象
    public <T> T selectOne(String statement,Object parameter){
        return executor.query(statement,parameter);
    }

开发Mapper接口和Mapper.xml

思路

  1. 编写MonsterMapper接口,里面有方法getMonsterById(Integer id)根据id返回一个monster对象
  2. 在resources下编写对应的monsterMapper.xml(简化:因为在resources 编译时会在类路径下比较好写)
  3. monsterMapper.xml 编写具体的sql语句,并指定语句类型,id,resultType(和原生Mybatis一样)

实现

  • 编写MonsterMapper接口,里面有方法getMonsterById(Integer id)根据id返回一个monster对象
public interface MonsterMapper {
    public Monster getMonsterById(Integer id);
}
  • 在resources下编写对应的monsterMapper.xml(简化:因为在resources 编译时会在类路径下比较好写)
  • monsterMapper.xml 编写具体的sql语句,并指定语句类型,id,resultType(和原生Mybatis一样)
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.code_study.mapper.MonsterMapper">
    <!--    实现配置接口方法 getMonsterById-->
    <select id="getMonsterById" resultType="com.code_study.entity.Monster">
        SELECT * FROM monster WHERE id = ?
    </select>
</mapper>

开发MapperBean,可以和Mapper接口相映射

思路

  1. 开发 Function类 ,用于记录对应的Mapper的方法信息,比如sql类型,方法名,执行的sql语句,返回类型,入参类型
  2. 开发 MapperBean类,记录接口信息和接口下的所有方法
  3. Function类 对应 monsterMapper.xml中的信息
  4. MapperBean类 对应 MonsterMapper 接口中的信息

实现

  • 开发 Function类 ,用于记录对应的Mapper的方法信息,比如sql类型,方法名,执行的sql语句,返回类型,入参类型
//对应 monsterMapper.xml中的信息
public class Function {
    private String sqlType;//sql类型,比如select,insert,update,delete
    private String funcName;//方法名
    private String sql;//执行的sql语句
    private Object resultType;//返回类型
    private String parameterType;//入参类型
}
  • 开发 MapperBean类,记录接口信息和接口下的所有方法
//对应 MonsterMapper 接口中的信息
public class MapperBean {
    private String interfaceName;//接口名

    //    接口下的所有方法
    private List<Function> functions;
}
  • Function类 对应 monsterMapper.xml中的信息
  • MapperBean类 对应 MonsterMapper 接口中的信息

在Configuration中解析MapperXML获取MapperBean对象

思路

  1. 在Configuration 添加方法readMapper(String path)
  2. 通过 path 读取接口对应的Mapper方法
  3. 保存接口下所有的方法信息
  4. 封装成 MapperBean对象

实现

  • 在Configuration 添加方法readMapper(String path)
  • 通过 path 读取接口对应的Mapper方法
  • 保存接口下所有的方法信息
  • 封装成 MapperBean对象
 //解析MapperXML获取MapperBean对象
    //path = xml的路径+文件名 是从类的加载路径计算的(如果放在resource目录下 之间传xml文件名即可)
    public MapperBean readMapper(String path) {
        MapperBean mapperBean = new MapperBean();

        InputStream resourceAsStream = classLoader.getResourceAsStream(path);
        SAXReader reader = new SAXReader();
        try {
            Document document = reader.read(resourceAsStream);
            Element root = document.getRootElement();

            String namespace = root.attributeValue("namespace");
            mapperBean.setInterfaceName(namespace);

            List<Function> list = new ArrayList<>();//保存接口下所有的方法信息

            //得到root的迭代器
            Iterator iterator = root.elementIterator();
            while(iterator.hasNext()){
                Element e = (Element)iterator.next();
                String sqlType = e.getName().trim();
                String sql = e.getText().trim();
                String funcName = e.attributeValue("id");
                String resultType = e.attributeValue("resultType");

                //ResultType 返回的是一个Object对象 ->反射
                Object instance = Class.forName(resultType).newInstance();

                //封装 function 对象
                Function function = new Function();
                function.setSql(sql);
                function.setSqlType(sqlType);
                function.setFuncName(funcName);
                function.setResultType(instance);

                //将封装好的function对象 放入 list中
                list.add(function);


                mapperBean.setFunctions(list);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return mapperBean;
    }

动态代理Mapper方法

思路

  1. 在SqlSession中添加方法 getMapper 输入一个Class类型,返回mapper的动态代理对象
  2. 编写动态代理类 实现 InvocationHandler 接口
  3. 取出mapperBean的functions 遍历
  4. 判断 当前要执行的方法和function.getFunctionName是否一致
  5. 调用方法返回 动态代理对象
  6. 编写SqlSessionFactory 会话工厂,可以返回SqlSession

实现

  • 编写动态代理类 实现 InvocationHandler 接口

  • 在SqlSession中添加方法 getMapper 输入一个Class类型,返回mapper的动态代理对象

 //返回mapper的动态代理对象
    public <T> T getMapper(Class<T> clazz){
        return (T) Proxy.newProxyInstance(
                clazz.getClassLoader(),
                new Class[]{clazz},
                new ZyMapperProxy(zyConfiguration,clazz,this));
    }
  • 取出mapperBean的functions 遍历
  • 判断 当前要执行的方法和function.getFunctionName是否一致
  • 调用方法返回 动态代理对象
public class ZyMapperProxy implements InvocationHandler {
    private ZySqlSession zySqlSession;
    private String mapperFile;
    private ZyConfiguration zyConfiguration;

    public ZyMapperProxy(ZySqlSession zySqlSession, Class clazz, ZyConfiguration zyConfiguration) {
        this.zySqlSession = zySqlSession;
        this.zyConfiguration = zyConfiguration;
        this.mapperFile = clazz.getSimpleName() + ".xml";
    }

    //当执行Mapper接口的代理对象方法时,会执行到invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MapperBean mapperBean = zyConfiguration.readMapper(this.mapperFile);

        //判断是否为当前xml文件对应的接口
        if (!method.getDeclaringClass().getName().equals(mapperBean.getInterfaceName())){
            return null;
        }

        //取出mapperBean的functions
        List<Function> functions = mapperBean.getFunctions();
        //判断当前的mapperBean 解析对应的MapperXML后,有方法
        if (null != functions || 0 != functions.size()){
            for (Function function : functions) {
                //当前要执行的方法和function.getFunctionName
                if (method.getName().equals(function.getFuncName())){
                    if ("SELECT".equalsIgnoreCase(function.getSqlType())){
                        return zySqlSession.selectOne(function.getSql(),String.valueOf(args[0]));
                    }
                }
            }
        }

        return null;
    }
}
  • 编写SqlSessionFactory 会话工厂,可以返回SqlSession
public class ZySqlSessionFactory {
    
    public static ZySqlSession open(){
        return new ZySqlSession();
    }
}

测试

@Test
public void openSession(){
    ZySqlSession zySqlSession = ZySqlSessionFactory.openSession();
    System.out.println("zySqlSession= "+zySqlSession);
    MonsterMapper mapper = zySqlSession.getMapper(MonsterMapper.class);
    Monster monster = mapper.getMonsterById(1);
    System.out.println("monster= "+monster);
}

本文学习内容来自韩顺平老师的课程

仅供个人参考学习

posted @ 2024-05-06 15:38  zy2596  阅读(121)  评论(0编辑  收藏  举报