1. 動態SQL(核心) 1.1 簡介 Mybatis框架的動態SQL技術是一種根據特定條件動態拼裝SQL語句的功能,它存在的意義是為瞭解決拼接SQL語句字元串時的難點問題。 比如: 我們在多條件查詢的時候會寫這樣的語句: select * from sys_user where 1=1 and 再 ...
1. 動態SQL(核心)
1.1 簡介
Mybatis框架的動態SQL技術是一種根據特定條件動態拼裝SQL語句的功能,它存在的意義是為瞭解決拼接SQL語句字元串時的難點問題。
比如: 我們在多條件查詢的時候會寫這樣的語句:
select * from sys_user where 1=1 and
再比如:做更新的時候,我們沒有修改的數據列也執行了更新操作。
1.2 if和where標簽
<!-- List<Emp> select ByCondition(Emp emp); -->
<select id="selectByCondition" resultType="com.hy.bean.Emp">
select emp_id,emp_name,emp_salary from sys_emp <!-- where標簽會自動去掉“標簽體內前面、後面多餘的and/or” --> <where> <!-- 使用if標簽,讓我們可以有選擇的加入SQL語句的片段。這個SQL語句片段是否要加入整個SQL語句,就看if標簽判斷的結果是否為true --> <!-- 在if標簽的test屬性中,可以訪問實體類的屬性,不可以訪問資料庫表的欄位 --> <if test="empName != null"> <!-- 在if標簽內部,需要訪問介面的參數時還是正常寫#{} -->
or emp_name=#{empName} </if> <if test="empSalary > 2000"> or emp_salary>#{empSalary} </if>
<!-- 第一種情況:所有條件都滿足 WHERE emp_name=? or emp_salary>? 第二種情況:部分條件滿足 WHERE emp_salary>? 第三種情況:所有條件都不滿足 沒有where子句 --> </where> </select>
|
1.3 set標簽
需求:實際開發時,對一個實體類對象進行更新。往往不是更新所有欄位,而是更新一部分欄位。此時頁面上的表單往往不會給不修改的欄位提供表單項 。
例如:上面的表單,如果伺服器端接收表單時,使用的是User這個實體類,那麼userName、userBalance、userGrade接收到的數據就是null 。
如果不加判斷,直接用User對象去更新資料庫,在Mapper配置文件中又是每一個欄位都更新,那就會把userName、userBalance、userGrade設置為null值,從而造成資料庫表中對應數據被破壞。
此時需要我們在Mapper配置文件中,對update語句的set子句進行定製,此時就可以使用動態SQL的set標簽。
<update id="updateEmployeeDynamic">
update sys_emp
<!-- set emp_name=#{empName},emp_salary=#{empSalary} -->
<!-- 使用set標簽動態管理set子句,並且動態去掉兩端多餘的逗號 -->
<set>
<if test="empName != null">
emp_name=#{empName},
</if>
<if test="empSalary < 3000">
emp_salary=#{empSalary},
</if>
</set>
where emp_id=#{empId}
</update> |
第一種情況:所有條件都滿足 SET emp_name=?, emp_salary=?
第二種情況:部分條件滿足 SET emp_salary=?
第三種情況:所有條件都不滿足 update t_emp where emp_id=?
沒有set子句的update語句會導致SQL語法錯誤
1.4 trim標簽
使用trim標簽控制條件部分兩端是否包含某些字元
prefix屬性:指定要動態添加的首碼內容
suffix屬性:指定要動態添加的尾碼
prefixOverrides屬性:指定要動態去掉的首碼,使用“|”分隔有可能的多個值
suffixOverrides屬性:指定要動態去掉的尾碼,使用“|”分隔有可能的多個值
下麵例子中,trim標簽內部如果有條件,則where會出現,否則where不出現。
suffixOverrides=“and|or”:整個條件部分,如果在後面有多出來的“and或or”會被自動去掉。
<!-- List<Emp> selectByConditionByTrim(Emp emp) --> <select id="selectByConditionByTrim" resultType="com.hy.bean.Emp"> select emp_id,emp_name,emp_age,emp_salary,emp_gender from sys_emp <!-- prefix屬性指定要動態添加的首碼 --> <!-- suffix屬性指定要動態添加的尾碼 --> <!-- prefixOverrides屬性指定要動態去掉的首碼,使用“|”分隔有可能的多個值 --> <!-- suffixOverrides屬性指定要動態去掉的尾碼,使用“|”分隔有可能的多個值 --> <!-- 當前例子用where標簽實現更簡潔,但是trim標簽更靈活,可以用在任何有需要的地方 --> <trim prefix="where" suffixOverrides="and|or"> <if test="empName != null"> emp_name=#{empName} and </if> <if test="empSalary > 3000"> emp_salary > #{empSalary} and </if> <if test="empAge < 20"> emp_age=#{empAge} or </if> <if test="empGender==’ m’ "> emp_gender=#{empGender} </if> </trim> </select> |
1.5 choose/when/otherwise標簽
在多個分支條件中,僅執行一個。
<!-- List<Emp> selectByConditionByChoose(Emp emp) --> <select id="selectEmployeeByConditionByChoose" resultType="com.hy.bean.Emp"> select emp_id,emp_name,emp_salary from sys_emp where <choose> <when test="empName != null">emp_name=#{empName}</when> <when test="empSalary > 3000">emp_salary > 3000</when> <otherwise>1=1</otherwise> </choose>
<!-- 第一種情況:第一個when滿足條件 where emp_name=? 第二種情況:第二個when滿足條件 where emp_salary < 3000 第三種情況:兩個when都不滿足 where 1=1 執行了otherwise --> </select>
|
1.6 foreach標簽
1.6.1 基本用法
比如:批量插入
abstract public void insertBatch(@Param(“empList”) List<Emp> empList));
<!-- INSERT INTO `sys_emp` VALUES (null, '李冰冰', 'lbb', 'f', 300,default,2), (null, '張彬彬', 'zbb', 'm', 599,default,3), (null, '萬茜', 'wq', 'm', 4000,default,1), (null, '李若彤', 'lrt', 'm', 5000.8,default,1) --> <insert id="insertBatch"> INSERT INTO `sys_emp`(emp_id,emp_name,emp_gender) <foreach collection="empList" item="emp" separator="," open="VALUES" > (null,#{emp.empName},#{emp.empGender}) </foreach> </insert> |
|
我們這裡寫的批量插入的例子本質上是一條SQL語句。
collection屬性:要遍歷的集合,如果介面中方法中使用了@Param ,collection屬性中要用這個名字。
item屬性:遍歷集合的過程中能得到每一個具體對象,在item屬性中設置一個名字,將來通過這個名字引用遍歷出來的對象
separator屬性:指定當foreach標簽的標簽體重覆拼接字元串時,各個標簽體字元串之間的分隔符
open屬性:指定整個foreach迴圈把字元串拼好後,這個字元串整體的前面要添加的字元串
close屬性:指定整個foreach迴圈把字元串拼好後,這個字元串整體的後面要添加的字元串
index屬性:這裡起一個名字,便於後面引用
遍歷List集合,這裡能夠得到List集合的索引值
遍歷Map集合,這裡能夠得到Map集合的key
如果介面形參位置沒有使用@Param註解,而且foreach標簽也沒有使用預設的名稱,則會拋出異常
Caused by:org.apache.ibatis.binding.BindingException: Parameter ‘empList’ not found. Available parameters are[arg0,collection, list]
1.6.2 批量更新
而實現批量更新則需要多條update SQL語句拼起來,並且用分號分開。也就是一次性發送多條SQL語句讓資料庫執行。
此時需要在資料庫連接信息的URL地址中設置
?allowMultiQueries=true
對應的foreach標簽如下:
<!-- int updateBatch(@Param("empList") List<Emp> empList) -->
<update id="updateBatch">
<foreach collection="empList" item="emp" separator=";">
update sys_emp set emp_name=#{emp.empName} where emp_id=#{emp.empId}
</foreach>
</update>
1.6.3 關於foreach標簽的collection屬性
如果沒有給介面中List類型的參數使用@Param註解指定一個具體的名字,那麼在collection屬性中預設可以使用collection或list來引用這個list集合。這一點可以通過異常信息看出來:
Parameter 'empList' not found. Available parameters are [collection, list]
在實際開發中,為了避免隱晦的表達造成一定的誤會,建議使用@Param註解明確聲明變數的名稱,然後在foreach標簽的collection屬性中按照@Param註解指定的名稱來引用傳入的參數。
1.7 sql標簽
1.7.1 抽取重覆的SQL片段
<!-- 使用sql標簽抽取重覆出現的SQL片段 --> <sql id="mySelectSql"> select emp_id,emp_name,emp_age,emp_salary,emp_gender from t_emp </sql> |
1.7.2 引用已抽取的SQL片段
<!-- 使用include標簽引用聲明的SQL片段 --> <include refid="mySelectSql"/> |
2.緩存
2.1 緩存機制
比如:以前農村裝水的水缸
2.2 一級緩存和二級緩存
二級緩存被所有的SqlSession共用,也就是能被訪問的。
一級緩存是SqlSession級別的緩存。在操作資料庫時需要構造 sqlSession對象,在對象中有一個數據結構(HashMap)用於存儲緩存數據。不同的sqlSession之間的緩存數據區域(HashMap)是互相不影響的。
二級緩存是Mapper(namespace)級別的緩存。多個SqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級緩存,二級緩存是跨SqlSession的。
第一次查詢,先將結果放入到一級緩存,等SqlSession提交事務後,再將數據放入到二級緩存。
2.2.1 一級緩存和二級緩存的使用順序
查詢的順序是:
1)先查詢二級緩存,因為二級緩存中可能會有其他程式已經查出來的數據,可以拿來直接使用。
2)如果二級緩存沒有命中,再查詢一級緩存
3)如果一級緩存也沒有命中,則查詢資料庫
SqlSession關閉之前,會將一級緩存中的數據會寫入二級緩存
2.2.2 效用範圍
一級緩存:SqlSession級別
二級緩存:SqlSessionFactory級別
它們之間範圍的大小參考下麵圖:
一個請求中可能包含多個事務 ------ 一個service方法(一個SqlSession)對應一個事務
註意:緩存是用再查詢過程中的,增刪改,反而會破壞緩存。造成緩存和資料庫不一致,所以很多情況,執行了一個增刪改操作,他會把緩存清空。
3. 一級緩存
Mybatis預設開啟了一級緩存
3.1 案例1:測試一級緩存是否存在
為了測試緩存失效,我們需要修改測試類中的代碼。需要關閉SqlSessioin對象,然後重新開,再重新開的話,需要操作SqlSessionFactory。
如何測試一級緩存是否存在???同一個數據查兩次,但是只發了一條SQL語句[給資料庫]。
3.1.1 證明一級緩存存在
package com.hy.mybatis.test;
import java.io.IOException;
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 org.junit.Before; import org.junit.Test;
import com.hy.bean.Emp; import com.hy.mapper.EmpMapper;
public class TestLevelOneCache1 { private SqlSessionFactory sqlSessionFactory; private Logger logger = null; @Before public void init() throws IOException { logger = LoggerFactory.getLogger(this.getClass()); sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml")); }
@Test public void testLevelOneCache1_01() throws IOException { SqlSession session = sqlSessionFactory.openSession(); EmpMapper empMapper = session.getMapper(EmpMapper.class);
Emp emp1 = empMapper.selectById(1); Emp emp2 = empMapper.selectById(1); // 第二次是從緩存中查詢出來的。
logger.info("是否相等:"+ (emp1 == emp2)); //true
session.commit(); session.close(); } } |
只發一條SQL語句。一級緩存預設開啟,不需要做額外的配置。二級緩存需要開啟才有,現在沒有開,所以只有一級緩存。
3.2 案例2:一級緩存失效的情況:
1)同一個SqlSession兩次查詢期間提交了事務
2)同一個SqlSession兩次查詢期間執行了任何一次增刪改操作,不論是夠提交事務,都清空一級緩存
3)同一個SqlSession但是查詢條件發生了變化
4)同一個SqlSession兩次查詢期間手動清空了緩存
5)不是同一個SqlSession
總結:在執行commit,rollback時會清空一級緩存
一級緩存的清除還有以下兩個地方:
1、就是獲取緩存之前會先進行判斷用戶是否配置了flushCache=true屬性(參考一級緩存的創建代碼截圖),如果配置了則會清除一級緩存。 2、MyBatis全局配置屬性localCacheScope配置為Statement時,那麼完成一次查詢就會清除緩存。 |
3.2.1 同一個SqlSession兩次查詢期間提交了事務
public class TestLevelCache1 { private SqlSessionFactory sqlSessionFactory; private Logger logger = null; @Before public void init() throws IOException { logger = LoggerFactory.getLogger(this.getClass()); sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml")); }
//1) 同一個SqlSession兩次查詢期間提交了事務 //1.1 沒有提交事務,使用一級緩存 @Test public void testLevelOneCache1_01 () throws IOException { SqlSession session = sqlSessionFactory.openSession(); EmpMapper empMapper = session.getMapper(EmpMapper.class);
Emp emp1 = empMapper.selectById(1); Emp emp2 = empMapper.selectById(1); // 第二次是從緩存中查詢出來的。
logger.info("是否相等:"+ (emp1 == emp2)); //true
session.commit(); session.close(); }
//1.2 兩次查詢中間,提交了事務,清空一級緩存 @Test public void testLevelOneCache1_02() throws IOException { SqlSession session = sqlSessionFactory.openSession(); EmpMapper empMapper = session.getMapper(EmpMapper.class);
Emp emp1 = empMapper.selectById(1); session.commit(); Emp emp2 = empMapper.selectById(1); // 由於進行了事務的提交,所以又發送了一條查詢語句。
logger.info("是否相等:"+ (emp1 == emp2)); //false session.close(); }
} |
3.2.2 同一個SqlSession兩次查詢期間執行了任何一次增刪改操作,並且提交了事務
@Test public void testLevelOneCache01_4() { SqlSession sqlSession = sqlSessionFactory.openSession();
EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class);
Emp emp1 = empMapper.selectById(1);
//Emp newEmp = new Emp(2L, "範冰冰ILoveYou", null, null, null, null); //empMapper.updateById(newEmp); //更新但是不提交事務
empMapper.deleteById(8); //執行了刪除沒有,提交事務也,重新一遍
Emp emp2 = empMapper.selectById(1); //logger.debug(emp2.toString()); logger.debug("是否相等:" + (emp1 == emp2));
sqlSession.commit(); sqlSession.close(); } |
// 2.2 @Test public void testLevelOneCache2_02() throws IOException { SqlSession session = sqlSessionFactory.openSession();
EmpMapper empMapper = session.getMapper(EmpMapper.class);
Emp emp1 = empMapper.selectById(1); //查詢後,將對象放入一級緩存
empMapper.deleteById(7); //執行了delete操作,但是並沒有提交 session.commit();
Emp emp11 = empMapper.selectById(1); //提交了事務,一級緩存清空 logger.info("是否相等:"+ (emp1 == emp11)); //false
session.commit(); session.close(); } |
第一次發起查詢用戶id為1的用戶信息,先去找緩存中是否有id為1的用戶信息,如果沒有,從資料庫查詢用戶信息,將查詢到的用戶信息存儲到一級緩存中。
如果中間sqlSession去執行插入、更新、刪除,並且commit,清空SqlSession中的一級緩存,這樣做的目的為了讓緩存中存儲的是最新的信息,避免臟讀。
第二次發起查詢用戶id為1的用戶信息,先去找緩存中是否有id為1的用戶信息,緩存中有,直接從緩存中獲取用戶信息。如果沒有,則重新發出一條查詢語句。
3.2.3 同一個SqlSession但是查詢條件發生了變化
// 3. 同一個SqlSession但是查詢條件發生了變化 @Test public void testLevelOneCache3_01() throws IOException { SqlSession session = sqlSessionFactory.openSession();
EmpMapper empMapper = session.getMapper(EmpMapper.class);
Emp emp1 = empMapper.selectById(1); //查詢後,將對象放入一級緩存 //查詢的是emp_id為2的員工信息,一級緩存中並沒有所以重新發出一條SQL語句 Emp emp2 = empMapper.selectById(2); session.commit(); session.close(); } |
3.2.4 同一個SqlSession兩次查詢期間手動清空了緩存
// 4)同一個SqlSession兩次查詢期間手動清空了緩存 @Test public void testLevelOneCache4() throws IOException { SqlSession session = sqlSessionFactory.openSession();
EmpMapper empMapper = session.getMapper(EmpMapper.class);
Emp emp1 = empMapper.selectById(1); session.clearCache(); Emp emp2 = empMapper.selectById(1); // 由於兩次查詢中間清空了一級緩存,所以又發送了一條查詢語句。 logger.info("是否相等:"+ (emp1 == emp2)); //false
session.commit(); session.close(); } |
3.2.5 不是同一個SqlSession
// 5)不是同一個SqlSession @Test public void testLevelOneCache1_05() throws IOException { SqlSession session01 = sqlSessionFactory.openSession(); SqlSession session02 = sqlSessionFactory.openSession(); EmpMapper empMapper01 = session01.getMapper(EmpMapper.class); EmpMapper empMapper02 = session02.getMapper(EmpMapper.class); Emp emp1 = empMapper01.selectById(1); // 由於是不同的SqlSession,所以又發送了一條查詢語句。各自有各自的一級緩存。 Emp emp2 = empMapper02.selectById(1);
logger.info("是否相等:"+ (emp1 == emp2)); //false session01.commit(); session02.commit(); session01.close(); session02.close(); } |
4. 二級緩存[mybatis自帶的二級緩存]
4.1 使用二級緩存步驟
1)在想要使用二級緩存的EmpMapper映射文件中加入cache標簽
開啟全局二級緩存配置:<setting name="cacheEnabled" value="true"></setting>
2)讓實體類支持序列化
public class Emp implements Serializable{
private static final long serialVersionUID = 1L;
這裡我們使用mybaits自帶的二級緩存,後面我們使用第三方的二級緩存EHCache。
3)SqlSession提交事務時才會將查詢到的數據存入二級緩存
@Test public void testLevelTwoCache() { // 測試二級緩存存在:使用兩個不同SqlSession執行查詢 // 說明:SqlSession提交事務時才會將查詢到的數據存入二級緩存 // 所以本例並沒有能夠成功從二級緩存獲取到數據 SqlSession sqlSession01 = sqlSessionFactory.openSession(); SqlSession sqlSession02 = sqlSessionFactory.openSession();
EmpMapper empMapper01 = sqlSession01.getMapper(EmpMapper.class); EmpMapper empMapper02 = sqlSession02.getMapper(EmpMapper.class);
//[00:01:07.699] [DEBUG] [main] [com.hy.mapper.EmpMapper] [Cache Hit Ratio [com.hy.mapper.EmpMapper]: 0.0] Emp emp01 = empMapper01.selectById(1); //[00:01:07.746] [DEBUG] [main] [com.hy.mapper.EmpMapper] [Cache Hit Ratio [com.hy.mapper.EmpMapper]: 0.0] Emp emp02 = empMapper02.selectById(1);
logger.info("是否相等:"+ (emp1 == emp2)); //false sqlSession01.commit(); sqlSession01.close();
sqlSession02.commit(); sqlSession02.close(); } |
修改代碼;
@Test public void testSecondLevelCache2() { SqlSession sqlSession01 = sqlSessionFactory.openSession(); SqlSession sqlSession02 = sqlSessionFactory.openSession();
EmpMapper empMapper01 = sqlSession01.getMapper(EmpMapper.class); EmpMapper empMapper02 = sqlSession02.getMapper(EmpMapper.class);
Emp emp01 = empMapper01.selectById(1);
sqlSession01.commit(); sqlSession01.close(); Emp emp02 = empMapper02.selectById(1);
//false 註意,從二級緩存中new了一個新的對象,所以emp01和emp02不是同一個對象 logger.info("是否相等:"+ (emp1 == emp2)); sqlSession02.commit(); sqlSession02.close(); } |
雖然兩個對象不相等,但是註意:只發出了一條查詢語句。
日誌中列印的Cache Hit Ratio叫做緩存命中率
緩存命中率=命中緩存的次數/查詢的總次數
4)mybatis序列化的特點
mybatis的二級緩存是屬於序列化,序列化的意思就是從記憶體中的數據傳到硬碟中,這個過程就是序列化;
反序列化意思就是相反而已;
也就是說,mybatis的二級緩存,實際上就是將數據放進了硬碟文件中去了;
現在呢,你僅僅的將Emp類給序列化了,如果有父類Person、級聯屬性,它們是不會跟著被序列化的,所以光這些是不夠的;