MyBatis動(dòng)態(tài)SQL與緩存原理深入分析
動(dòng)態(tài)SQL
為什么叫做動(dòng)態(tài)SQL:因?yàn)樵诔绦驁?zhí)行中,mybatis提供的sql可以根據(jù)用戶提供的字段數(shù)量、類型,合理的選擇對(duì)應(yīng)的執(zhí)行sql。正是這一動(dòng)態(tài)的選擇特性,極大的優(yōu)化了使用JDBC的代碼冗余。
根據(jù)不同條件生成不同的sql語句執(zhí)行
環(huán)境準(zhǔn)備
以博客表為例:
create table `blog`(
`id` varchar(50) primary key comment '博客ID',
`title` varchar(100) not null comment '博客標(biāo)題',
`author` varchar(50) not null comment '博客作者',
`create_time` datetime not null comment '創(chuàng)建時(shí)間',
`view` int not null comment '瀏覽量'
);
Blog實(shí)體:
@Data
@AllArgsConstructor
public class Blog {
private String id;
private String title;
private String author;
private Date creatTime;
private int views;
}
IDutils,用于隨機(jī)生成的ID名稱
public static String getId(){
return UUID.randomUUID().toString().replaceAll("-","");
}
IF語句
以上述搭建的環(huán)境為例,當(dāng)我們需要查詢博客時(shí),如果用戶指定了搜索搜索字段那就根據(jù)該字段查找,如果沒有指定那就查詢?nèi)俊H绻闷胀ǖ膕ql語句實(shí)現(xiàn),需要我們?cè)贘ava程序中進(jìn)行判斷,但是MyBatis提供了動(dòng)態(tài)SQL,我們就可以利用內(nèi)置的IF標(biāo)簽來實(shí)現(xiàn):
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>
測(cè)試:
@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中直接對(duì)title和author字段進(jìn)行判斷,如果非空則執(zhí)行拼接sql,反之查詢?nèi)?/p>
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>
此時(shí)當(dāng)上述兩個(gè)if滿足任一時(shí),sql拼接后變成:
select * from blog where and author = #{author}這是不符合sql語法規(guī)則的。對(duì)此MyBatis提供了where標(biāo)簽來處理這種情況。
<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元素只會(huì)在它的任一子元素返回內(nèi)容時(shí),才會(huì)在sql中插入where子句。如果返回的sql開頭為and 或on,where標(biāo)簽會(huì)自動(dòng)將其抹去
set
sql中更新語句update在mybatis常用set標(biāo)簽來判定都需要更新哪些字段,如果用戶設(shè)置了新的該字段屬性,則會(huì)在set檢測(cè)到,從而執(zhí)行更新語句
并且set子句會(huì)動(dòng)態(tài)的在行首添加上set關(guān)鍵字,包括刪除額外的逗號(hào)
<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包含四個(gè)屬性:
prefix前綴、prefixOverrides前綴覆蓋、suffix后綴、suffixOverrides后綴覆蓋
當(dāng)where和set不能得到預(yù)期的結(jié)果時(shí),可以使用trim進(jìn)行配置。也可以直接使用trim實(shí)現(xiàn)和where、set相同的效果:
<!-- trim實(shí)現(xiàn)set -->
<trim prefix="set" suffixOverrides=",">
<if test="title != null">
title = #{title},
</if>
<if test="author != null">
author = #{author},
</if>
</trim>
<!-- trim實(shí)現(xiàn)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標(biāo)簽,類似于Java中的switch語句。當(dāng)我們不想要執(zhí)行全部的sql,而只是選擇性的去執(zhí)行對(duì)應(yīng)的sql。
三者的關(guān)系類似于switch–>choose、case–>when、default–>otherwise
BlogMapper.xml編譯sql
<select id="queryBlogChoose" parameterType="map" resultType="blog">
select * from blog
<where>
<choose>
<!--title不為null執(zhí)行-->
<when test="title != null">
title = #{title}
</when>
<!--author不為null執(zhí)行-->
<when test="author != null">
and author = #{author}
</when>
<!--默認(rèn)執(zhí)行-->
<otherwise>
and view != 0
</otherwise>
</choose>
</where>
</select>
sql片段
利用sql標(biāo)簽,抽離重復(fù)代碼。在需要使用的地方使用include標(biāo)簽直接引入即可
<!-- 抽離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可以在動(dòng)態(tài)sql中對(duì)集合進(jìn)行遍歷
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集合存儲(chǔ)list集合交給foreach,此處collection通過鍵“ids”獲取list,item為值,open為拼接sql的開始,close為拼接sql的結(jié)束,separator表示分隔符
緩存
什么是緩存?
緩存是存在于內(nèi)存中的臨時(shí)數(shù)據(jù)
使用緩存可以減少和數(shù)據(jù)庫的交互次數(shù),提高數(shù)據(jù)庫性能和執(zhí)行效率
官網(wǎng)給出:
- 映射語句文件中的所有 select 語句的結(jié)果將會(huì)被緩存。
- 映射語句文件中的所有 insert、update 和 delete 語句會(huì)刷新緩存。
- 緩存會(huì)使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。
- 緩存不會(huì)定時(shí)進(jìn)行刷新(也就是說,沒有刷新間隔)。
- 緩存會(huì)保存列表或?qū)ο螅o論查詢方法返回哪種)的 1024 個(gè)引用。
- 緩存會(huì)被視為讀/寫緩存,這意味著獲取到的對(duì)象并不是共享的,可以安全地被調(diào)用者修改,而不干擾其他調(diào)用者或線程所做的潛在修改。
一級(jí)緩存
也叫做本地緩存,對(duì)應(yīng)MyBatis中的sqlSession。
一級(jí)緩存是默認(rèn)開啟的,作用域僅在sqlSession中有效
緩存示例
用戶user表id查詢兩次相同數(shù)據(jù)示例:
@Select("select * from user where id = #{id}")
Users getUserById(@Param("id") int id);
// 測(cè)試
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();
}
打印效果分析:

