Mybatis 高级应用,将使用自己手写的mybatis改造为使用真正的mybatis

源码(码云):https://gitee.com/yin_zhipeng/implement_mybatis_of_myself.git

一、手写Mybatis

篇幅限制,我将其放在这篇文章中:https://blog.csdn.net/grd_java/article/details/122894621

二、Mybatis高级应用

1. 基本概念

ORM(Object/Relation Mapping,对象/关系数据库映射)
  1. 完成面向对象的编程语言到关系数据库的映射。当ORM框架完成映射后,程序员就可以利用面向对象的简单易用性和关系型数据库的技术优势完成对数据库的操作
  2. ORM把关系型数据库包装成面向对象的模型。是面向对象语言和关系型数据库的中间解决方案
  3. 使用ORM框架,应用程序不再直接访问底层数据库,而是以面向对象的方式操作持久化对象,ORM框架将面向对象操作转换成底层SQL操作
  4. 效果:把持久化对象的保存、修改、删除等操作,装换为对数据库的操作
Mybatis
  1. 优秀的半自动化轻量级ORM框架,持久层框架,支持定制化SQL、存储过程以及高级映射
  2. 避免了几乎所有JDBC代码和手动设置参数以及获取结果集。Mybatis可以使用XML或注解来配置和映射原生类型、接口和Java的POJO(Plain Old Java Objects 普通老式Java对象)为数据库中记录
发展历史
  1. 原是Apache的开源项目iBatis。2010nian6月由Apache Software Foundation迁移到了Google Code,随着开发团队转投Google Code旗下,iBatis3.x正式更名为MyBatis,代码与2013年11月迁移到Github
MyBatis优势
  1. 半自动化持久层框架。对开发人员而言,核心Sql需要自己优化,sql和java编码解耦合,分开编写,功能边界清晰,一个专注业务,一个专注数据
    在这里插入图片描述

2. 环境说明

  1. 复制上面使用手写mybatis的模块,用真正的mybatis改造
    在这里插入图片描述
  2. 依赖
    在这里插入图片描述
  3. xml配置文件,xml映射基本一样,核心配置文件有些不同,它更完善,它可以针对不同环境,采用不同的配置,并用一个< mappers>标签来当所有配置映射mapper.xml文件路径的父标签
  1. mapper.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.yzpnb.mapper.AccountMapper">

    <select id="selectList" resultType="com.yzpnb.entity.Account">
        select * from account
    </select>
    <select id="selectOne" resultType="com.yzpnb.entity.Account" parameterType="com.yzpnb.entity.Account">
        select * from account where id = #{id}
    </select>
</mapper>
  1. 核心配置文件(下图有一错误:jdbcUrl不是mybatis指定,需要换成url)
    在这里插入图片描述
<?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>
    <!--配置运行环境,环境不同,配置也不同,比如开发环境,数据库是测试库,生产环境,就是真正的业务数据库
        default="默认运行环境"
    -->
    <environments default="development">
        <!--每个environment都是一个运行环境,id="运行环境的名称"-->
        <environment id="development">
            <!--当前事务交给JDBC管理-->
            <transactionManager type="JDBC"></transactionManager>
            <!--配置数据源
               -type="POOLED"表示使用mybatis提供的连接池,UNPOOLED表示不使用
            -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"></property>
                <property name="url" value="jdbc:mysql:///bank"></property>
                <property name="username" value="root"></property>
                <property name="password" value="123456"></property>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <!--存放mapper.xml的全路径-->
        <mapper resource="BankMapper.xml"></mapper>
    </mappers>


</configuration>
  1. 测试(代码基本不变)
    在这里插入图片描述

3. 基本crud操作

mapper接口

在这里插入图片描述

xml

在这里插入图片描述

测试

在这里插入图片描述

4. 核心配置文件常用配置

Properties,可以引入外部资源文件,常用于mybatis配置文件和数据库配置解耦合
  1. 将数据库配置抽离成一个单独文件
    在这里插入图片描述
  2. mybatis核心配置文件,引用
    在这里插入图片描述
