系列文章
SSM之MyBatis 01 —— 第一个MyBatis程序、增删改查(模糊查询)
SSM之MyBatis 02 —— 配置文件说明、日志工厂、分页(Limit和RowBounds)
SSM之MyBatis 03 —— 使用注解开发、Lombok、多对一&一对多处理
SSM之MyBatis 04 —— 动态SQL、缓存Cache
文章目录
十二、动态SQL
动态SQL就是可以根据不同条件生成不同的SQL语句,在之前的smbms项目中,我们都是利用StringBuffer进行SQL语句的拼接,而MyBatis动态SQL可以避免这种麻烦,使用起来和JSTL类似。
所谓的动态SQL,其本质还是SQL语句,只是我们可以在SQL层面去执行逻辑代码(例如if)
1、搭建环境
SQL语句
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
1、创建基础工程,编写工具类
package com.zcy.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory = null;
static{
String resource = "mybatis-config.xml";
try {
InputStream is = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession(true);
}
}
IdUtils.java 通过UUID获得随机且唯一的id(实际项目都不会是从1一直加,而是随机ID号)
package com.zcy.utils;
import org.junit.Test;
import java.util.UUID;
public class IdUtils {
public static String getId(){
return UUID.randomUUID().toString().replace("-", "");
}
@Test
public void test(){
System.out.println(getId());
}
}
2、编写配置文件
<?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="db.properties">
<property name="name" value="root"/>
<property name="pwd" value="123456"/>
</properties>
<!-- 配置日志 -->
<settings>
<!-- 使用日志,需要导包log4j -->
<setting name="logImpl" value="LOG4J"/>
<!-- 开启驼峰命名自动映射:例如 实体类中属性名 userName <-> 数据库字段 user_name -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- 别名 -->
<typeAliases>
<typeAlias type="com.zcy.pojo.Blog" alias="Blog"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${name}"/>
<property name="password" value="${pwd}"/>
</dataSource>
</environment>
</environments>
<!-- 注册mapper -->
<mappers>
<mapper class="com.zcy.dao.BlogMapper"/>
</mappers>
</configuration>
3、编写实体类
package com.zcy.pojo;
import lombok.Data;
import java.util.Date;
@Data
public class Blog {
private String id;
private String title;
private String author;
//和数据库字段不一致,create_Time。在核心配置文件中开启驼峰命名自动映射
private Date createTime;
private int views;
}
4、编写实体类对应的Mapper接口和Mapper.xml
package com.zcy.dao;
import com.zcy.pojo.Blog;
public interface BlogMapper {
//插入数据
public int addBlog(Blog blog);
}
<?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.zcy.dao.BlogMapper">
<!-- 插入数据 -->
<insert id="addBlog" parameterType="Blog">
insert into blog (id, title, author, create_time, views)
values (#{id},#{title},#{author},#{createTime},#{views})
</insert>
</mapper>
5、测试
package com.zcy.dao;
import com.zcy.pojo.Blog;
import com.zcy.utils.IdUtils;
import com.zcy.utils.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.Date;
public class BlogTest {
@Test
public void addBlog(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
String id = IdUtils.getId();
Blog blog = new Blog(id, "Java如此简单", "小白",new Date(), 100);
blogMapper.addBlog(blog);
id = IdUtils.getId();
blog = new Blog(id, "MyBatis如此简单", "小红",new Date(), 300);
blogMapper.addBlog(blog);
id = IdUtils.getId();
blog = new Blog(id, "Spring如此简单", "小白",new Date(), 500);
blogMapper.addBlog(blog);
id = IdUtils.getId();
blog = new Blog(id, "微服务如此简单", "小白",new Date(), 250);
blogMapper.addBlog(blog);
sqlSession.close();
}
}
2、IF
使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分,比如当我们想查询某个记录时,要求id为xxx,并且姓名为xxx,或者只输入id或者只输入姓名,这种多场景查询。
BlogMapper.java
public interface BlogMapper {
//插入数据
public int addBlog(Blog blog);
//查询数据,多个参数用Map(这里没用泛型也可以)
public List<Blog> queryBlogIF(Map map);
}
where 标签等同于where,并且假如我们只满足第二个if,它会自动帮我们去掉and或or。
if标签,test是必填参数,当test里的条件满足时,就追加if标签里的SQL。
BlogMapper.xml
<!-- 查询数据 -->
<!-- map是Map的别名 -->
<select id="queryBlogIF" parameterType="map" resultType="Blog">
select * from blog
<where>
<if test="title != null">
title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
<if test="views != null">
and views >= #{views}
</if>
</where>
</select>
<!--
事实上我们当然也可以写成
select * from blog where 1=1
<if test="title != null">
and title = #{title}
</if>
但1=1这种方式不正规
-->
BlogTest.java
@Test
public void selectBlog(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
//通过Map添加多个参数,然后select里的if会进行判断并追加SQL
HashMap map = new HashMap();
//map.put("title","Java如此简单");
map.put("author", "小白");
map.put("views", 300);
List<Blog> blogs = blogMapper.queryBlogIF(map);
for (Blog blog : blogs)
System.out.println(blog);
sqlSession.close();
}
结果:可以发现,author前面没有and,因为where标签自动帮我们去掉了
3、choose(when, otherwise)
choose相当于Java的switch,它只会选择一个执行,而前面的if是满足条件都会执行。而when相当于switch的case,otherwise相当于default。
BlogMapper.java
//查询数据 choose(when,otherwise)
public List<Blog> queryBlogChoose(Map map);
BlogMapper.xml
<!-- 查询数据 -->
<select id="queryBlogChoose" parameterType="map" resultType="Blog">
select * from blog
<where>
<choose>
<when test="title != null">title = #{title}</when>
<when test="author != null">author = #{author}</when>
<otherwise>views = #{views}</otherwise>
</choose>
</where>
</select>
BlogTest.java
@Test
public void selectBlog(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
HashMap map = new HashMap();
map.put("author", "小白");
map.put("views", 300);
List<Blog> blogs = blogMapper.queryBlogChoose(map);
for (Blog blog : blogs)
System.out.println(blog);
sqlSession.close();
}
结果:可以发现,测试中写了author和views,但结果中的SQL只有author,没有追加views
4、trim(where、set)
where标签就如同前面if里的使用,它替代SQL中的where,同时会帮我们去掉多余的and、or。
set则是用在update中,会帮我们去掉多余的逗号。
BlogMapper.java
//更新
public int updateBlog(Map map);
BlogMapper.xml
<!-- 更新数据 -->
<update id="updateBlog" parameterType="map">
update blog
<set>
<!-- 这里要有逗号,不然满足两个if时,SQL语法会有错 -->
<if test="title != null">title = #{title},</if>
<if test="author != null">author = #{author}</if>
</set>
where id = #{id}
</update>
BlogTest.java
@Test
public void updateBlog(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
HashMap map = new HashMap();
map.put("author", "小新");
map.put("id","2b8a9e125ae9475b864c2032746caa97");
blogMapper.updateBlog(map);
sqlSession.close();
}
结果:可以发现auther后面没有逗号,因为set自动帮我们去掉了
trim
trim相当于可以自定义where和set这种功能的标签,但一般用where和set就够用了。
<!-- 等价于where -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
<!-- 等价于set -->
<trim prefix="SET" suffixOverrides=",">
...
</trim>
5、SQL片段
可以将SQL语句抽取出来,方便复用
//原本的样子
<select id="queryBlogIF" parameterType="map" resultType="Blog">
select * from blog
<where>
<if test="title != null">
title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
<if test="views != null">
and views >= #{views}
</if>
</where>
</select>
//抽取SQL后的样子
<!-- id作为标识,include标签引用 -->
<sql id="if-title-author-views">
<if test="title != null">
title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
<if test="views != null">
and views >= #{views}
</if>
</sql>
<select id="queryBlogIF" parameterType="map" resultType="Blog">
select * from blog
<where>
<include refid="if-title-author-views"/>
</where>
</select>
**注意事项:**sql片段里最好不要存在where标签,因为它会优化sql,比较复杂,SQL片段里最好放简单的,比如if判断。
6、Foreach
考虑下面的SQL
select * from blog where 1=1 and (id=2 or id=3)
这种SQL我们就需要用到foreach,因为当我们用Map作为参数传进来的时候,它只有一个键值对能叫做id,而上面的SQL中需要判断两个id。
BlogMapper.java
//查询数据 foreach
public List<Blog> queryBlogForeach(Map map);
BlogMapper.xml
<!-- foreach 相当于便利传递进来的集合,对集合每个值进行操作
下面就等价于select * from blog where (id=2 or id=3)
open:表示开头
separator:表示分隔符
close:表示结尾
collection:表示传递进来的集合,item:集合中的每个元素
-->
<select id="queryBlogForeach" parameterType="map" resultType="Blog">
select * from blog
<!-- where标签自动去掉and -->
<where>
<foreach item="id" collection="ids" open="and (" separator="or" close=")">
id = #{id}
</foreach>
</where>
</select>
BlogTest.java
@Test
public void queryBlogForeach(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("2f0a4ca699e24dfea15bd90807874749");
arrayList.add("2b8a9e125ae9475b864c2032746caa97");
HashMap map = new HashMap();
map.put("ids",arrayList);
List<Blog> blogs = blogMapper.queryBlogForeach(map);
for (Blog blog : blogs)
System.out.println(blog);
sqlSession.close();
}
建议:先在MySQL写好SQL语句,再将其修改为对应的动态SQL
十三、缓存
13.1、简介
- 什么是缓存[Cache]?
- 存在内存中的临时数据
- 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,而是从缓存中查询,从而提高查询效率,解决高并发系统的性能问题。
- 为什么使用缓存?
- 减少和数据库的交互次数,减少系统开销,提高系统效率
- 什么样的数据能使用缓存?
- 经常查询且不会经常改变的数据
- 经常查询且不会经常改变的数据
13.2、MyBatis缓存
- MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
- MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存,只在SqlSession开启到它关闭期间有效)
- 二级缓存需要手动开启和配置,它是基于namespace级别的缓存。
- 为了提高扩展性,MyBatis定义了缓存接口Cache,我们可以通过实现Cache接口自定义二级缓存。
13.3、一级缓存
一级缓存作用不太大,它底层就是一个Map集合。
- 一级缓存也叫本地缓存:SqlSession连接时自动开启
- 有效期从SqlSession打开到关闭,数据存在本地
- 后面如果需要获取相同的数据,直接会从缓存中获取,不再查询数据库
- 一级缓存失效:
- 查询不同的东西
- 增删改操作,会改变原来数据,所以必定刷新缓存
- 查询不同的mapper.xml
- 手动清理
测试代码
@Test
public void testCache(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println("--------------------------");
User user1 = userMapper.getUserById(1);
User user2 = userMapper.getUserById(1);
System.out.println(user1==user2);
System.out.println("--------------------------");
User user3 = userMapper.getUserById(2);
userMapper.updateUser(new User(3, "123", "1234567"));
//只要增删改都会刷新一级缓存
User user4 = userMapper.getUserById(2);
//手动刷新缓存 sqlSession.clearCache();
System.out.println(user3==user4);
System.out.println("--------------------------");
sqlSession.close();
}
结果:可以发现第一次和第二次查询得到的User是同一个对象,而第三次和第二次中间进行了更新操作,刷新了缓存,所以第三次和第四次的对象不同。
13.4、二级缓存
-
二级缓存也叫全局缓存,一级缓存的作用域太低,所以诞生了二级缓存。
-
基于namespace级别的缓存,一个命名空间对应一个二级缓存
-
工作机制:
- 一个会话查询一条数据,这个数据会被放在当前会话的一级缓存中
- 如果当前会话关闭,这个会话对应的一级缓存就没了。开启二级缓存后,会话关闭了,一级缓存的数据会被放在二级缓存中。
- 新会话会到二级缓存中获取数据。
- 不同mapper查出的数据会放在自己对应的缓存(map)中。
步骤:
-
核心配置文件中开启全局缓存
<settings> <!-- 开启全局缓存 --> <setting name="cacheEnabled" value="true"/> </settings>
-
在mapper.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.zcy.dao.UserMapper"> <!-- 这样就算使用了,但也可以配置参数 --> <!-- <cache/> --> <!-- 配置参数 eviction:缓存清除的策略包括 LRU-最近最少使用、FIFO先进先出、SOFT、WEAK flushInterval:刷新时间60秒,最多存512个对象,返回的对象是只读 --> <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> <select id="getUserById" resultType="user"> select * from user where id = #{id} </select> <update id="updateUser" parameterType="user"> update user set name=#{name}, password=#{password} where id=#{id} </update> </mapper>
-
测试
@Test public void testCache(){ SqlSession sqlSession1 = MyBatisUtils.getSqlSession(); UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class); User user1 = userMapper1.getUserById(1); sqlSession1.close(); SqlSession sqlSession2 = MyBatisUtils.getSqlSession(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class); User user2 = userMapper2.getUserById(1); sqlSession2.close(); System.out.println(user1==user2); }
结果:
小结:
- 二级缓存只在同一个mapper下有效
- 所有数据都会先放在一级缓存,SqlSession关闭后才会转存到二级缓存
- 实体类尽量实现序列化,实现Serializable接口