【JavaLearn】(27)MyBatis进阶:Mapper代理(接口绑定)、多参数传递、模糊查询、分页、自增主键回填、动态SQL、一级缓存、二级缓存

1. Mapper代理 (接口绑定)

之前已经使用 MyBatis完成了对 emp表的 CRUD操作(MyBatis基础),都是由 SqlSession调用自身的方法发送 SQL命令,并得到结果

缺点:

  • 不管是 selectList()、selectOne(),都只能提供一个查询参数,如果需要多个,就需要封装到 JavaBean中
  • 方法的返回值类型比较固定
  • 只提供了映射文件,没有提供数据库操作的接口,不利于后期维护

基于此,MyBatis提供了一种叫 **Mapper代理(接口绑定)**的操作方式 ---- 增加一个接口 EmpMapper,并修改映射文件和测试类

注意接口的名字,必须和映射文件的名字一模一样

image-20220502105835551

1.1 使用 Mapper代理方式实现查询

1.1.1 实现步骤

首先定义接口文件 EmpMapper

public interface EmpMapper {
    /**
     * 查询所有员工信息
     * @return
     */
    List<Employee> findAll();

    /**
     * 根据id查询
     * @param empno
     * @return
     */
    Employee findById(int empno);
}

然后定义映射文件 EmpMapper.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.lwclick.mapper.EmpMapper">
    <!-- SQL的id名字,必须和接口中的【方法名】一致 -->
    <select id="findAll" resultType="employee">
        SELECT empno, ename, sal, hireDate FROM emp
    </select>

    <select id="findById" resultType="employee">
        SELECT empno, ename, sal, hireDate FROM emp WHERE empno = #{param1}
    </select>
</mapper>

测试类中的修改:

@Test
public void testFindAll() {
    // 获取 SqlSessionFactory,获取SqlSession
    SqlSession sqlSession = DBUtil.getSqlSession();

    // 获取 Mapper
    EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);

    // (使用Mapper)访问数据库并获取结果
    // 此时对数据库的操作不是由SqlSession发起,而是由EmployeeMapper接口发起,直接调用接口的方法!!!!!!
    List<Employee> employeeList = mapper.findAll();

    // 关闭 SqlSession
    DBUtil.closeSqlSession(sqlSession);

    // 输出结果
    employeeList.forEach((emp) -> {
        System.out.println(emp);
    });
}

1.1.2 注意事项

  • 使用 Mapper代理,namespace必须是接口的全路径名
  • select等映射标签的 id必须是接口中方法的名字
  • 使用 #{},底层使用的是 PreparedStatement,而使用 ${},底层使用了 Statement,会有SQL注入的风险(除非是表名的动态改变类似情况)

EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);

这条语句的底层使用了动态代理模式,动态创建一个EmpMapper的一个代理对象并赋给接口引用

1.2 使用sql元素重用数据库字段

映射文件中,添加 <sql>标签,重用数据库字段

<sql id="empCols">
    empno, ename, sal, hireDate
</sql>

select映射标签中进行修改

<select id="findAll" resultType="employee">
    SELECT <include refid="empCols"> FROM emp
    </select>

2. 更多的映射

2.1 多参数传递

在 EmpMapper 接口中定义方法实现同时按照 job、sal 两个字段完成信息的查询,可以有四种方式:

  • 1:直接传递多个参数

    映射文件中,参数使用 param1、param2....表示,或者arg0、arg1....(可读性低)

    List<Employee> findEmp(String job, double sal);
    
    <select id="findEmp" resultType="employee">
        SELECT <include refid="empCols" /> FROM emp WHERE job = #{param1} AND sal > #{param2}
        <!-- SELECT <include refid="empCols" /> FROM emp WHERE job = #{arg0} AND sal > #{arg1} -->
    </select>
    
  • 2:使用 Param注解传递多个参数

    接口的方法中,使用 Param注解定义参数,在映射文件中使用 Param中的名字来表示,同时保留了param1、param2表示

  • 3:使用 JavaBean传递多个参数

    映射文件中的参数直接使用 JavaBean的属性来接收,底层调用的是相应属性的 getter方法

    List<Employee> findEmp(Employee emp);
    
    SELECT <include refid="empCols" /> FROM emp WHERE job = #{job} AND sal > #{sal}
    
  • 4:使用 Map 传递多个参数

    映射文件中,使用相应参数在 map中的key来表示

    List<Employee> findEmp(Map<String, Object> params);
    
    SELECT <include refid="empCols" /> FROM emp WHERE job = #{job} AND sal > #{sal}
    

