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

Posted LRcoding

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaLearn#(27)MyBatis进阶:Mapper代理(接口绑定)多参数传递模糊查询分页自增主键回填动态SQL一级缓存二级缓存相关的知识,希望对你有一定的参考价值。

1. Mapper代理 (接口绑定)

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

缺点:

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

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

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

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表示

    List<Employee> findEmp(@Param("job") String job, @Param("sal") double sal);
    
    SELECT <include refid="empCols" /> FROM emp WHERE job = #job AND sal > #sal
    <!-- SELECT <include refid="empCols" /> FROM emp WHERE job = #param1 AND sal > #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的二级缓存数据。

4.1 一级缓存

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

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

当第一次执行某个查询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);

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

4.3 缓存相关细节