目录
- 动态SQL
- 环境准备
- IF语句
- trim(where&Set)
- where
- set
- trim
- choose&when&otherwise
- sql片段
- Foreach
- 缓存
- 一级缓存
- 缓存示例
- 缓存失效场景
- 二级缓存
- 开启全局缓存
- 二级缓存示例
动态SQL
为什么叫做动态SQL:因为在程序执行中,mybatis提供的sql可以根据用户提供的字段数量、类型,合理的选择对应的执行sql。正是这一动态的选择特性,极大的优化了使用JDBC的代码冗余。
根据不同条件生成不同的sql语句执行
环境准备
以博客表为例:
create table `blog`(
`id` varchar(50) primary key comment '博客ID',
`title` varchar(100) not null comment '博客标题',
`author` varchar(50) not null comment '博客作者',
`create_time` datetime not null comment '创建时间',
`view` int not null comment '浏览量'
);
Blog实体:
@Data
@AllArgsConstructor
public class Blog {
private String id;
private String title;
private String author;
private Date creatTime;
private int views;
}
IDutils,用于随机生成的ID名称
public static String getId(){
return UUID.randomUUID().toString().replaceAll("-","");
}
IF语句
以上述搭建的环境为例,当我们需要查询博客时,如果用户指定了搜索搜索字段那就根据该字段查找,如果没有指定那就查询全部。如果用普通的sql语句实现,需要我们在Java程序中进行判断,但是MyBatis提供了动态SQL,我们就可以利用内置的IF标签来实现:
BlogMapper.xml配置
<select id="getBlogListIF" parameterType="map" resultType="Blog">
select * from blog where 1 = 1
<if test="title != null">
and title like "%"#{title}"%"
</if>
<if test="author != null">
and author like "%"#{author}"%"
</if>
</select>
测试:
@Test
public void testGetBlogList(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("title","Java");
map.put("author",null);
List<Blog> blogListIF = mapper.getBlogListIF(map);
for (Blog blog : blogListIF) {
System.out.println(blog);
}
sqlSession.commit();
sqlSession.close();
}
此处采用模糊查询,在xml中直接对title和author字段进行判断,如果非空则执行拼接sql,反之查询全部
trim(where&Set)
where
看下列代码:
<select id="getBlogListIF" parameterType="map" resultType="Blog">
select * from blog where
<if test="title != null">
and title like "%"#{title}"%"
</if>
<if test="author != null">
and author = #{author}
</if>
</select>
此时当上述两个if满足任一时,sql拼接后变成:
select * from blog where and author = #{author}这是不符合sql语法规则的。对此MyBatis提供了where标签来处理这种情况。
<select id="getBlogListIF" parameterType="map" resultType="Blog">
select * from blog
<where>
<if test="title != null">
and title like "%"#{title}"%"
</if>
<if test="author != null">
and author = #{author}
</if>
</where>
</select>
where元素只会在它的任一子元素返回内容时,才会在sql中插入where子句。如果返回的sql开头为and 或on,where标签会自动将其抹去
set
sql中更新语句update在mybatis常用set标签来判定都需要更新哪些字段,如果用户设置了新的该字段属性,则会在set检测到,从而执行更新语句
并且set子句会动态的在行首添加上set关键字,包括删除额外的逗号
<update id="updateBlogInfo" parameterType="map">
update blog
<set>
<if test="title != null">
title = #{title},
</if>
<if test="author != null">
author = #{author},
</if>
</set>
where id = #{id}
</update>
trim
trim包含四个属性:
prefix前缀、prefixOverrides前缀覆盖、suffix后缀、suffixOverrides后缀覆盖
当where和set不能得到预期的结果时,可以使用trim进行配置。也可以直接使用trim实现和where、set相同的效果:
<!-- trim实现set -->
<trim prefix="set" suffixOverrides=",">
<if test="title != null">
title = #{title},
</if>
<if test="author != null">
author = #{author},
</if>
</trim>
<!-- trim实现where -->
<trim prefix="where" prefixOverrides="and | or">
<choose>
<when test="title != null">
title = #{title}
</when>
<when test="author != null">
and author = #{author}
</when>
<otherwise>
and view != 0
</otherwise>
</choose>
</trim>
choose&when&otherwise
choose标签,类似于Java中的switch语句。当我们不想要执行全部的sql,而只是选择性的去执行对应的sql。
三者的关系类似于switch–>choose、case–>when、default–>otherwise
BlogMapper.xml编译sql
<select id="queryBlogChoose" parameterType="map" resultType="blog">
select * from blog
<where>
<choose>
<!--title不为null执行-->
<when test="title != null">
title = #{title}
</when>
<!--author不为null执行-->
<when test="author != null">
and author = #{author}
</when>
<!--默认执行-->
<otherwise>
and view != 0
</otherwise>
</choose>
</where>
</select>
sql片段
利用sql标签,抽离重复代码。在需要使用的地方使用include标签直接引入即可
<!-- 抽离sql -->
<sql id="checkTitleAuthor">
<if test="title != null">
title = #{title},
</if>
<if test="author != null">
author = #{author},
</if>
</sql>
<select id="getBlogListIF" parameterType="map" resultType="Blog">
select * from blog
<where>
<!-- 引入sql片段 -->
<include refid="checkTitleAuthor"/>
</where>
</select>
Foreach
利用Foreach可以在动态sql中对集合进行遍历
BlogMapper.xml
<select id="getBlogForeach" parameterType="map" resultType="blog">
select * from blog
<where>
<foreach collection="ids" item="id" open="(" separator="or" close=")">
id=#{id}
</foreach>
</where>
</select>
上述代码,利用map集合存储list集合交给foreach,此处collection通过键“ids”获取list,item为值,open为拼接sql的开始,close为拼接sql的结束,separator表示分隔符
缓存
什么是缓存?
缓存是存在于内存中的临时数据
使用缓存可以减少和数据库的交互次数,提高数据库性能和执行效率
官网给出:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
一级缓存
也叫做本地缓存,对应MyBatis中的sqlSession。
一级缓存是默认开启的,作用域仅在sqlSession中有效
缓存示例
用户user表id查询两次相同数据示例:
@Select("select * from user where id = #{id}")
Users getUserById(@Param("id") int id);
// 测试
public void testGetUsersList() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Users user1 = mapper.getUserById(2);
System.out.println(user1);
Users user2 = mapper.getUserById(2);
System.out.println(user2);
System.out.println(user1==user2);
sqlSession.close();
}
打印效果分析:
上述程序分别调用两次getUserById方法,如果没有缓存机制那么最终应该会执行两次查询sql来返回数据,但是根据日志可以看到最终只执行了一次sql。 这说明,第一次查询到的数据就已经存放在了缓存当中,而第二次执行查询时将会直接从缓存中获取,不再进入sql层面查询。
看下面的示例:
<update id="updateUserInfo" parameterType="map" >
update user
<set>
<if test="name != null">
name = #{name},
</if>
<if test="pwd != null">
pwd = #{pwd},
</if>
</set>
where id = #{id}
</update>
@Test
public void testGetUsersList() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询id=2数据
Users user1 = mapper.getUserById(2);
System.out.println(user1);
System.out.println("-----------------------------------------------");
// 修改id=2数据
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("name","冯七七");
map.put("id",2);
int i = mapper.updateUserInfo(map);
sqlSession.commit();
// 第二次查询id=2数据
Users user2 = mapper.getUserById(2);
System.out.println(user2);
System.out.println(user1==user2);
sqlSession.close();
}
首先第一次查询数据,查询完之后调用修改方法将name修改为“ 冯七七 ” 然后再次执行查询语句
日志分析:
Created connection 594427726.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@236e3f4e]
-- 第一次执行查询sql 数据保存在sqlSession中
==> Preparing: select * from user where id = ?
==> Parameters: 2(Integer)
<== Columns: id, name, pwd
<== Row: 2, 冯子, 234
<== Total: 1
Users(id=2, name=冯子, pwd=234)
-----------------------------------------------
-- 修改刚刚查询的数据
==> Preparing: update user SET name = ? where id = ?
==> Parameters: 冯七七(String), 2(Integer)
<== Updates: 1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@236e3f4e]
-- 再次执行查询语句 查询刚刚修改过的数据
==> Preparing: select * from user where id = ?
==> Parameters: 2(Integer)
<== Columns: id, name, pwd
<== Row: 2, 冯七七, 234
<== Total: 1
Users(id=2, name=冯七七, pwd=234)
false-- 数据发生改变
得出结论,数据在执行select之后会将查询到的数据保存在缓存中,以便下次直接使用
对于增删改则会在完成之后刷新缓存,刷新之后如果需要获取数据智能再次查询数据库
缓存失效场景
- 查询不同数据时,自然无法从缓存中直接拿到。
- 增删改操作可能会改变原数据,所以一定会刷新缓存
- 手动清理缓存:
sqlSession.clearCache();
- 创建不同的sqlSession对象查询
二级缓存
一级缓存是默认开启的,但是由于一级缓存作用域太低,所以诞生二级缓存
二级缓存就是全局缓存,它对应于一个namespace命名空间级别。只要开启了二级缓存,在用一个Mapper下就始终有效
工作机制:
- 一个会话查询一条数据,查询成功后该数据会存放在一级缓存中
- 如果当前会话关闭了,则其对应的一级缓存消失。
- 如果开启了二级缓存,那么一级缓存消失后,其中的数据就会被保存到二级缓存中
- 当新开的会话去查询同一数据时,就会从二级缓存中拿到
开启全局缓存
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。
有效值true | false 默认值 true
但通常我们会在settings中显式的开启
<setting name=" cacheEnabled " value="true"/>
然后需要在想要使用二级缓存的Mapper.xml文件中配置cache
<!-- 开启 使用默认参数 -->
<cache/>
<!-- 自定义参数 -->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
上述eviction
是指驱逐策略,FIFO先进先出,按照对象进入缓存的顺序移出
flushInterval
为刷新缓存的间隔时间,size
为最大缓存容量,readOnly
是否设置为只读
二级缓存示例
要注意的一点是,只有在一级缓存销毁之后。sqlSession才会将它缓存的东西交给二级缓存
// 测试二级缓存
@Test
public void testGetUserById(){
// 创建两个sqlSession会话
SqlSession sqlSession1 = MyBatisUtils.getSqlSession();
SqlSession sqlSession2 = MyBatisUtils.getSqlSession();
// selSession1查询一次
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
Users user = mapper1.getUserById(2);
System.out.println(user);
sqlSession1.close();// 关闭sqlSession1
System.out.println("---------------------------------------------");
// selSession2查询与sqlSession相同的数据
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
Users user1 = mapper2.getUserById(2);
System.out.println(user1);
System.out.println(user==user1);
sqlSession2.close();
}
打印日志如下:
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@55536d9e]
-- sqlSession1执行sql查询数据
==> Preparing: select * from user where id = ?
==> Parameters: 2(Integer)
<== Columns: id, name, pwd
<== Row: 2, 冯七七, 234
<== Total: 1
Users(id=2, name=冯七七, pwd=234)
-- 回收sqlSession1到链接池
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@55536d9e]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@55536d9e]
Returned connection 1431530910 to pool.
---------------------------------------------
Cache Hit Ratio [com.yuqu.dao.UserMapper]: 0.5
-- sqlSession查询数据结果
Users(id=2, name=冯七七, pwd=234)
true
很明显,sqlSession1查询到的数据首先保存在了自己的缓存中,也就是一级缓存。那么关闭sqlSession1之后,数据被交给到二级缓存。此时sqlSession再次查询相同数据,则会直接在二级缓存中拿到
一个问题:
上文提到了妖使用二级缓存则必须在对应的Mapper.xml文件中配置cache标签。一种是隐式参数第一种,采用这种方式,就必须让实体类pojo实现serializable接口,否则会报出异常
java.io.NotSerializableException: com.yuqu.pojo.Users
如果采用自定义参数形式,就不需要实现Serializable接口。因为cache中有一个参数为eviction
驱逐策略直接就规定了缓存中的数据读/写的规则。
但是通常无论是否采用自定义参数,都会将实体类实现序列化接口