<!--引入外部资源文件-->
<properties resource="jdbc.properties"></properties>
typeAliases,为Java类型设置一个短的名字,比如我们com.yzpnb.entity.Account,可以配置为account,这样写起来更简单
  1. Mybatis为我们设置了一些常用的别名
    在这里插入图片描述
  2. 我们自己配置一下,将com.yzpnb.entity.Account,配置为account
    在这里插入图片描述
    <!--配置名字,可配置多个别名,都配置这个标签里面,用typeAlias 和 package 配置-->
    <typeAliases>
        <!--Account实体类,单独配置,这样只适用于类少的情况,可以一个个配置-->
        <typeAlias type="com.yzpnb.entity.Account" alias="account"></typeAlias>
        <!--给一整个包下的类起别名,别名就是类名,不区分大小写-->
        <package name="com.yzpnb.entity"/>
    </typeAliases>
  1. mapper映射文件就可以使用了
    在这里插入图片描述

5. 动态sql

mybatis提供了很多处理动态sql的手段,可以动态拼接条件,循环等等,如下,讲解在代码后面
<?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="cn.edu.imau.zy.online_education.mapper.OeUserMapper">
    <!--结果集封装-->
    <resultMap type="OeUser" id="OeUserResult">
        <result property="id"    column="id"    />
        <result property="jobNumber"    column="job_number"    />
        <result property="studentNumber"    column="student_number"    />
        <result property="userName"    column="user_name"    />
        <result property="phoneNum"    column="phone_num"    />
        <result property="level"    column="level"    />
        <result property="description"    column="description"    />
        <result property="avatar"    column="avatar"    />
        <result property="gmtCreate"    column="gmt_create"    />
        <result property="gmtModified"    column="gmt_modified"    />
    </resultMap>
    <!--公共头-->
    <sql id="selectOeUserVo">
        select id, job_number, student_number, user_name, phone_num, level, description, avatar, gmt_create, gmt_modified from oe_user
    </sql>
    
    <select id="selectOeUserList" parameterType="OeUser" resultMap="OeUserResult">
        <include refid="selectOeUserVo"/>/*引入公共头*/
        <where>
            <trim>
                <if test="jobNumber != null  and jobNumber != ''">and job_number = #{jobNumber}</if>
                <if test="studentNumber != null  and studentNumber != ''">student_number = #{studentNumber}</if>
                <if test="userName != null  and userName != ''"> and user_name like concat('%', #{userName}, '%')</if>
                <if test="phoneNum != null  and phoneNum != ''">phone_num = #{phoneNum}</if>
                <if test="level != null  and level != ''">level = #{level}</if>
                <if test="description != null  and description != ''">description = #{descrition}</if>
            </trim>
        </where>
    </select>
    
    <select id="selectOeUserById" parameterType="Long" resultMap="OeUserResult">
        <include refid="selectOeUserVo"/>
        where id = #{id}
    </select>
    <!--查询本校学生-->
    <select id="selectOurSchoolStudent" resultMap="OeUserResult">
        <include refid="selectOeUserVo"/>
        where student_number is not null
    </select>
    <!--查询本校讲师-->
    <select id="selectOurSchoolTeacher" resultMap="OeUserResult">
        <include refid="selectOeUserVo"/>
        where job_number is not null
    </select>
    <!--查询普通用户-->
    <select id="selectCommonUser" resultMap="OeUserResult">
        <include refid="selectOeUserVo"/>
        where job_number is null and student_number is null
    </select>
    <insert id="insertOeUser" parameterType="OeUser">
        insert into oe_user
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="id != null ">id,</if>
            <if test="jobNumber != null  and jobNumber != ''">job_number,</if>
            <if test="studentNumber != null  and studentNumber != ''">student_number,</if>
            <if test="userName != null  and userName != ''">user_name,</if>
            <if test="phoneNum != null  and phoneNum != ''">phone_num,</if>
            <if test="level != null  and level != ''">level,</if>
            <if test="description != null  and description != ''">description,</if>
            <if test="avatar != null  and avatar != ''">avatar,</if>
            <if test="gmtCreate != null ">gmt_create,</if>
            <if test="gmtModified != null ">gmt_modified,</if>
         </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="id != null ">#{id},</if>
            <if test="jobNumber != null  and jobNumber != ''">#{jobNumber},</if>
            <if test="studentNumber != null  and studentNumber != ''">#{studentNumber},</if>
            <if test="userName != null  and userName != ''">#{userName},</if>
            <if test="phoneNum != null  and phoneNum != ''">#{phoneNum},</if>
            <if test="level != null  and level != ''">#{level},</if>
            <if test="description != null  and description != ''">#{description},</if>
            <if test="avatar != null  and avatar != ''">#{avatar},</if>
            <if test="gmtCreate != null ">#{gmtCreate},</if>
            <if test="gmtModified != null ">#{gmtModified},</if>
         </trim>
    </insert>

    <update id="updateOeUser" parameterType="OeUser">
        update oe_user
        <trim prefix="SET" suffixOverrides=",">
            <if test="jobNumber != null  and jobNumber != ''">job_number = #{jobNumber},</if>
            <if test="studentNumber != null  and studentNumber != ''">student_number = #{studentNumber},</if>
            <if test="userName != null  and userName != ''">user_name = #{userName},</if>
            <if test="phoneNum != null  and phoneNum != ''">phone_num = #{phoneNum},</if>
            <if test="level != null  and level != ''">level = #{level},</if>
            <if test="description != null  and description != ''">description = #{description},</if>
            <if test="avatar != null  and avatar != ''">avatar = #{avatar},</if>
            <if test="gmtCreate != null ">gmt_create = #{gmtCreate},</if>
            <if test="gmtModified != null ">gmt_modified = #{gmtModified},</if>
        </trim>
        where id = #{id}
    </update>

    <delete id="deleteOeUserById" parameterType="Long">
        delete from oe_user where id = #{id}
    </delete>

    <delete id="deleteOeUserByIds" parameterType="String">
        delete from oe_user where id in 
        <foreach item="id" collection="array" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
    
</mapper>
if标签和trim标签
  1. if标签根据条件是否成立来拼接sql,但是很可能出现and开头,and结尾等语法错误的情况。例如select * from account where and username=“101001”,where后面里面跟了and,这是错误的
  2. where标签,可以让我们省略where关键字,并去掉开头和末尾的and
  3. trim可以指定去掉某些内容,比如逗号,也可以指定拼接前缀,后缀
foreach 标签,可以遍历拼接sql,可以指定遍历的属性,还有类似循环变量的功能
  1. item=“循环变量”,相当于循环变量,每次遍历的值都会渲染到它所在的位置
  2. collection=“容器类型”,如果传过来参数是数组就array,list就写list
  3. open,指定循环的前缀,就是循环开始前,先把这个字符拼接到sql
  4. separator,指定每个参数的分隔符
  5. close,指定循环结束的后缀,循环结束后,把这个字符拼接到sql
sql 和 include标签,相当于全局变量。sql可以定义一段sql为共用的(重复的sql),include可以在需要的地方引入

6. 复杂映射

新建一张表

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

6.1 一对一

一对一关系映射(不是站在数据库设计角度,而是java对象角度)
  1. 大致关系如下(一个订单,只能属于一个用户)
    在这里插入图片描述
    在这里插入图片描述
  2. 新建一个实体类,并封装对应关系
    在这里插入图片描述
  3. mapper层接口
    在这里插入图片描述
现在的问题是,我们如何让mybatis返回时,正常的返回Order,正常的将Account对象封装进Order的属性中,方式就是使用mybatis的结果集封装标签

在这里插入图片描述

<?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.yzpnb.mapper.OrderMapper">
    <!--将整个返回形式,我们人为的定义出来,这样mybatis就可以按照我们的要求完成结果集封装-->
    <resultMap id="orderMap" type="com.yzpnb.entity.Order">
        <result property="id" column="id"></result>
        <result property="orderTime" column="orderTime"></result>
        <result property="total" column="total"></result>
        <!--封装Account到orderMap中-->
        <association property="account" javaType="com.yzpnb.entity.Account">
            <result property="id" column="account_id"></result>
            <result property="username" column="username"></result>
            <result property="money" column="money"></result>
        </association>
    </resultMap>

    <select id="selectById" resultMap="orderMap" parameterType="string">
        select
            *
        from
            `order` as o
        left join
            account as a
        on
            o.account_id = a.id
        where
            o.id = #{id}
    </select>
</mapper>
测试结果

在这里插入图片描述

6.2 一对多

一对多(不是站在数据库设计角度,而是java对象角度),刚刚是一个订单属于一个用户,现在我们查一个用户的所有订单
  1. 大致关系
    在这里插入图片描述
  2. 实体类封装对应关系,用集合封装
    在这里插入图片描述
  3. mapper.xml改造
    在这里插入图片描述
    <!--方式一-->
    <resultMap id="accountMap" type="account">
        <result column="aid" property="id"></result>
        <result column="username" property="username"></result>
        <result column="money" property="money"></result>
        <!--封装一个集合,一个结果集,利用ofType直接指定集合的类型-->
        <collection property="orders" ofType="order"><!--order是核心配置类配置的别名-->
            <result column="oid" property="id"></result>
            <result property="orderTime" column="orderTime"></result>
            <result property="total" column="total"></result>
        </collection>
    </resultMap>
    <!--方式二:如果order需要复用,可以单独定义出来-->
    <resultMap id="orderMap" type="com.yzpnb.entity.Order">
        <result property="id" column="id"></result>
        <result property="orderTime" column="orderTime"></result>
        <result property="total" column="total"></result>
    </resultMap>
    <resultMap id="accountMap2" type="account">
        <result column="aid" property="id"></result>
        <result column="username" property="username"></result>
        <result column="money" property="money"></result>
        <!--可以直接通过resultMap引用-->
        <collection property="orders" resultMap="orderMap"><!--order是核心配置类配置的别名--></collection>
    </resultMap>
    <select id="selectOne" resultMap="accountMap2" parameterType="account">
        select
            a.id as aid,
            o.id as oid,
            username,
            money,
            orderTime,
            total
        from
            `order` as o
                left join
            account as a
            on
                o.account_id = a.id
        where
            a.id = #{id}
    </select>
  1. 测试
    在这里插入图片描述

7. Mybatis注解开发

常用注解
描述(🍅:不常用/🏆:常用)注解
🏆新增@Insert
🏆更新@Update
🏆删除@Delete
🏆查询@Select
🏆结果集封装@Result
🏆可与@Result一起使用,封装多个结果集@Results
🏆一对一结果集封装@One
🏆一对多结果集封装@Many

7.1 基本CRUD

mapper接口使用注解

在这里插入图片描述

核心配置类,指定扫描注解

在这里插入图片描述

测试

在这里插入图片描述

7.2 复杂映射,一对一

基于注解的复杂映射,其实就是执行多个sql,组合在一起
  1. 假设查询订单,以及这个订单属于谁
  2. 先根据id查询出订单,这时会查询出account_id,属于哪个account
  3. 然后再根据account_id查询账户
一对一注解,用@One来映射,例子如下
  1. 首先我们要拥有根据id查询出账户account的接口
    在这里插入图片描述
  2. 然后我们就可以编辑一对一映射了
    在这里插入图片描述
    //查询账单和账单所属账户
    //根据@Select查询出的account_id 再查询Account
    //@One表示查询一条
    @Results({
            @Result(column = "id",property = "id"),
            @Result(column = "orderTime",property = "orderTime"),
            @Result(column = "total",property = "total"),
            //封装Account到orderMap中,将查询条件account_id作为column
            @Result(column = "account_id",property = "account",
            javaType = Account.class,//指定类型
            one = @One(select = "com.yzpnb.mapper.AccountMapper.selectOne"))//指定这条sql去哪执行
    })
    @Select("select * from `order` where id = #{id}")
    public Order selectById(String id);
  1. 测试
    在这里插入图片描述

7.3 复制映射,一对多

一样的,根据账户,查询订单,我们需要先提供一个根据账户id查询订单的接口

在这里插入图片描述

然后编写映射,用@many,指定多个,列表形式

在这里插入图片描述

    //查询单个
    @Select("select * from account where id = #{id}")
    @Results({
            @Result(column = "id",property = "id"),
            @Result(column = "username",property = "username"),
            @Result(column = "money",property = "money"),
            @Result(column = "id",property = "orders",
            javaType = List.class,
            many = @Many(select = "com.yzpnb.mapper.OrderMapper.selectByAid")),
    })
    public Account selectOne(Account account);
测试

在这里插入图片描述

8. Mybatis一级缓存和源码

缓存就是把一些频繁访问的数据,放在内存中,这样用户想要查询数据,就不用频繁和数据库进行交换IO,而是直接从内存中获取数据。提高访问速度。
  1. mybatis提供了对缓存的支持,分为一级和二级缓存
    在这里插入图片描述
  2. 一级缓存,是SqlSession级别的缓存,操作数据库时需要构造sqlSession对象。而对象中有一个缓存(HashMap,数据结构)用于存储缓存数据。不同sqlSession之间的缓存数据区域(HashMap)互不影响
  3. 二级缓存,mapper级别的缓存。多个sqlSession操作同一个Mapper的sql语句,多个sqlSession共用二级缓存,是跨sqlSession的。
一级缓存,除了一些增删改会刷新缓存外,还可以调用sqlSession.clearCache()手动刷新

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

  1. 查询时,会先去缓存找是否有相应查询信息,没有就从数据库查,然后存到一级缓存
  2. 如果中间sqlSession执行commit操作(增删改),或调用clearCache(),当前sqlSession会清空自己的一级缓存,避免脏读。
  3. 再次查询时,依然先去缓存找,如果有,就直接缓存取出,否则还是去数据库,然后存入一级缓存
源码分析
  1. 以sqlSession接口clearCache方法为入口,点击进入实现类DefaultSqlSession。你会发现,此方法是调用executor执行器的clearLocalCache()方法完成的
    在这里插入图片描述
  2. 进入executor,它有两个实现类,我们观察BaseExecutor实现类的clearLocalCache方法,发现它清空了两个容器。这两个容器都是PerpetualCache类实例
    在这里插入图片描述
    在这里插入图片描述
  3. 我们点击进入PerpetualCache,我们发现,缓存就是一个HashMap,而一级缓存就是PerpetualCache对象
    在这里插入图片描述
  4. 那么既然executor会操作一级缓存,那么我们看看缓存什么时候创建,原来是构造方法,也就是执行器一创建,缓存就会创建
    在这里插入图片描述
  1. 我们先看看负责查询的query方法,发现它调用了createCacheKey(MappedStatement, parameter, rowBounds, boundSql)
    在这里插入图片描述
  2. 进入此方法,发现它就是负责创建CacheKey对象,这个对象就是一个根据MappedStatement(sql映射,mapper.xml或注解),查询参数(用户传过来的条件),RowBounds(分页参数),BoundSql(解析后的sql),configuration.getEnvironment().getId()//核心配置文件,当前环境的id值;生成key值的对象
    在这里插入图片描述
  3. 回到query,发现它拿着生成好的key,调用了query重载方法
    在这里插入图片描述
  4. 这个重载,先是判断是否有缓存(通过getObject(key)查看是否能获取到),没有才调用queryFromDatabase创建添加,
    在这里插入图片描述
  5. queryFromDatabase方法,负责添加缓存
    在这里插入图片描述
  1. 修改操作,会直接清空缓存
    在这里插入图片描述
    在这里插入图片描述

9. Mybatis二级缓存

前面介绍一级缓存时,发现缓存就是PerpetualCache对象,而这也是mybatis的默认二级缓存,如果我们配置时,不指定参数,那么就默认使用PerpetualCache来作为一个二级缓存
  1. 但是底层,它并不是,将一个对象存入二级缓存中,而是将数据存储到二级缓存中。因为二级缓存涉及多个sqlSession,难免发生并发同步问题
开启二级缓存前,需要将对应实体类,继承序列化接口,因为二级缓存的存储介质不同,可能不止将数据存储到内存,可能会持久化到磁盘,可能需要反序列化操作

在这里插入图片描述

二级缓存
  1. 和一级缓存原理一样,第一次查询判断缓存是否有,没有就查数据库。
  2. 不同点在于,一级缓存基于sqlSession。二级缓存基于mapper文件的namespace。也就是一个mapper的相关业务的缓存,很多个sqlSession共享这片区域
  3. 如果两个mapper的namespace相同,即使有两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同二级缓存中
    在这里插入图片描述
开启二级缓存的方法,一级缓存默认开启,但是二级缓存需要手动开启
  1. 全局配置文件,通过< settings >标签开启功能
    在这里插入图片描述
    <!--配置-->
    <settings>
        <!--二级缓存-->
        <setting name="cacheEnabled" value="true"/>
    </settings>
  1. 到需要二级缓存的mapper.xml开启缓存,可以直接空标签,使用默认二级缓存PerpetualCache,也可以自己指定
    在这里插入图片描述
    <!--开启二级缓存,不指定参数,等同于<cache type="org.apache.ibatis.cache.impl.PerpetualCache"></cache>
       - PerpetualCache是mybatis默认缓存-->
    <cache></cache>
  1. 使用注解@CacheNamespace开启缓存,可以指定缓存
    在这里插入图片描述
    在这里插入图片描述
@CacheNamespace(implementation = PerpetualCache.class)
两个用在子标签的配置(select、insert等子标签),useCache是否使用缓存和flushCache是否刷新缓存

在这里插入图片描述

  1. 对应注解
    在这里插入图片描述
 @Options(useCache = false)//不使用缓存
 @Options(flushCache = Options.FlushCachePolicy.TRUE)//刷新缓存

9.1 使用redis自定义二级缓存

前面我们说过mybatis默认是PerpetualCache为二级缓存,既然是默认,那么也就表示,我们可以自定义缓存,接下来介绍如何通过reids自定义二级缓存。只需要继承Cache接口重写方法即可
为什么不使用默认的,默认的使用HashMap作为缓存
  1. 如果是单服务器,HashMap有很大缺点,就是无法实现分布式缓存
  2. A和B两个服务器的缓存无法立刻同步
  3. 所以,分布式架构下,最好找一个分布式缓存,专门存储缓存数据,这样不同服务器要用缓存,都从这个分布式缓存中操作
    在这里插入图片描述
为什么用redis呢,因为快么?
  1. 当然是因为懒了,mybatis不仅提供cache,让我们自己实现缓存逻辑,还自己提供了一个基于cache接口的redis实现类。该类在mybatis-redis包中
  2. 引入依赖即可使用
    在这里插入图片描述
        <!--mybatis-redis-->
        <dependency>
            <groupId>org.mybatis.caches</groupId>
            <artifactId>mybatis-redis</artifactId>
            <version>1.0.0-beta2</version>
        </dependency>
使用reids缓存
  1. 注解指定实现类,xml同理,两种方式根据自己喜好选择
    在这里插入图片描述
    在这里插入图片描述
  2. 配置redis,配置文件必须叫redis.properties,放在resources目录下(classpath:)
    在这里插入图片描述
redis.host = localhost
redis.port = 6379
redis.connectionTimeout = 5000
redis.database = 0
使用,然后查询redis缓存,依然要保证,查询的任何实体类都要实现序列化,否则不会添加到缓存

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

9.2 mybatis-redis源码分析

1. 想要实现二级缓存,必须继承Cache接口.可以发现RedisCache这个类,继承了Cache接口,并实现了相关方法。而我们发现构造方法,传入一个id,也就是外界进行了构建,猜测是构建者设计模式
  1. 我们还发现这里面封装redisConfig对象,这个就是redis的配置,自己看就好,没什么好说的
    在这里插入图片描述
2. 构建者分析
  1. 构建的本质,构建指定的对象,我们知道,我们配置时,配置的@CacheNamespace(implementation = PerpetualCache.class),所以implementation就是构建对象
    在这里插入图片描述
  2. build方法
    在这里插入图片描述

10. Mybatis插件

插件
  1. 开源框架一般都会提供插件或其它拓展点,供开发者自行拓展。
  2. 增加框架灵活性,方便开发者对框架拓展
  3. 我们可以基于Mybatis插件机制,实现分页,分表,监控等功能
  4. 插件和业务无关,业务无法感知插件存在,可以无感插入插件,无形中增强功能
MyBatis插件
  1. Mybatis在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插件扩展机制
    在这里插入图片描述
  2. MyBatis对持久层操作就是借助这4大核心对象,而MyBatis支持用插件对这四大核心对象拦截,对mybatis来说,插件就是拦截器
  3. 本质上还是借助底层动态代理实现。Mybatis四大对象都是代理对象
Mybatis允许拦截的方法
  1. Executor执行器:update、query、commit、rollback等
  2. StatementHandler,SQL语法构建器:prepare、parameterize、batch、update、query
  3. ParameterHandler参数处理器:getParameterObject、setParameters
  4. ResultSetHandler:handleResultSets、handleOutputParameters等
Mybatis插件原理,当四大对象创建时
  1. 每个创建出来的对象不直接返回,而是interceptorChain.pluginAll(parameterHandler);
  2. 获取到所有Interceptor(拦截器)(插件需要实现的接口);调用interceptor.plugin(target);返回target包装后对象
  3. 插件机制,我们可以使用插件为目标对象创建一个代理对象;可以拦截到四大对象的每一个执行
    在这里插入图片描述
  1. interceptorChain保存了所有拦截器(interceptor),在mybatis初始化时创建,调用拦截链中的拦截器依次对目标进行拦截或增强
  2. interceptor.plugin(target)中target可以理解为mybatis中四大对象,返回target是被重重代理的对象
  1. 如果我们要拦截Exector的query方法,可以像这样来定义插件
    在这里插入图片描述
  1. 这样Mybatis启动时就可以加载插件,保存插件实例到相关对象(InterceptorChain,拦截链中)
  2. 准备工作完成后,Mybatis处于就绪状态,执行sql时,需要先通过DefaultSqlSessionFactory创建SqlSession
  3. Exector实例会在创建SqlSession过程中被创建,Executor实例创建完成后,MyBatis会通过JDK动态代理为实例生成代理类
  4. 此时,插件逻辑可以在Executor相关方法被调用前执行。

10.1 自定义插件练手

假设,我们要对下面这个四大对象之一StatementHandler的prepare(Connection connection, Integer transactionTimeout)方法进行拦截

在这里插入图片描述

自定义插件
  1. 插件
    在这里插入图片描述
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;

import java.sql.Connection;
import java.util.Properties;

/**
 * 自定义插件
 */
@Intercepts({//可以定义多个@Singnature,对多个地方拦截,都用这个拦截器
        @Signature(type = StatementHandler.class,//指定拦截StatementHandler接口
                method = "prepare",//指定要拦截这个接口(指定拦截StatementHandler接口)的具体方法名,必须写对
                args={ Connection.class,Integer.class}//拦截方法的参数列表,必须和你要拦截的方法一一对应
                 )
})
public class MyPlugin implements Interceptor {

    //每次执行操作,都会进入此拦截
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //增强,插件逻辑
        System.out.println("增强,自定义插件逻辑");
        return invocation.proceed();//原方法执行,并返回
    }

    /**
     * 将当前拦截器,生成代理对象放在拦截链中
     * @param target 目标对象,被代理对象,被拦截对象
     * @return 代理对象
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("为"+target+"生成代理");
        return Plugin.wrap(target,this);
    }
    //获取配置文件属性,xml中配置的属性,从Properties获取,插件初始化时调用。只调用一次,插件配置的属性从这里设置
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的初始化参数:"+properties);
    }
}
  1. xml配置
    在这里插入图片描述
    <plugins>
        <plugin interceptor="com.yzpnb.plugin.MyPlugin">
            <!--配置参数,-->
            <property name="name" value="Bob"/>
        </plugin>
    </plugins>
测试

在这里插入图片描述

10.2 源码分析

上面我们已经知道,插件的核心原理就是动态代理,所以看源码关键就是,如何动态代理
  1. 自定义插件时, 我们使用Plugin.wrap(target,this)生成代理对象。所以我们以Plugin类为看源码的入口。我们发现,Plugin就是一个代理类,我们通过Plugin对target进行代理。
  1. 但是厉害的是,它通过静态方法wrap,让用户传入代理对象和自定义拦截器
  2. 然后根据用户传入的,生成Plugin对象,也就是最终代理对象,就是Plugin。然后他把拦截器保存到成员变量interceptor中
  3. 所以,Plugin这个代理类,可以随时使用自定义拦截器
    在这里插入图片描述
  1. 既然Plugin是代理类,那么核心逻辑就在invoke()方法中,它会拦截所有方法。可以发现它会检测当前拦截的方法是否是我们插件要拦截的,如果是,执行插件逻辑,否则继续执行被拦截的方法
    在这里插入图片描述
  2. 而执行插件逻辑,就是我们自己实现的intercept方法
    在这里插入图片描述
  3. 这个intercept方法的参数Invocation,也很简单,就是保存目标对象,方法,和参数列表
    在这里插入图片描述

10.3 pageHelper分页插件

介绍了自定义插件的方法,接下来看看,Mybatis常用的第三方插件
  1. PageHelper就是一个插件,将分页操作进行封装,让我们可以方便的进行分页操作
  2. 导入maven依赖
    在这里插入图片描述
        <!--pagehelper-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>3.7.5</version>
        </dependency>
        <dependency>
            <groupId>com.github.jsqlparser</groupId>
            <artifactId>jsqlparser</artifactId>
            <version>0.9.1</version>
        </dependency>
  1. 核心配置文件xml中配置
    在这里插入图片描述
    <plugins>
        <!--分页插件-->
        <plugin interceptor="com.github.pagehelper.PageHelper">
            <!--指定数据库-->
            <property name="dialect" value="mysql"/>
        </plugin>
    </plugins>
测试分页插件

在这里插入图片描述

10.4 通用mapper

单表操作,全部给我们写好了

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

三、Mybatis源码

篇幅原因,我将其放在这篇文章https://blog.csdn.net/grd_java/article/details/122927311
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值