目录
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
表示 -
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()函数
来连接参数和通配符
对于特殊字符,比如<,不能直接书写,应该使用字符实体 <
替换
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 <= #{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 缓存相关细节
-
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永久缓存进行缓存策略等方便的控制
-
-
查询数据顺序
- 二级缓存
- 一级缓存
- 数据库
- 把数据保存到一级缓存
- 当 SqlSession关闭(close)或者提交(commit)时,把数据刷入到二级缓存中