1. 程式人生 > >MyBatis技術全講(全)

MyBatis技術全講(全)

  • 配置
  • 簡單查詢
  • 高階查詢
  • 程式碼生成器
  • 快取配置

前言

這裡要提到一個詞:關注點分離。業務邏輯與資料邏輯相分離。沒必要為每一個DAO物件的初始化過程編寫大量程式碼,只需關注其SQL,並將SQL從程式碼中分離出來,mybatis就是這樣一個實現關注點分離思路的框架。
本篇為mybatis學習筆記,設計到mybatis的環境搭建、配置詳解、簡單應用、程式碼生成器、高階查詢、快取配置。基本上是全了。
前身是iBatis,以接近JDBC的效能實現了Java程式碼與SQL語句的分離。但是mybatis有其自己不完美的地方:物理分頁、快取。
mybatis支援宣告式快取,當一條資料被標記為“可快取”後,首次執行它所獲取的資料會被快取到快取記憶體中,後面再次執行直接命中快取,基於HashMap實現。

配置

配置檔案
mybatis-config.xml
mapper.xml

查詢

四個基礎標籤

<select>、<insert>、<update>、<delete>
select

單表


Model類
package com.abc.model;

public class User{
	public String name;
	public Integer age;
}

查詢寫法
<select id=”selectUserById”resultType=”com.abc.model.User”>
	select name,age from user where id=#{id}
</select>


呼叫
User user = selectUserById(1001);

第二個例子

public class User{
	public String name;
	public Integer age;
	public Role role;
}

public class Role{
	public String roleId;
	public String roleName;
}

<select id=”selectUserById”resultType=”com.abc.model.User”>

select name,age,role.Id,role.roleName 
from user u 
inner join role r on u.id=r.id 
where u.id=#{id}

</select>

第二個例子另一種寫法

<resultMap id=”userMap” type=”com.abc.model.User”>
	<id property=”id” column=”id”/>
	<result property=”name” column=”name”/>
	<result property=”age” column=”age”>
</resultMap>


<select id=”selectUserById”resultMap=”userMap”>
select name,age 
from user u 
inner join role r on u.id=r.id 
where u.id=#{id}
</select>

多表

當返回值涉及一個model時,和單表一樣。如果有多餘返回值,可以巢狀model,在寫xml寫sql時,select u.user_name as “user.userName”

insert

獲取主鍵值
自增方式的資料庫:

<insert id="insert" userGenerateKeys="true" keyProperty="id">
insert into ..
</insert>

序列的:

<insert id="insert" userGenerateKeys="true" keyProperty="id">
insert into ..
<selectKey keyColume=="id" resultType="long" keyProperty="id" order="before"> //oracle的序列是先獲取的,隨意要before。mysql的自增主鍵是插入後才有的,要改成after。
//mysql  SELECT LAST_INSERT_ID()
//oracle SELECT SEQ_ID.nextval from dual
//這裡的寫法不同資料庫不一樣
</selectKey>
</insert>

用法

<update id="updateById">
	updatre sys_user
	set user_name=#{userName},
	user_password=#{userPassword},
	user_email=#{userEmail},
	user_info = #{userInfo}
	hear_img = #{headImg,jdbcType=BLOB},
	create_time=#{createTime,jdbcType=TIMESTAMP}
	where id=#{id}
</update>

在UserMapper中新增介面
int updateById(SysUser sysUser);
測試類:

public void testUpdateById(){
	Reader reader = Resource.GetResourceAsReader("mybatis-config.xml");
	SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().builder(reader);
	SqlSession sqlSession = sqlSessionFactory.openSession();
	UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
	userMapper.updateById.......//大概流程如此
}

多查詢條件,介面需要修改為:

List<SysRole> selectRolesByUserIdAndRoleEnable(@Param("userId")Long userId,@Param("enable")Integer enabled);

如果入參為物件

List<SysRole> selectRolesByUserIdAndRoleEnable(@Param("user")User user,@Param("role")SysRole role);

在xml使用的時候,要用user.userId和role.enable了。和上面查詢一樣。

delete update略
Mapper動態代理實現

首先宣告一個介面

public interface TestMapper{
	List<User> selectAll();
}

建立一個代理類

