Mybatis框架动态SQL与缓存
一、动态SQL
什么是动态SQL
- 动态SQL就是指根据不同的条件生产不同的SQL语句
-
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的
空格
,还要注意去掉列表最后一个列名的逗号
。利用动态 SQL,可以彻底摆脱这种痛苦。 -
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
-
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
对象导航图语言(Object Graph Navigation Language),简称OGNL,是应用于Java中的一个开源的表达式语言(Expression Language)
在这里我们主要学习四种(if、choose、trim 、foreach)
1. 搭建环境
(1)数据库搭建
CREATE TABLE `blog`(
`id` VARCHAR(50) NOT NULL COMMENT '博客id',
`title` VARCHAR(100) NOT NULL COMMENT '博客标题',
`author` VARCHAR(30) NOT NULL COMMENT '博客作者',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`views` INT(30) NOT NULL COMMENT '浏览量'
)ENGINE=INNODB DEFAULT CHARSET=utf8;
(2)创建类
1) Blog.class
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Blog {
private String id; //博客id
private String title; //博客标题
private String author; //作者
private Date createTime; //创建时间
private int views; //点赞量
}
2) BlogMapper.class
public interface BlogMapper {
//插入数据
int addBlog(Blog blog);
//使用If标签查询
List<Blog> queryBlogIF(Map map);
//使用choose标签查询
List<Blog> queryBlogChoose(Map map);
//使用set标签更行
int updateBlog(Map map);
//查询第1-2-3号记录的博客
List<Blog> queryBlogForeach(Map map);
}
(3)核心配置文件(mybatis-config.xml)
<?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>
<!-- 使用外部文件导入数据 -->
<properties resource="database.properties" />
<!-- 配置SQL运行的每一步过程 -->
<settings>
<!-- 是否开启驼峰命名自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 标准的日志工厂实现 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- <setting name="logImpl" value="LOG4J"/> -->
</settings>
<!-- 给实体类起别名 -->
<typeAliases>
<package name="com.xxx.pojo"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/xxx/dao/BlogMapper.xml" />
</mappers>
</configuration>
(4)连接数据库字段值文件(database.properties)
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
2. 测试
(1)If
在对应映射接口文件中(BlogMapper.xml),添加一下字段
<select id="queryBlogIF" parameterType="map" resultType="Blog">
select * from blog where 1=1
<if test="title != null">
and title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
</select>
<!--由于存在where 1 = 1,不正规,往下看-->
测试
@Test
public void testqueryBlogIF() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
//由于设置方法时,参数设置为Map类型,需要new 一个HashMap
// HashMap 是一个散列表,它存储的内容是键值对(key-value)映射
Map<String,String> map = new HashMap<>();
//根据参数是否为null,我们来设置SQL语句需要传递的参数
map.put("title","Java入门2");
List<Blog> blogs = mapper.queryBlogIF(map);
for (Blog blog : blogs) {
System.out.println("=============="+blog);
}
}
(2)choose (when,otherwise)
同样增加以下字段
这里的choose和我们Java代码中的switch类似,选择条件符合的一项作为查询,当然,这里必须注意的是,查询时根据从上到下的条件查询,一旦符合其中一个条件,就不再拼接SQL语句
<select id="queryBlogChoose" parameterType="map" resultType="Blog">
select * from blog
<where>
<choose>
<when test="title != null">
title = #{title}
</when>
<when test="author != null">
and author = #{author}
</when>
<otherwise>
and views = #{views}
</otherwise>
</choose>
</where>
</select>
测试
@Test
public void testqueryBlogChoose() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
Map<String,Object> map = new HashMap<String,Object>();
map.put("author","李四");
map.put("title","Java入门");
List<Blog> blogs = mapper.queryBlogChoose(map);
for (Blog blog : blogs) {
System.out.println("=============="+blog);
}
}
数据库数据
可以看到我们传递了两个参数,但是只有一个参数拼接到SQL语句中。给出数据库的数据是为了验证我们传参的两个值到底有没有被拼接上
(3)trim (where,set)
where
- 刚刚在使用
if
标签时,存在了where 1 = 1的sql语句,让条件为永真,这是当我们条件都不存在的时候,需要完整的SQL语句进行查询,不然就会出现“ select * from blog where (后面都是空的)”SQL语句异常 - 这时,我们的where标签就可以在这里使用了
<select id="queryBlogIF" parameterType="map" resultType="Blog">
select * from blog
<where>
<if test="title != null">
and title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
</where>
</select>
set
这个例子中,set元素会动态地在首行插入SET关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)
在我们XML文件中添加一下字段
<update id="updateBlog" parameterType="map">
update blog
<set>
<if test="title != null">
title = #{title},
</if>
<if test="author != null">
author = #{author}
</if>
</set>
where id = #{id}
</update>
测试
@Test
public void testupdateBlog() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
Map<String,Object> map = new HashMap<String,Object>();
map.put("id",1);
map.put("title","Java入门2");
int i = mapper.updateBlog(map);
if (i > 0) {
System.out.println("更新成功!");
}
}
(4)SQL片段
有的时候,我们可能会将一些功能的部分抽取出来,方便复用
- 使用SQL标签抽取公共的部分
- 在需要使用的地方使用include标签引用即可
注意事项: - 最好基于单表来定义SQL片段
- 不要存在where标签
3. 拓展(foreach)
- 原生SQL语句
# 查询前三条数据
select * from blog where 1=1 and (id=1 or id=2 or id=3)
- 使用foreach元素
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)
和索引(index)变量
。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符
<!-- 传递一个万能的map,这个map中可以存在一个集合 -->
<select id="queryBlogForeach" resultType="Blog" parameterType="map">
select * from blog
<where>
<foreach collection="ids" item="id" open="and (" close=")" separator="or">
id = #{id}
</foreach>
</where>
</select>
<!--
collection: //传递的参数字段名,一般以集合的形式传递参数
item: //集合项,集合中的每一项
open: //起始字符串
close: //结尾字符串
separator: //以or的字符进行分割集合项
#{id}: //集合项的别名
select * from blog where ( id = #{id} or id = #{id} or id = #{id} ....)
(这里本身有一个and,但是有where元素,就会帮我们去掉)
-->
测试
@Test
public void testqueryBlogForeach() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
Map<String,Object> map = new HashMap<String,Object>();
List<Integer> ids = new ArrayList<Integer>();
ids.add(1);
ids.add(2);
ids.add(3);
map.put("ids",ids);
List<Blog> blogs = mapper.queryBlogForeach(map);
for (Blog blog : blogs) {
System.out.println("=============="+blog);
}
}
动态SQL就是在拼接SQL,我们只要保证SQL的正确性,按照SQL的格式,去排列组合就可以了
二、缓存
MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或者特定的数据库,或者特定的表。
基本上就是这样。这个简单语句的效果如下:
- 映射语句文件中的
所有 select 语句的结果
将会被缓存 - 映射语句文件中的
所有 insert、update 和 delete 语句
会刷新缓存 - 缓存会使用最近最少使用算法(LRU,Least Recently Used)算法来清除不需要的缓存
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着
获取到的对象
并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改
- 什么是缓存[Cache]
- 存在内存中的临时数据
- 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题
- 为什么使用缓存
- 减少和数据库的交互次数,减少系统的开销,提高系统效率
- 什么样的数据能使用缓存
- 经常查询并且不经常改变的数据
Mybatis缓存
- Mybatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率
- Mybatis系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有
一级缓存开启
。(SqlSession级别的缓存,也称为本地缓存) 二级缓存
需要手动开启和配置,它是基于namespace级别的缓存- 为了提高扩展性,Mybatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
1. 一级缓存
- 一级缓存也叫本地缓存
- 与数据库同一次会话期间查询到的数据会放在本地缓存中
- 以后如果需要获取相同的数据,直接从缓存中拿,没必要再去查询数据库
测试步骤
- 开启日志(在核心配置文件中)
<!-- 配置SQL运行的每一步过程 -->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
- 测试在一个session中查询两次记录
@Test
public void testQuery(){
//获取不同的会话sqlSession
SqlSession sqlSession1 = MybatisUtils.getSqlSession();
SqlSession sqlSession2 = MybatisUtils.getSqlSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
//第一次查询
User user1 = mapper1.queryUserById(1);
System.out.println(user1);
sqlSession1.close();
System.out.println("===================================================================");
//第二次查询
User user2 = mapper2.queryUserById(1);
System.out.println(user2);
System.out.println(user1==user2);
sqlSession2.close();
}
缓存失效情况:
- 查询使用不同sqlSession对象(注意:上面两个sqlSession虽然不同,但是在使用另外一个会话之前,已经将第一个给关闭了)
- 增删改操作,可能会改变原来的数据,所以必定会刷新缓存
@Test
public void testQuery(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//获取第一个数据
User user1 = mapper.queryUserById(1);
System.out.println(user1);
//更新
mapper.updateUser(new User(1,"123456","Totoro"));
System.out.println("===================================================================");
//更新之后再次获取
User user2 = mapper.queryUserById(1);
System.out.println(user2);
System.out.println(user1==user2);
sqlSession.close();
}
3. 查询不同的xxxMapper.xml(这里就是使用两种xml文件对应一个接口)
4. 手动清理缓存 sqlSession.clearCache()
@Test
public void testQuery(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//获取第一个数据
User user1 = mapper.queryUserById(1);
System.out.println(user1);
//清除缓存
sqlSession.clearCache();
System.out.println("===================================================================");
//更新之后再次获取
User user2 = mapper.queryUserById(1);
System.out.println(user2);
System.out.println(user1==user2);
sqlSession.close();
}
- 一级缓存是默认开启的,只在一次SqlSession中有效,也就是拿到连接到关闭这个区间段
- 一级缓存相当于一个Map
为什么这么说呢?我们来看看刚刚那个清理缓存的方法clearCache()
可以看到,这个方法是在一个名叫SqlSession接口中的一个方法,并且有两个类实现了该方法,我们点击DefaultSqlSession.class这个类
它执行了清除本地缓存,也就是我们的一级缓存
这里它又是名为Executor接口中的一个方法,我们继续点击这个实现了这个接口的类BaseExecutor.class
这里它又执行了当前类的一个方法(本地缓存的清除),我们继续点击这个localCache
发现它是一个受保护的属性类PerpetualCache.class,我们继续点进去看看
可以看到在这个类中有个clearCache()
的方法,方法里是调用当前属性为cache的一个方法clear()
而这个cache属性就是我们当前这个类PerpetualCache.class的一个属性,这个属性的类型就是一个Map类型,而且当前这个类也实现了Cache接口
2. 二级缓存
- 二级缓存也叫全局缓存,一级缓存作用域太低,所以诞生了二级缓存
- 基于namespace级别的缓存,一个名称空间,对应一个二级缓存
测试步骤
(1)开启全局缓存(核心配置文件中)
<!--显示开启全局缓存-->
<setting name="cacheEnabled" value="true"/>
(2)在要使用二级缓存的xxxMapper.xml中开启
<!--在当前xxxMapper.xml中使用二级缓存-->
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
可用的清除策略有
LRU
– 最近最少使用:移除最长时间不被使用的对象。FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。也可以自定义一些参数
代码测试
- 未开启缓存前(使用一级缓存的第一个测试代码)
- 开启缓存后
问题:我们需要将实体类序列化
需要给我们的实体类加上implement Serializable
public class User implements Serializable
小结
- 只要开启了二级缓存,在同一个Mapper下就有效
- 所有的数据都会先放在一级缓存中
- 只有当会话提交,或者关闭的时候,才会提交到二级缓存中
【提示】二级缓存是事务性
的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行flushCache=true
的 insert/delete/update 语句时,缓存会获得更新
flushCache
将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true
3. 缓存原理
4.自定义缓存
Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存
要在程序中使用ehcache
- 第一步:导包
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
- 第二步:配置xxxMapper.xml
在我们mapper中指定使用我们的ehcache缓存实现
<!--在当前Mapper.xml中使用二级缓存-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
- 第三步:创建ehcache.xml文件(注意:文件名不能自定义)
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--
diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置,参数解释如下:
user.home - 用户主目录
user.dir - 用户当前工作目录
java.io.tmpdir - 默认临时文件路径
-->
<diskStore path="./tmpdir/Tmp_EhCache"/>
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="259200"
memoryStoreEvictionPolicy="LRU"/>
<!--
defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略,只能定义一个
-->
<!--
name:缓存名称
maxElementInMemory:缓存最大数目
maxElementOnDisk:硬盘最大存储个数
eternal:对象是否永久有效,一旦设置了,timeout将不起作用
overflowToDisk:是否保存到磁盘,当系统宕机时
diskPersistent:是否缓存虚拟机重启期数据
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)仅当eternal=false对象不是永久有效时使用,可选属性,默认值为0也就是可以
timeToLiveSeconds:设置对象在失效前的允许存活时间(单位:秒)最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用
memoryStoreEvictionPolicy:可选策略 LRU(最近最少使用,默认策略) FIFO(先进先出) LFU(最少访问次数)
-->
<cache
name="cloud_user"
eternal="false"
maxElementsInMemory="5000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="1800"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
之后我们需要写一个类去实现一个叫Cache的接口,一般在Redis(非关系型NOSQL)数据库去实现
具体可参考《SpringBoot整合Shiro部分的ehcache缓存配置》
三、总结
学完Mybatis,还有Mybatis-Plus,如果有什么错误的地方,请大家在下面评论区留言,也希望大家也能完结撒花,给我点点赞!加油!