总结:

  • 使用 Map方式虽然简便,但导致了业务可读性的丧失,导致后续可扩展和维护的困难,果断放弃
  • 直接传递多个参数,会导致映射文件中,可读性的降低,也不推荐使用
  • 如果参数数量 <= 5个,推荐 Param注解的方式
  • 如果参数数量 > 5个,推荐使用 JavaBean方式
  • 如果涉及到多个 JavaBean参数,可以同时使用 Param注解进行标记

2.2 模糊查询

在进行模糊查询时,在映射文件中可以使用concat()函数来连接参数和通配符

对于特殊字符,比如<,不能直接书写,应该使用字符实体 &lt;替换

EmpMapper 接口

List<Employee> findEmpByParams(@Param("ename") String ename, @Param("hireDate") Date hireDate);

EmpMapper.xml 映射文件:

<select id="findEmpByParams" resultType="employee">
    SELECT
    	<include refid="empCols" />
    FROM
    	emp
    WHERE
    	ename LIKE concat('%', #{ename}, '%')
    	AND hireDate &lt;= #{hireDate}
</select>

2.3 分页查询

MyBatis 不仅提供分页,还内置了一个专门处理分页的类 RowBounds(其实就是一个简单的实体类),包含两个成员变量

  • offset:偏移量,从 0 开始计数
  • limit:限制条数
/**
 * 带分页的查询所有
 * @param rowBounds
 * @return
 */
List<Employee> findEmpPage(RowBounds rowBounds);
<!-- 语句不用改变,MyBatis自动处理 -->
<select id="findEmpPage" resultType="employee">
    SELECT <include refid="empCols" /> FROM emp
</select>

测试类中:

RowBounds bounds = new RowBounds(2, 4);     // 从第3个开始,显示4条数据
List<Employee> empPage = mapper.findEmpPage(bounds);

但是不建议使用:MyBatis先查出所有的数据,然后再根据偏移量和限制条数去筛选

建议自己封装分页类来实现,使用 SELECT <include refid="empCols" /> FROM emp limit #{offset}, #{length}的形式,在接口的方法中传 offset和 length参数

2.4 自增主键回填

MySQL支持主键自增。有时候完成添加后需要立即获取刚刚自增的主键

映射文件中进行配置(只能获取自增类型的主键);

  • 通过 useGeneratedKeys 属性

    <!-- DML操作需要手动的提交事务 sqlSession.commit() -->
    <insert id="saveEmp" useGeneratedKeys="true" keyProperty="empno">
        INSERT INTO emp VALUES (null, #{ename}, #{job}, #{mgr}, #{hireDate}, #{sal}, #{comm}, #{deptno})
    </insert>
    
    • useGeneratedKeys:表示要使用自增的主键
    • keyProperty:表示把自增的主键赋给 JavaBean的哪个成员变量

    以添加 Employee为例,添加前 empno是空的,添加完成后,empno的值就为刚才新增记录的id

  • 通过 selectKey 标签

<insert id="saveEmp">
    <selectKey order="AFTER" keyProperty="empno" resultType="int">
        SELECT @@IDENTITY 
    </selectKey>
    INSERT INTO emp VALUES (null, #{ename}, #{job}, #{mgr}, #{hireDate}, #{sal}, #{comm}, #{deptno})
</insert>

  • order:取值 AFTER | BEFORE,表示在操作之前还是之后执行 selectKey中的 SQL命令
  • keyProperty:执行 SQL 语句后,结果赋給哪个属性
  • resultType:执行SQL后,结果的类型

3. 动态SQL

在进行前端页面列表展示数据时,我们需要根据不同的条件展示不同的数据,但是有的条件是空值,那么 SQL该怎么写呢?

----> 使用 MyBatis的动态 SQL 功能,在映射文件中根据标签拼接 SQL语句(语法和JSTL类似,但却是基于强大的 OGNL表达式)

接口中定义下列语句来练习动态 SQL语句

/**
 * 通过参数查询数据
 */
List<Employee> findEmp(@Param("job") String job, @Param("sal") double sal, @Param("deptno") double deptno);

/**
 * 更新数据
 */
int updateEmp(String job, double sal, int empno);

/**
 * 通过 list 查询数据
 */
List<Employee> findEmpByList(@Param("deptnoList") List<Integer> deptNoList);

/**
 * 通过 array 查询数据
 */
List<Employee> findEmpByArr(int[] arr);

3.1 if

每一个 if 相当于一个 if单分支语句

一般添加一个 where 1=1 的查询条件,作为第一个条件,这样可以让后面每个 if语句的SQL语句都以and开始

<select id="findEmp" resultType="employee">
    SELECT <include refid="empCols" /> FROM emp
    WHERE 1 = 1                             <!-- 添加 where 1=1 统一后面的 if语句 -->
    <if test="job != null and job != ''">
        AND job = #{job}
    </if>
    <if test="sal > 0">
        AND sal > #{sal}
    </if>
    <if test="deptno != 0">
        AND deptno = #{deptno}
    </if>
</select>

3.2 where

使用 where 元素,就不需要提供 where 1=1 这样的条件了

如果 where标签内部内容不为空则自动添加 where 关键字,并且会自动去掉第一个条件的 and 或者 or

<select id="findEmp" resultType="employee">
    SELECT <include refid="empCols" /> FROM emp
    <where>                                    <!-- 使用<where>标签,自动加where,自动去掉第一个条件的 and或 or -->
        <if test="job != null and job != ''">
            AND job = #{job}
        </if>
        <if test="sal != 0">
            AND sal > #{sal}
        </if>
        <if test="deptno != null and deptno != ''">
            AND deptno = #{deptno}
        </if>
    </where>
</select>

3.3 bind

bind 主要的一个重要场合是模糊查询

通过 bind设置通配符和查询值,可以避免使用数据库的具体语法来进行拼接(比如MySQL使用concat来拼接,而Oracle使用 || )

<select id="findEmp" resultType="employee">
    <!-- name的值为下面要使用的值 -->
    <bind name="enameBind" value="'%'+ename+'%'"/>
    SELECT <include refid="empCols" /> FROM emp WHERE ename LIKE #{enameBind}  <!-- 此处使用 bind的 name值 -->
</select>

3.4 set

set元素用在update语句给字段赋值

借助 if 的配置,可以只对有具体值的字段进行更新。set元素会自动添加 set关键字,自动去掉最后一个if语句的多余的逗号

<update id = "updateEmp">
    UPDATE emp
    <set>
        <if test = "param1 != null and param1 != ''">
            job = #{param1},      <!-- 直接传递的参数,所以此处使用 param1 的形式 -->
        </if>
        <if test = "param2 > 0">
            sal = #{param2},
        </if>
    </set>
    WHERE empno = #{param3}
</update>

3.5 foreach

允许指定一个集合或者数组,声明集合项和索引变量,它们可以用在元素体内,也允许指定开放和关闭的字符串,在迭代之间放置分隔符

注意:可以传递一个 List 实例 或者 数组 作为参数对象传给 MyBatis,MyBatis会自动将它包装在一个 Map中,List实例以 list 作为键,而数组会以 array 作为键(若通过Param指定了参数的名称,则必须使用该名称

<!-- List实例方式 -->
<select id="findEmpByList" resultType="employee">
    SELECT <include refid="empCols" /> FROM emp
    WHERE deptno IN
   	<!-- 通过 @Param("deptnoList") 指定了参数的名称,则必须使用该名称 -->
    <foreach collection="deptnoList" item="deptno" open="(" close=")" separator=",">
        #{deptno}
    </foreach>
</select>

<!-- 数组方式 -->
<select id="findEmpByArr" resultType="employee">
    SELECT <include refid="empCols" /> FROM emp
    WHERE deptno IN
    <foreach collection="array" item="deptno" open="(" separator="," close=")">
        #{deptno}
    </foreach>
</select>

4. 缓存

相同查询条件的SQL语句执行一遍后所得到的结果存在内存或者某种缓存介质当中,当下次遇到一模一样的查询SQL时候,不再执行SQL与数据库交互,而是直接从缓存中获取结果,减少服务器的压力。

MyBatis分为一级缓存(默认开启的)和二级缓存一级缓存SqlSession上的缓存,二级缓存是在 SqlSessionFactory上的缓存,当数据量大的时候可以借助一些第三方缓存框架或Redis缓存来协助保存 MyBatis的二级缓存数据。

image-20220502231158101

4.1 一级缓存

一级存储是 SqlSession 上的缓存,默认开启,不要求实体类对象实现Serializable接口

Employee emp = mapper.findById(7698);
Employee emp2 = mapper.findById(7698);

image-20220502232914424

当第一次执行某个查询SQL语句时,会将查询到的结果缓存到一级缓存中,当第二次再执行一模一样的查询SQL时,会使用缓存中的数据(可以看到只有一个SQL语句),而不是对数据库再次执行SQL(需要保证是同一个 SqlSession)

4.2 二级缓存

二级缓存是 SqlSessionFactory 上的缓存,由一个 SqlSessionFactory 创建的所有 SqlSession都可以共享缓存数据,默认不开启

如何开启二级缓存

  • 全局开关:在 mybatis-cfg.xml 中使用 <setting>标签配置开启二级缓存

    <settings>
        <!-- 开启二级缓存,默认是开启的 -->
        <setting name="cacheEnabled" value="true"/>
    </settings>
    
  • 分开关:在要开启二级缓存的映射文件中开启缓存

    <cache />
    
  • 缓存中存储的 JavaBean对象必须实现序列化接口

    public class Employee implements Serializable {  }
    

经过设置后,请求到来时,第一个 SqlSession 会首先去二级缓存中查找,如果不存在,就查询数据库,在 commit() 或者 close()的时候将数据放入到二级缓存,第二个 SqlSession执行相同的SQL语句时,直接从二级缓存中获取了

// 创建两个 SqlSession,执行相同的 SQL语句,让第二个 SqlSession使用第一个 SqlSession查询后缓存的数据
@Test
public void testCacheLevel2() throws IOException {
    InputStream is = Resources.getResourceAsStream("mybatis-cfg.xml");
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);

    // 创建两个 SqlSession
    SqlSession sqlSession = factory.openSession();
    SqlSession sqlSession2 = factory.openSession();

    EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
    EmpMapper mapper2 = sqlSession2.getMapper(EmpMapper.class);

    Employee emp = mapper.findById(7698);
    sqlSession.commit();  // 作用之一:将一级缓存的数据放入二级缓存,没有关闭当前 SqlSession,一级缓存存在  (另一个作用:提交事务)

    Employee emp2 = mapper2.findById(7698);
    sqlSession2.commit();

    sqlSession.close();   // 作用之一:将一级缓存的数据放入二级缓存,关闭当前 SqlSession,一级缓存不存在
    sqlSession2.close();

    System.out.println(emp);
    System.out.println(emp2);
}

image-20220502234353119

没有二级缓存时的执行结果

image-20220502234516978

4.3 缓存相关细节

  • MyBatis的二级缓存的缓存介质有多种多样,而并不一定是在内存中,所以需要对 JavaBean对象实现序列化接口

  • 二级缓存是以 namespace 为单位的,不同 namespace下的操作互不影响

  • 加入 <cache />元素后,该映射文件中的所有 select元素查询结果都会进行缓存,可以使用 useCache="false" 设置不进行缓存

    <select id="findEmp" resultType="employee" useCache="false">
    </select>
    
       
       
    • 1
    • 2

    而其中的 insert、update、delete元素进行操作时,会清空整个 namespace的缓存

  • cache 有一些可选的属性,type、eviction、flushInterval、size、readOnly、blocking

    <cache  type="" readOnly="" eviction="" flushInterval="" size="" blocking=""/>
    
       
       
    • 1
    • type:自定义缓存类,要求实现org.apache.ibatis.cache.Cache接口,不设置默认使用的是org.apache.ibatis.cache.impl.PerpetualCache
    • readOnly:是否只读(默认false)
      • true:给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势
      • false:返回缓存对象的拷贝(通过序列化) 。安全
    • eviction:缓存策略(默认LRU)
      • LRU:最近最少使用——移除最长时间不被使用的对象
      • FIFO:先进先出——按对象进入缓存的顺序来移除它们
      • SOFT:软引用——基于垃圾回收器状态和软引用规则移除对象
      • WEAK:弱引用——更积极地基于垃圾收集器状态和弱引用规则移除对象
    • size:缓存对象个数(默认1024)
    • flushInterval:刷新间隔,单位为毫秒(默认为null)也就是没有刷新间隔,只有执行update、insert、delete语句才会刷新
    • blocking:是否使用阻塞性缓存BlockingCache(默认false)
      • true:在查询缓存时锁住对应的Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,保证只有一个线程到数据库中查找指定key对应的数据
      • false:不使用阻塞性缓存,性能更好
  • 缓存相关API

    • 缓存的功能由根接口 org.apache.ibatis.cache.Cache 定义。整个体系采用装饰器设计模式

    • 数据存储和缓存的基本功能由 org.apache.ibatis.cache.impl.PerpetualCache 永久缓存实现,其底层采用了HashMap结构来存储缓存信息

    • 通过一系列的装饰器来对PerpetualCache永久缓存进行缓存策略等方便的控制

      image-20220503001152041

  • 查询数据顺序

    • 二级缓存
    • 一级缓存
    • 数据库
    • 把数据保存到一级缓存
    • 当 SqlSession关闭(close)或者提交(commit)时,把数据刷入到二级缓存中

    image-20220503002149154

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值