public class MyMapperProxy<T> implements InvocationHandler{
	private Class<T> myMapper; 
	private SqlSession sqlSession;
		public MyMapperProxy(Class<T> myMapper,SqlSession sqlSession){
		  this.myMapper = myMapper;
		  this.sqlSession = sqlSession;
	}
	public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
		List<T> list = sqlSeesion.selectList(mapperInterface.getCanonicalName()+"."+method.getName());
		return list;
	}
}

這樣就獲取了interface的全限定名,所以mybatis是一種約定大於配置的方式簡化了操作。

Mybatis註解的用法

Insert註解,不需要返回主鍵的情況:

@Insert("insert into sys_role(id,name) values(#{id},#{name,jdbcType=String})")

需要返回主鍵的情況(自增主鍵)

@Insert("insert into sys_role(name) values(#{name,jdbcType=String})")
@Options(useGeneratedKsys=true,keyProperty="id")

需要返回主鍵的情況(非自增主鍵),xml裡用的selectKey標籤

@Insert("insert into sys_role (name,enable,create_time),values(#{name},#{enable},#{createTime,jdbcType=TIMESTAMP})")
@SelectKey(statement="SELECT LAST_INSERT_ID()",keyProperty="id",resultType=Long.class,before=false)

Update和Delete註解
同上,沒什麼特殊的。
Provider註解
除了上述增刪改查意外,mybatis還提供了四種Provider註解。分別為:@SelectProvider @InsertProvider @UpdateProvider @DeleteProvider。
就是將SQL封裝到程式碼裡。

@SelectProvider(type=PrivilegeProvider.class,method="selectById")
SysPrivilege selectById(Long id);

然後PrivilegeProvider類如下

public class PrivilegeProvider{
	public String selectById(final Long id){
		return new SQL(){
			SELECT("id,privilege_name,privilege_url");
			FROM("sys_privilege");
			WHERE("id=#{id}");
		}.toString();
	}
}

由於MyBatis註解方式不是主流,僅做了解。

動態SQL

MyBatis強大特性之一便是這一節的東西。MyBatis使用OGNL表示式,用於xml配置中。支援如下幾種標籤:

<if />		判斷
<choose />	選擇
<where />	條件
<set />		更新設值
<trim />	按規則替換
<foreach />
<bind />

其中where、set、trim這三個標籤解決了類似問題,並且where、set都屬於trim的一種具體用法。

if標籤

有如下查詢場景:當用戶傳入name時,根據name查,當輸入的是email時,根據email查。如何用OGNL表示式來寫這條sql呢?

<select id="selectByUser" resultType="com.abc.simple.model.user">
 select id,
	user_name userName,
	user_password userPassword,
	user_email userEmail,
	user_info userInfo