上述程序分別調(diào)用兩次getUserById方法,如果沒有緩存機(jī)制那么最終應(yīng)該會(huì)執(zhí)行兩次查詢sql來返回?cái)?shù)據(jù),但是根據(jù)日志可以看到最終只執(zhí)行了一次sql。 這說明,第一次查詢到的數(shù)據(jù)就已經(jīng)存放在了緩存當(dāng)中,而第二次執(zhí)行查詢時(shí)將會(huì)直接從緩存中獲取,不再進(jìn)入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數(shù)據(jù)
Users user1 = mapper.getUserById(2);
System.out.println(user1);
System.out.println("-----------------------------------------------");
// 修改id=2數(shù)據(jù)
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("name","馮七七");
map.put("id",2);
int i = mapper.updateUserInfo(map);
sqlSession.commit();
// 第二次查詢id=2數(shù)據(jù)
Users user2 = mapper.getUserById(2);
System.out.println(user2);
System.out.println(user1==user2);
sqlSession.close();
}
首先第一次查詢數(shù)據(jù),查詢完之后調(diào)用修改方法將name修改為“ 馮七七 ” 然后再次執(zhí)行查詢語句
日志分析:
Created connection 594427726.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@236e3f4e]
-- 第一次執(zhí)行查詢sql 數(shù)據(jù)保存在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)
-----------------------------------------------
-- 修改剛剛查詢的數(shù)據(jù)
==> Preparing: update user SET name = ? where id = ?
==> Parameters: 馮七七(String), 2(Integer)
<== Updates: 1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@236e3f4e]
-- 再次執(zhí)行查詢語句 查詢剛剛修改過的數(shù)據(jù)
==> 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-- 數(shù)據(jù)發(fā)生改變
得出結(jié)論,數(shù)據(jù)在執(zhí)行select之后會(huì)將查詢到的數(shù)據(jù)保存在緩存中,以便下次直接使用
對(duì)于增刪改則會(huì)在完成之后刷新緩存,刷新之后如果需要獲取數(shù)據(jù)智能再次查詢數(shù)據(jù)庫
緩存失效場(chǎng)景
- 查詢不同數(shù)據(jù)時(shí),自然無法從緩存中直接拿到。
- 增刪改操作可能會(huì)改變?cè)瓟?shù)據(jù),所以一定會(huì)刷新緩存
- 手動(dòng)清理緩存:
sqlSession.clearCache(); - 創(chuàng)建不同的sqlSession對(duì)象查詢
二級(jí)緩存
一級(jí)緩存是默認(rèn)開啟的,但是由于一級(jí)緩存作用域太低,所以誕生二級(jí)緩存
二級(jí)緩存就是全局緩存,它對(duì)應(yīng)于一個(gè)namespace命名空間級(jí)別。只要開啟了二級(jí)緩存,在用一個(gè)Mapper下就始終有效
工作機(jī)制:
- 一個(gè)會(huì)話查詢一條數(shù)據(jù),查詢成功后該數(shù)據(jù)會(huì)存放在一級(jí)緩存中
- 如果當(dāng)前會(huì)話關(guān)閉了,則其對(duì)應(yīng)的一級(jí)緩存消失。
- 如果開啟了二級(jí)緩存,那么一級(jí)緩存消失后,其中的數(shù)據(jù)就會(huì)被保存到二級(jí)緩存中
- 當(dāng)新開的會(huì)話去查詢同一數(shù)據(jù)時(shí),就會(huì)從二級(jí)緩存中拿到
開啟全局緩存
cacheEnabled 全局性地開啟或關(guān)閉所有映射器配置文件中已配置的任何緩存。
有效值true | false 默認(rèn)值 true
但通常我們會(huì)在settings中顯式的開啟
<setting name=" cacheEnabled " value="true"/>
然后需要在想要使用二級(jí)緩存的Mapper.xml文件中配置cache
<!-- 開啟 使用默認(rèn)參數(shù) -->
<cache/>
<!-- 自定義參數(shù) -->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
上述eviction是指驅(qū)逐策略,F(xiàn)IFO先進(jìn)先出,按照對(duì)象進(jìn)入緩存的順序移出
flushInterval為刷新緩存的間隔時(shí)間,size為最大緩存容量,readOnly是否設(shè)置為只讀
二級(jí)緩存示例
要注意的一點(diǎn)是,只有在一級(jí)緩存銷毀之后。sqlSession才會(huì)將它緩存的東西交給二級(jí)緩存
// 測(cè)試二級(jí)緩存
@Test
public void testGetUserById(){
// 創(chuàng)建兩個(gè)sqlSession會(huì)話
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();// 關(guān)閉sqlSession1
System.out.println("---------------------------------------------");
// selSession2查詢與sqlSession相同的數(shù)據(jù)
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執(zhí)行sql查詢數(shù)據(jù)
==> 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查詢數(shù)據(jù)結(jié)果
Users(id=2, name=馮七七, pwd=234)
true
很明顯,sqlSession1查詢到的數(shù)據(jù)首先保存在了自己的緩存中,也就是一級(jí)緩存。那么關(guān)閉sqlSession1之后,數(shù)據(jù)被交給到二級(jí)緩存。此時(shí)sqlSession再次查詢相同數(shù)據(jù),則會(huì)直接在二級(jí)緩存中拿到
一個(gè)問題:
上文提到了妖使用二級(jí)緩存則必須在對(duì)應(yīng)的Mapper.xml文件中配置cache標(biāo)簽。一種是隱式參數(shù)第一種,采用這種方式,就必須讓實(shí)體類pojo實(shí)現(xiàn)serializable接口,否則會(huì)報(bào)出異常
java.io.NotSerializableException: com.yuqu.pojo.Users
如果采用自定義參數(shù)形式,就不需要實(shí)現(xiàn)Serializable接口。因?yàn)閏ache中有一個(gè)參數(shù)為eviction驅(qū)逐策略直接就規(guī)定了緩存中的數(shù)據(jù)讀/寫的規(guī)則。
但是通常無論是否采用自定義參數(shù),都會(huì)將實(shí)體類實(shí)現(xiàn)序列化接口
到此這篇關(guān)于MyBatis動(dòng)態(tài)SQL與緩存原理深入分析的文章就介紹到這了,更多相關(guān)MyBatis動(dòng)態(tài)SQL與緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項(xiàng)目從18.18M瘦身到0.18M的實(shí)現(xiàn)
本文主要介紹了SpringBoot項(xiàng)目從18.18M瘦身到0.18M的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
Java實(shí)現(xiàn)stream的三個(gè)常用方式(toMap,groupingBy,findFirst)
本文主要介紹了Java實(shí)現(xiàn)stream的三個(gè)常用方式,主要包括toMap,groupingBy,findFirst,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10
SpringBoot2使用WebFlux函數(shù)式編程的方法
這篇文章主要介紹了SpringBoot2使用WebFlux函數(shù)式編程的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
Java7之forkjoin簡(jiǎn)介_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Java7引入了Fork Join的概念,來更好的支持并行運(yùn)算。接下來通過本文給大家分享Java7之forkjoin簡(jiǎn)介,感興趣的朋友一起看看吧2017-06-06
java實(shí)戰(zhàn)技巧之if-else代碼優(yōu)化技巧大全
代碼中如果if-else比較多,閱讀起來比較困難,維護(hù)起來也比較困難,很容易出bug,下面這篇文章主要給大家介紹了關(guān)于java實(shí)戰(zhàn)技巧之if-else代碼優(yōu)化技巧的相關(guān)資料,需要的朋友可以參考下2022-02-02
Java單元測(cè)試Powermockito和Mockito使用總結(jié)
公司單元測(cè)試框架選用了Junit 4.12,Mock框架選用了Mockito和PowerMock,本文主要介紹了Java單元測(cè)試Powermockito和Mockito使用總結(jié),感興趣的可以了解一下2021-09-09