from sys_user
where 1=1
<if test="userName != null and userName!=">
	and user_name like concat('%',#{userName},'%')  //concat是mysql語法
</if>
<if test="userEmail !=null and userEmail!=">
	and user_email = #{userEmail}
</if>
</select> //注意首先判斷非空,然後再判斷值。兩個if標籤可同時為真。

if標籤有一個必填的屬性test,test的屬性值一個符合OGNL要求的判斷表示式,裡面就是一個條件判斷語句,支援and or,分組判斷,巢狀判斷。

update中使用if標籤

<update id="updateById">
 update sys_user
set
<if test="userName!=null and userName!=">
	user_name = #{userName},
</if>
<if test="userPassword!=null and userPassword!=">
	user_password = #{userPassword},
</if>
	id=#{id}
	where id=#{id}
</update> //1、注意逗號 2、注意where條件前的語句id=#{id},這條語句可以最大限度的保證不出錯。

insert中使用if標籤

<insert id="insert1" useGeneratedKey="true" keyProperty="id">
insert into sys_user(
 user_name,user_password,<if test="userEmail !=null and userEmail!=">user_email,</if>userinfo
)values(#{userName},#{userPassword},<if test="userEmail !=null and userEmail!=">#{userEmail},</if>)#{userInfo}
</insert>

欄位和值的條件判斷要一致。

choose標籤

choose的作用就如同if…else,語句為choose when otherwise

<select id=”selectByUser” resultType=”com.abc.model.User”>
    select * from user where 1=1
    from user 
    where 1=1
    <choose>
	<when test=”id!=null”>	
		and id =#{id}
	</when>
	<when test=”userName!=null and userName!='' ”>
		and userName=#{userName}
	<otherwise>
		and 1=2
	<otherwise>
    </choose>
</select>

最後這個1=2是為了當什麼都沒有輸入的時候,返回為空。因為這個條件肯定是false的。

where標籤

where標籤的作用:如果標籤包含的元素中有返回值,那麼就插入一個where,如果where後面的字串以and和or開頭,就刪除它。

<select id=”selectUser” resultType=”com.abc.model.User”>
	select * from user
	<where>
		<if test=”userName!=null and userName!='' ”>
		and user_name like concat('%',#{userName},'%')
		</if>

		<if>
		and user_email = #{userEmail}
		</if>
	</where>
</select>
set標籤

set標籤作用:如果該標籤包含的元素中有返回值,就插入一個set,如果set後面的字串是以逗號結尾的,就將逗號去掉。

<update id=”updateUserById”>
	update user
	<set>
		<if test=”userName!=null and userName!='' ”>
			user_name=#{userName},
		</if>
		<if test=”userEmail!=null and userEmail!='' ”>
			user_email=#{userEmail},
		</if>
		<if test=”headImg!=null”>
			head_img=#{headImg,jbdcType=BLOB},
		</if>
		where id=#{id}
	</set>
</update>
trim標籤

where和set標籤的功能都是通過trim來實現的

where對應的是:

<trim prefix=”WHERE”prefixOverrides=”AND |OR ”>
</trim>

set對應的是:

<trim prefix=”SET” suffixOverrides=”,”>
</trim>

prefixOverrides:當trim元素內包含內容時,會把內容中匹配的字首字串去掉
suffixOverrides:當trim元素內包含內容時,會把內容中匹配的字尾字串去掉

foreach標籤

批量查詢,在SQL中會使用類似 id in (1,2,3)

<select id=”selectByIdList”resultType=”com.abc.model.User”>
	select * from user
	where id in
	<foreach collection=”list”
		open=”(” 
		close=”)” 
		separator=”,”
		item=”id” 
		index=”i”>
		#{id}	
	</foreach>
</select>

批量插入是SQL-92新增特性,目前支援的資料庫有DB2、SQL Server2008、MySQL、SQLite3.7.11以上版本。(2017年11月再版)

INSERT INTO tablename (column-a,column-b,column-c) values (value-a’,value-b’,‘value-c’);

<insert id="insertList">
insert into sys_user(user_name,user_password,user_email)
values
<foreach collection="list" item="user" separator=",">
(
	{user.userName},#{user.userPassword},#{user.userEmail}
)
</foreach>
</insert>

foreach插入map型別

<foreach collection="_parameter" item="val" index="key" separator=",">
(
	${key}=#{val}
)
</foreach>

注:$取值和#取值的區別。
1.取值會將輸入當成字串,比如輸入123,輸出"123",可以防sql注入
$取值將輸入原樣輸出,比如輸出123,輸出123。

bind標籤

bind可以繫結變數,避免更換資料庫造成的修改sql,當然只是在一定語法程度上。

<if test="userName!=null">
<bind name="nameLike" value="'%'+userName+'%'">
	and user_name like #{nameLike}
</if>

多資料支援

提供了一個databaseId的屬性值,增刪改查、selectKey,sql標籤都包含這個屬性。
在mybatis-config.xml中配置,並給對應的資料庫id起了一個別名。

<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver">
<property name="DB2" value="db2">
...
</databaseIdProvider>

然後在寫select標籤時,加上

<select id=”selectUserById” databaseId=”mysql”></select>
<select id=”selectUserById” databaseId=”oracle”></select>

比如針對oracle和mysql的like查詢

<select id="selectByUser" databaseId="mysql" resultType="com.abc.model.user">
	select * from sys_user where user_name like concat('%',#{userName},'%')
</select>
<select id="selectByUser" databaseId="oracle" resultType="com.abc.model.user">
	select * from sys_user where user_name like '%'||#{userName}||'%'
</select>

就可以根據不同的資料庫進行操作,比如mysql有自增主鍵,oralce有序列。

OGNL語法

a or b
a and b
a==b 或 a eq b
a!=b 或 a neq b
a lt b 小魚
a lte b 小魚等魚 gt大魚 gte大魚等魚
a+b a*b a/b a-b a%b
!a not a
a.method(args) 呼叫物件方法 比如判斷 list.size()>0
a.property 物件屬性值 比如user.address.name,比如保證存在這個欄位,不然會報錯
a[index]按索引取值
@[email protected](args)呼叫靜態類方法 比如test="@[email protected](userName)"
@[email protected]呼叫類的靜態欄位

MyBatis程式碼生成器

MyBatis程式碼生成器又叫MyBatis Generator,縮寫MBG,這東西是用來根據資料庫來自動生成mapper檔案、model檔案、dao檔案。
執行方式1:下載mybatis-generatorjar檔案,然後編寫一個xml檔案。用

java -jar mybatis-generator-core-1.3.2.jar -configfile generatorConfig.xml -overwrite

執行,還要準備一個數據庫連線的jar。
執行方式2:通過maven獲取

<dependency>
	<groupId>org.mybatis.generator</groupId>
	<artifactId>mybatis-generator-core</artifactId>
	<version>1.3.3</version>
</dependency>

然後建立Generator.java類

public static void main(String args[]){
	 List<String> warnings = new ArrayList<String>();
	 boolean override = true;
	 InputStream is = Generator.class.getResourceAsStream("/generator/generatorConfig.xml");
	 ConfigurationParser cp = new ConfigurationParser(warnings);
	 Configuration config = cp.parseConfiguration(is);
	 is.close();
	 DefaultShellCallBack callback = new DefaultShellCallBack(override);
	 //建立MBG
	 MyBatisGenerator myBatisGenerator = new MyBatisGenerator(conifg,callback,warnings);
	 //執行生成程式碼
	 myBatisGenerator.generate(null);
	 //輸出警告資訊
	 for(String warning : warnings){
	   syso(warning);
	 }
}

使用java程式碼好處是,可以在當前專案中找一些自定義類,比如裡配置的自定義註釋類。
執行方式3:maven plugin方式
執行方式4:eclipse外掛方式 安裝mybatis-generator的eclipse外掛
MBG有豐富的配置可供使用,所以首先來看MBG的xml配置。首先在xml檔案中引入MBG的dtd檔案。dtd是用來定義標籤的。mybatis-generator-config_1_0.dtd
然後所有的配置要寫在

<generatorConfiguration>
	<properties></properties> 最多配置一個
	<classPathEntry></classPathEntry> 選填
	<context></context> 至少配置一個
</generatorConfiguration>

MyBatis高階查詢

高階結果對映

一對一
<select>
	select u.id,
	u.user_name userName,
	u.user_info userInfo,
	r.id "role.id",
	r.role_name "role.roleName"
	from sys_user u 
	inner join sys_user_role ur on u.id=ur.user_id
	where u.id=#{id}
</select>

User這樣寫

public User{
	@setter @getter public UserRole role;
}

就可以實現巢狀結果對映,mybatis就將查詢結果對映到兩個類裡了。這樣寫的好處就是降低資料庫的訪問次數,缺點是要寫複雜的sql。
要實現上述功能,mybatis還可以用reusltMap來實現啊。其實這倆是一個東西。

<resultMap>
	<result property="role.roleName" column="role_name" jdbcType="String"/>
</resultMap>

如果有十來個屬性,那麼寫起來比較費勁。這時候mybatis的resultMap的繼承就起作用了。
將通用的寫一個resultMap,需要inner join查其他表的屬性,就繼承一下。

<resultMap id="userRoleMap" extends="userMap" type="com.abc.model.user">
</result>

在此基礎上,繼續做修改,resultMap有一個子標籤用來配置複雜對映關係

<resultMap id="userRoleMap" extends="userMap" type="com.abc.model.user">
  <association property="role" columnPrefix="role_" javaType="com.abc.mode.role">
    <result />
  </association>
</resultMap>

在inner join的時候,和sys_role相關的要加上role_字首。

<association resultMap="">功能也可以將association內的result標籤獨立出來。
<association>還實現了一個重要的功能:巢狀查詢的延遲載入。
<association
	 property="role"
	 fetchType="lazy"
	 select="com.abc.mybatis.mapper.RoleMapper.selectRoleById"
	 column="{id=role_id}"
/>

在RoleMapper.xml中加入

<select id="selectRoleById" resultMap="roleMap">
	select * from sys_role where id=#{id}
</select>

然後在mybatis-config.xml中配置

<settings>
	<setting name="aggressiveLazyLoading" value="false" />
</settings>

即可。但是注意,在和Sping整合時,要確保只能在Service中使用延遲載入屬性,因為SqlSession的生成周期交給了Spring來管理,當物件的生命週期超出了SqlSession的生命週期時,就會報錯。在Controller查詢會因為SqlSession已經關閉。
當我們想主動觸發延遲載入時,mybatis提供了另一個引數來解決lazyLoadingTriggerMethods,當呼叫預設方法“equals、clone、hashCode、toString”時會觸發。

一對多

就是將結果對映成一個List,如下:
@setter @getter List roleList;

<resutMap>
<result />
...
<collection property="roleList" columnPrefix="role_" ofType="com.abc.model.SysRole">
		<result property="id" column="id" />
		<result property="roleName" column="role_name">
</collection>
</resultMap>

簡而言之就是把association變成了collection。
原理:mybatis會根據
主鍵,來講查出來的資料,userMap id相同的合併。
利用resultMap巢狀的特性,可以實現多層model的巢狀查詢。

collection集合的巢狀查詢

association關聯的巢狀查詢這種方式會執行額外的SQL查詢,對映配置會簡單很多。下面是collection的巢狀對映。
首先新增PriMapper.xml

<select id="selectPriByRoleId" resultMap="priMap">
	select p.* from sys_pri p where role_id=#{id}
</select>
<resultMap id="rolePriListMapSelect" extends="roleMap" type="com.abc.model.Role">
<collection property="priList"
        fetchType="lazy"
        column="{roleId=id}"
        select="com.abc.simple.mapper.PriMapper.selectPriByRoleId" />
</resultMap>
<select id="selectRoleByUserId" resultMap="rolePriListMapSelect">
	select r.id,r.role_name,r.role_enable,r.create_time from sys_role r inner join sys_user_role ur on ur.role_id=r.id where ur.user_id=#{userId}
</select>

最頂層使用者資訊

<resultMap id="userRoleListMapSelect" extends="userMap" type="com.abc.model.User">
<collection property="roleList"
        fetchType="lazy"
        select="com.abc.simple.mapper.RoleMapper.selectRoleByUserId"
        column="{userId=id}" />
</resultMap>
<select id="selectAllUserAndRolesSelect" resultMap="userRoleListMapSelect">
select u.id,
    u.user_name,
    u.user_info,
    u.create_time
from sys_user u where u.id=#{id}
</select>

這樣就實現了多層collection巢狀迴圈
單元測試

@Test
public void testSelectAllUserAndRolesSelect(){
	SqlSession sqlSession = getSession();
	//省略try catch
	UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
	SysUser user = userMapper.selectAllUserAndRolesSelect(1L);
	syso(user.getUserName);
	//開始延遲載入
	foreach(SysRole role : user.getRoleList()){
		syso(role.getRoleName);
		//繼續延遲載入
		for(SysPri pri : role.getPriList()){
			syso(pri.getPriName());
		}
	}
}

鑑別器對映

有時一個單獨資料庫查詢會返回很多不同型別的資料,類似Java中的switch語句。這個功能很強大。
比如當我們查詢一個分類的科目時,如果該分類的狀態為不可用狀態,那麼就不去查他的科目了。下面這個例子邏輯一樣。

<resultMap id="rolePriListMapChoose" type="com.abc.model.SysRole">
   <discriminator column="enable" javaType="int">
       <case value="1" resultMap="rolePriListMapSelect" />
       <case value="0" resultMap="roleMap" />
    </discriminator>
</resultMap>

根據enbale的值,來對映不同的resultMap。

儲存過程

<select id="selectUserById" statementType="CALLABLE" useCache="false">
{
  call select_user_by_id(
   #{id,mode=IN},
   #{userName,mode=OUT,jdbcType=VARCHAR},
   #{userInfo,mode=OUT,jdbcType=VARCHAR},
   #{createTime,mode=OUT,jdbcType=TIMESTAMP},
   #{headImg,mode=OUT,jdbcType=BLOB,javaType=_byte[]}
)}
</select>

使用mybatis呼叫儲存過程,不支援mybatis二級快取,所以直接把快取useCache=“false”,一定要注意基本型別的對映。比如上面的headImg。這個儲存過程沒有返回值,使用出參的方式獲得了,相當於c#中的
out user
使用出參方式獲取返回值,必須保證所有出參在JavaBean中都存在。因為JavaBean物件中不存在出參對應的setter方法。
下面一個儲存過程場景:分頁查詢
需要返回兩個值:一個是List 一個是int total。返回值用resultMap接,total用上述的方式接。
插入、刪除也是

<insert id="insert" statementType="CALLABLE">
{call insert_user(
  #{user.id,mode=OUT,jdbcType=BIGINT},
  #{user.userName,mode=IN},
  #{user.headImg,mode=IN,jdbcType=BLOB},
  #{roleIds,mode=IN}
)}
</insert>
 
<delete id="delete" statementType="CALLABLE">
{call delete_user(
	{id,mode=IN}
)}
</delete>

oracle帶遊標的儲存過程
遊標其實對應多個sql語句。建立兩個引數的遊標:

create or replace procedure SELECT_COUNTRY(ref_cur1 out sys_refcursor,ref_cur2 out sys_refcursor)
is begin
open ref_cur1 for select * from country where id<3;
open ref_cur1 for select * from country where id>=3;
end SELECT_COUNTRY;

CountryMapper.xml寫法

<select id="selectCountry" statementType="CALLABLE" useCache="false">
{
call SELECT_COUNTRY(
 #{list1,mode=OUT,jdbcType=CURSOR,javaType=ResultSet,resultMap=BaseResultMap},
 #{list2,mode=OUT,jdbcType=CURSOR,javaType=ResultSet,resultMap=BaseResultMap},
)
}
</select>

對列舉值的對映

MyBatis在遇到列舉值時,預設使用TypeHandler處理,這個處理器會將列舉值轉換為字串型別的字面值並使用,對Enable而言便是disable和enable字串
所以不能使用預設的,使用另一個EnumOrdinalTypeHandler,這個處理器使用列舉的索引進行處理,在mybatis-config.xml中新增

<typeHandlers>
	<typeHandler javaType="com.abc.type.Enable這個是列舉類" handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
</typeHandlers>

還可以實現自己的型別處理器,繼承實現TypeHandler介面。1.6對Java 8日期(JSR-310)的支援,在pom.xml中引入依賴

<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis-typehandlers-jsr310</artifactId>
	<version>1.0.2</version>
</dependency>

MyBatis快取配置

一級快取

在一個SqlSession中,如果查詢的方法和引數都一樣,那麼第二次查的就不會從資料庫裡查了,第一次查詢的結果會以Map形式存在快取裡。這時候如果對第一次查詢的結果進行修改操作,也會影響第二次查詢的結果。因為這倆的引用完全一樣。如何避免這種情況?在查詢語句上新增一個屬性

<select id="selectById" flushCache="true" resultMap="userMap">
	select * from user where id =#{id}
</select>

flushCache="true"會保證每次查詢都從資料庫裡查,這時候如果查兩次,那麼返回的例項物件的引用就不同了。要避免這麼做。還有一種重新整理快取的方式,就是執行insert update delete的時候,會自動清空一級快取。如果在一個SqlSession中,先查id=1的User1,再查一次id=1的User2,那麼User1和User2是同一個東西。如果先查id=1的User1,然後隨便刪除或更新一個id=2的User,然後再查id=1的User2,那麼User1和User2就不是同一個引用了。因為快取被清空了。
剛才說到,是在一個SqlSession中。如果打開了兩個SqlSession,那麼第一個SqlSession查出來的User和第二個SqlSession查出來的User物件,儘管值一樣,但是是不同的引用物件。
這就是MyBatis的一級快取。

二級快取

我們常說的MyBatis快取其實是值的它的二級快取,因為一級快取是預設不可被控制的。
MyBatis二級快取非常強大。在存在於比SqlSession範圍更大的SqlSessionFactory生命週期中。

二級快取配置

mybatis-config.xml中新增配置,開啟二級快取

<settings>
<setting name="cacheEnabled" value="true" />
</settings>
xml中新增快取配置

保證上面快取全域性開啟的情況下進行,在UserMapper.xml中追加cache標籤即可

<mapper namespace="com.abc.mapper.UserMapper">
<cache />
</mapper>

新增這句話有什麼效果呢?
1、所有SELECT語句會被快取
2、所有insert update delete會重新整理快取
3、使用LRU演算法來回收 least recently used
4、根據時間表,快取不會以任何時間來重新整理,可以設定
5、快取會儲存集合或物件的1024個引用
6、快取是可讀可寫的

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readonly="true"
/>

這個配置更為高階,建立了一個FIFO快取,每60秒重新整理一次,儲存512個引用,只讀的。
cache的eviction回收策略屬性值如下:
LRU 最近最少使用
FIFO 按進入快取的順序
SOFT 移除基於GC規則的軟引用
WEAK 移除基於GC規則的弱引用

接口裡配置

在使用註解方式時,如果想對註解方法啟用二級快取,還需要在Mapper介面中建配置。如果Mapper介面也存在對應的zml對映檔案,兩者同時開啟快取時,還需要特殊配置。只在Mapper介面中配置的話,在UserMapper的interface中

@CacheNamespace(
eviction=FifoCache.class,
flushInterval=60000,
size=512,
readWrite=true
)

如果介面不存在使用註解式的方法,可以只在xml中配置,否則要在

@CacheNamespaceRef(UserMapper.class)
public interface UserMapper{
}

因為想讓介面方法和xml檔案使用相同的快取,就要這樣。然後在UserMapper.xml中新增

<cache-ref namespace="com.abc.mapper.UserMapper"/>

這樣配置後,xml就會引用Mapper介面中配置的二級快取,同樣可以避免同時配置二級快取導致的衝突。
MyBatis中很少同時使用Mapper介面註解方式和xml對映檔案,所以參照快取不是為了解決這個問題而設計的。參照快取除了能夠通過引用減少配置外,主要作用是用來解決髒讀。

使用二級快取

如果快取配置的是隻讀的,那麼無所謂,從快取裡讀的物件就是同一個例項。
如果配置成可讀可寫的,mybatis使用SerializedCache來序列化可讀可寫快取類。被序列化的物件要實現Serializable介面。
二級快取什麼時候會有資料呢?看下面的流程
1、開啟SqlSession
2、查詢id=1的使用者
3、再次查詢id=1的使用者
這時候2和3查出來的是一個
4、關閉SqlSession
5、開啟新的SqlSession
6、查詢id=1的使用者
7、再次查詢id=1的使用者
這時候雖然6和2查出來的不是一個引用,但是6並沒有去查資料庫,而是去二級快取裡查了。因為是可讀可寫的情況,6查出來的和7查出來的都是經過反序列化的,不是同一個例項。但6和7都沒有去資料庫查,都到二級快取裡去查了。二級快取是當這個查詢所在的SqlSession執行close()方法後,才將資料刷到二級快取裡。
這裡可能會有疑問,如果我在第2步查出來資料後,修改其資訊,那麼後來查出來的不就都出問題了嗎?Yes,所以不要做無意義的修改,避免人為增加髒資料。
MyBatis是通過Map來實現快取的。少量資料可以,大量資料怎麼辦?
可以通過其他工具或框架來儲存。

整合EhCache快取

EhCache是一個純Java程序內快取,快速、簡單、多種策略、記憶體和磁碟兩級、可通過RMI實現分散式。該專案由MyBatis官方提供,叫ehcache-cache。https://github.com/mybatis/ehcache-cache
依賴

<dependency>
	<groupId>org.mybatis.caches</groupId>
	<artifactId>mybatis-ehcache</artifactId>
	<version>1.0.3</version>
</dependency>

整合Redis

MyBatis還提供了二級快取的Redis支援,命名為redis-cache。https://github.com/mybatis/redis-cache。

<dependency>
	<groupId>org.mybatis.caches</groupId>
	<artifactId>mybatis-redis</artifactId>
	<version>1.0.0-beta2</version>
</dependency>

整合基本上的套路都是引用依賴,在src/main/resources新增配置檔案,在mybatis-config.xml中配置快取標籤。

髒資料

二級快取雖然能提高效率,但是也容易產生髒資料,由於多標聯查時。要掌握避免髒資料的技巧。比如參照快取。

二級快取適用場景

1、以查詢為主,少的增刪改
2、絕大多數單表操作
3、按照業務對錶進行分組時,如果關聯表比較少,可以使用參照快取。
在使用二級快取時,一定要考慮髒資料對系統的影響,在任何情況下,都可以考慮在業務層使用可控制的快取來代替二級快取。