我在CSDN博客寫了不少和MyBatis相關的博客,2015年時覺得自己寫的太零散,不夠系統全面,所以在GitBook創建了一本名爲MyBatis最佳實踐的開源電子書,當時寫好了目錄大綱,由於維護好幾個開源項目,業餘時間幾乎都被佔用,最終也只寫了MyBatis Generator部分,也就是博客中的MyBatis Generator 詳解。2016年3月份時,博文視點的孫奇俏編輯和我聯繫,然後就開始了 MyBatis 相關書籍的編寫。
本書的內容和各種實踐源自於和廣大開發人員的交流,特別感謝大家的建議和幫助!
更多本書資源還可以通過以下網址獲取更多
MyBatis 相關工具:http://mybatis.tk
GitHub在線交流:https://github.com/mybatis-book/book
簡介
本書中從一個簡單的 MyBatis 查詢入手,搭建起學習 MyBatis 的基礎開發環境。通過全面的示例代碼和測試講解了在 MyBatis XML 方式和註解方式中進行增、刪、改、查操作的基本用法,介紹了動態 SQL 在不同方面的應用以及在使用過程中的最佳實踐方案。針對 MyBatis 高級映射、存儲過程和類型處理器提供了豐富的示例,通過自下而上的方法使讀者更好地理解和掌握MyBatis 的高級用法,同時針對 MyBatis 的代碼生成器提供了詳細的配置介紹。此外,本書還提供了緩存配置、插件開發、Spring、Spring Boot 集成的詳細內容。最後通過介紹 Git 和 GitHub 讓讀者瞭解MyBatis 開源項目,通過對 MyBatis 源碼和測試用例的講解讓讀者更好掌握 MyBatis。
地址
本書內容
全書共11 章,每一章的具體內容如下。
第1 章 MyBatis 入門
本章先簡單介紹了 MyBatis 的發展歷史和特點,然後通過一步步的操作搭建了一個學習 MyBatis 的基礎環境,這個開發環境也是學習後續幾個章節的基礎。
第2 章 MyBatis XML 方式的基本用法
本章設定了一個簡單的權限控制需求,使用 MyBatis XML 方式實現了數據庫中一個表的常規操作。在查詢方面,通過根據主鍵查詢和查詢全部兩個方法讓讀者在學會使用 MyBatis 查詢方法的同時,還深入瞭解 MyBatis 返回值的設置原理。在增、刪、改方面提供了大量詳細的示例,這些示例覆蓋了MyBatis 基本用法的方方面面。
第3 章 MyBatis 註解方式的基本用法
雖然 XML 方式是主流,但是仍然有許多公司選擇了註解方式,因此本章非常適合使用註解方式的讀者。本章使用註解方式幾乎實現了同XML 方式類似的全部方法,包含許多常用註解的基本用法。對於初學者來說,即使不使用註解方式,通過本章和第2 章的對比也可以對 MyBatis 有更深的瞭解。
第4 章 MyBatis 動態 SQL
本章詳細介紹了MyBatis 最強大的動態 SQL 功能,通過豐富的示例講解了各種動態 SQL 的用法,爲動態 SQL 中可能出現的問題提供了最佳實踐方案,還提供了動態 SQL 中常用的OGNL 用法。
第5 章 MyBatis 代碼生成器
本章介紹的MyBatis 代碼生成器可以減輕基本用法中最繁重的那部分書寫工作帶來的壓力。通過本章的學習,可以使用代碼生成器快速生成大量基礎的方法,讓大家更專注於業務代碼的開發,從枯燥的基礎編碼中解脫出來。
第6 章 MyBatis 高級查詢
本章介紹了MyBatis 中的高級結果映射,包括一對一映射、一對多映射和鑑別器映射。通過循序漸進的代碼示例讓讀者輕鬆地學會使用MyBatis 中最高級的結果映射。本章還通過全面的示例講解了存儲過程的用法和類型處理器的用法。
第7 章 MyBatis 緩存配置
本章講解了MyBatis 緩存配置的相關內容,提供了EhCache 緩存和Redis 緩存的集成方法。雖然二級緩存功能強大,但是使用不當很容易產生髒數據。本章針對髒數據的產生提供了最佳解決方案,並且介紹了二級緩存適用的場景。
第8 章 MyBatis 插件開發
本章介紹了MyBatis 強大的擴展能力,利用插件可以很方便地在運行時改變MyBatis 的行爲。通過兩個插件示例讓讀者初窺門徑,結合第11 章的內容可以讓讀者開發出適合自己的插件。
第9 章 Spring 集成MyBatis
本章介紹了最流行的輕量級框架Spring 集成MyBatis 的方法,通過一步步操作從零開始配置,搭建一個基本的Spring、Spring MVC、MyBatis 開發環境。
第10 章 Spring Boot 集成MyBatis
本章介紹了最流行的微服務框架Spring Boot 集成MyBatis 的方法,通過MyBatis 官方提供的starter 可以很方便地進行集成。同時,本章對starter 中的配置做了簡單的介紹,可以滿足讀者對MyBatis 各項配置方面的需要。
第11 章 MyBatis 開源項目
本章是一扇通往開源世界的大門,也是一扇通往 MyBatis 源碼學習的大門。從 Git 入門到GitHub 入門,讀者可以學會使用最流行的分佈式版本控制系統和源代碼託管服務。通過一段代碼讓大家瞭解 MyBatis 中的一部分關鍵類,通過代碼包講解可以瞭解MyBatis 每個包中所含的功能。最後通過MyBatis 豐富的測試用例爲讀者提供更多更有用的學習內容。
試讀內容 - 第4章 MyBatis 動態 SQL
4.4 foreach
用法
SQL語句中有時會使用IN
關鍵字,例如id in (1,2,3)
。可以使用${ids}
的方式直接獲取值,但這種寫法不能防止 SQL 注入,想避免 SQL 注入就需要用#{}
的方式,這時就要配合使用 foreach
標籤滿足需求。
foreach
可以對數組、Map
或實現了 Iterable
接口(如 List
、Set
)的對象進行遍歷。數組在處理時會轉換爲 List
對象進行處理,因此 foreach
遍歷的對象可以分爲兩大類:Iterable
類型和 Map
類型。這兩種類型在遍歷循環時情況不一樣,這一節會通過3個例子來講解 foreach
的用法。
4.4.1 foreach
實現 in 集合
foreach
實現 in
集合(或數組)是最簡單和常用的一種情況,下面介紹如何根據傳入的用戶 id
集合查詢出所有符合條件的用戶。首先在 UserMapper
接口中增加如下方法。
/**
* 根據用戶id集合查詢
*
* @param idList
* @return
*/
List<SysUser> selectByIdList(List<Long> idList);
在 UserMapper.xml 中增加如下代碼。
<select id="selectByIdList" resultType="tk.mybatis.simple.model.SysUser">
select id,
user_name userName,
user_password userPassword,
user_email userEmail,
user_info userInfo,
head_img headImg,
create_time createTime
from sys_user
where id in
<foreach collection="list" open="(" close=")" separator="," item="id" index="i">
#{id}
</foreach>
</select>
foreach
包含以下屬性。
-
collection
:必填,值爲要迭代循環的屬性名。這個屬性值的情況很多。 -
item
:變量名,值爲從迭代對象中取出的每一個值。 -
index
:索引的屬性名,在集合數組情況下值爲當前索引值,當迭代循環的對象是Map
類型時,這個值爲Map
的key(鍵值)。 -
open
:整個循環內容開頭的字符串。 -
close
:整個循環內容結尾的字符串。 -
separator
:每次循環的分隔符。
collection
的屬性要如何設置呢?來看一下 MyBatis 是如何處理這種類型的參數的。
1. 只有一個數組參數或集合參數
以下代碼是 DefaultSqlSession
中的方法,也是默認情況下的處理邏輯。
private Object wrapCollection(final Object object) {
if (object instanceof Collection) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("array", object);
return map;
}
return object;
}
當參數類型爲 Collection
的時候,默認會轉換爲 Map
類型,並添加一個 key 爲 collection
的值(MyBatis 3.3.0版本增加),如果參數類型是 List
集合,那麼就繼續添加一個 key 爲 list
的值(MyBatis 3.2.8及低版本只有這一個key),這樣,當collection="list"
時,就能得到這個集合,並對它進行循環操作。
當參數類型爲 Array
的時候,也會轉換成 Map
類型,默認的 key 爲 array
。當採用如下方法使用數組參數時,就需要把foreach
標籤中的 collection
屬性值設置爲 array
。
/**
* 根據用戶id集合查詢
*
* @param idArray
* @return
*/
List<SysUser> selectByIdList(Long[] idArray);
上面說是數組或集合類型的參數默認的名字,推薦使用@Param
來指定參數的名字,這時 collection
就設置爲通過@Param
註解指定的名字。
2. 有多個參數
第 2 章中講過,當有多個參數的時候,要使用@Param
註解給每個參數指定一個名字,否則在 SQL 中使用參數時就會不方便。因此將collection
設置爲 @Param
註解指定的名字即可。
3. 參數是 Map
類型
使用 Map
和使用@Param
註解方式類似,將 collection
指定爲對應 Map
中的 key 即可。如果要循環所傳入的 Map
,推薦使用@Param
註解指定名字,此時可將 collection
設置爲指定的名字,如果不想指定名字,就要使用默認值_parameter
。
4. 參數是一個對象
指定爲對象的屬性名即可。當使用對象內多層嵌套的對象時,使用屬性.屬性
(集合和數組可以使用下標取值)的方式可以指定深層的屬性值。
先來看一個簡單的測試代碼,驗證以上說法。
@Test
public void testSelectByIdList(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<Long> idList = new ArrayList<Long>();
idList.add(1L);
idList.add(1001L);
//業務邏輯中必須校驗idList.size() > 0
List<SysUser> userList = userMapper.selectByIdList(idList);
Assert.assertEquals(2, userList.size());
} finally {
//不要忘記關閉sqlSession
sqlSession.close();
}
}
該測試輸出的日誌如下。
DEBUG [main] - ==> Preparing: select id, user_name userName,
user_password userPassword,
user_email userEmail,
user_info userInfo,
head_img headImg,
create_time createTime
from sys_user
where id in ( ? , ? )
DEBUG [main] - ==> Parameters: 1(Long), 1001(Long)
TRACE [main] - <== Columns: id, userName, userPassword, userEmail,
userInfo, headImg, createTime
TRACE [main] - <== Row: 1, admin, 123456, [email protected],
<<BLOB>>, <<BLOB>>, 2016-06-07 00:00:00.0
TRACE [main] - <== Row: 1001, test, 123456, [email protected],
<<BLOB>>, <<BLOB>>, 2016-06-07 00:00:00.0
DEBUG [main] - <== Total: 2
可以觀察日誌中打印的SQL語句,foreach元素中的內容最終成爲了in ( ? , ? )
,根據這部分內容很容易就能理解 open
、item
、separator
和 close
這些屬性的作用。
關於不同集合類型參數的相關內容,建議大家利用上面的基礎方法多去嘗試,幫助更好地理解。
4.4.2 foreach
實現批量插入
如果數據庫支持批量插入,就可以通過 foreach
來實現。批量插入是SQL-92新增的特性,目前支持的數據庫有 DB2、SQL Server 2008 及以上版本、PostgreSQL 8.2 及以上版本、MySQL、sqlite 3.7.11 及以上版本、H2。批量插入的語法如下。
INSERT INTO tablename (column-a, [column-b, ...])
VALUES ('value-1a', ['value-1b', ...]),
('value-2a', ['value-2b', ...]),
...
從待處理部分可以看出,後面是一個值的循環,因此可以通過 foreach
實現循環插入。
在 UserMapper
接口中增加如下方法。
/**
* 批量插入用戶信息
*
* @param userList
* @return
*/
int insertList(List<SysUser> userList);
在 UserMapper.xml 中添加如下 SQL。
<insert id="insertList">
insert into sys_user(
user_name, user_password,user_email,
user_info, head_img, create_time)
values
<foreach collection="list" item="user" separator=",">
(
#{user.userName}, #{user.userPassword},#{user.userEmail},
#{user.userInfo}, #{user.headImg, jdbcType=BLOB}, #{user.createTime, jdbcType=TIMESTAMP})
</foreach>
</insert>
注意:通過 item
指定了循環變量名後,在引用值的時候使用的是屬性.屬性
(如 user.userName
)的方式。
針對該方法編寫測試如下。
@Test
public void testInsertList(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//創建一個user對象
List<SysUser> userList = new ArrayList<SysUser>();
for(int i = 0; i < 2; i++){
SysUser user = new SysUser();
user.setUserName("test" + i);
user.setUserPassword("123456");
user.setUserEmail("[email protected]");
userList.add(user);
}
//將新建的對象批量插入數據庫中,特別注意,這裏的返回值result是執行SQL影響的行數
int result = userMapper.insertList(userList);
Assert.assertEquals(2, result);
} finally {
//爲了不影響數據庫中的數據導致其他測試失敗,這裏選擇回滾
sqlSession.rollback();
//不要忘記關閉sqlSession
sqlSession.close();
}
}
爲了使輸出的日誌不那麼長,這裏就測試插入兩條數據的情況,輸出的日誌如下。
DEBUG [main] - ==> Preparing: insert into sys_user(
user_name, user_ password,
user_email, user_info,
head_img, create_time)
values ( ?, ?,?, ?, ?, ?) ,
( ?, ?,?, ?, ?, ?)
DEBUG [main] - ==> Parameters: test0(String), 123456(String),
[email protected](String),
null, null, null,
test1(String), 123456(String),
[email protected](String),
null, null, null
DEBUG [main] - <== Updates: 2
從日誌中可以看到通過批量SQL語句插入了兩條數據。
從 MyBatis 3.3.1 版本開始,MyBatis 開始支持批量新增回寫主鍵值的功能(該功能由本書作者提交),這個功能首先要求數據庫主鍵值爲自增類型,同時還要求該數據庫提供的 JDBC 驅動可以支持返回批量插入的主鍵值(JDBC 提供了接口,但並不是所有數據庫都完美實現了該接口),因此到目前爲止,可以完美支持該功能的僅有 MySQL 數據庫。由於 SQL Server 數據庫官方提供的 JDBC 只能返回最後一個插入數據的主鍵值,所以不能支持該功能。
如果要在 MySQL 中實現批量插入返回自增主鍵值,只需要在原來代碼基礎上做如下修改。
<insert id="insertList" useGeneratedKeys="true" keyProperty="id">
和單表一樣,增加了useGeneratedKeys
和keyProperty
兩個屬性,增加這兩個屬性後,簡單修改測試類,輸出 id
值。
//在調用insertList之後
for(SysUser user : userList){
System.out.println(user.getId());
}
執行測試後,可以看到id部分的日誌如下。
1023
1024
關於批量插入的內容就介紹這麼多,對於不支持該功能的數據庫,許多人會通過 select...union all select...
的方式去實現,這種方式不同數據庫的實現也不同,並且這種實現也不安全,這裏不再提供示例。
4.4.3 foreach
實現動態UPDATE
這一節主要講當參數類型是 Map
時,foreach
如何實現動態 UPDATE 。
當參數是 Map
類型的時候,foreach
標籤的 index
屬性值對應的不是索引值,而是 Map
中的 key,利用這個 key 可以實現動態 UPDATE。
現在需要通過指定的列名和對應的值去更新數據,實現代碼如下。
<update id="updateByMap">
update sys_user
set
<foreach collection="_parameter" item="val" index="key" separator=",">
${key} = #{val}
</foreach>
where id = #{id}
</update>
這裏的 key 作爲列名,對應的值作爲該列的值,通過 foreache
將需要更新的字段拼在 SQL 語句中。
該 SQL 對應在 UserMapper
接口中的方法如下。
/**
* 通過Map更新列
*
* @param map
* @return
*/
int updateByMap(Map<String, Object> map);
這裏沒有通過@Param
註解指定參數名,因而MyBatis在內部的上下文中使用了默認值_parameter
作爲該參數的key,所以在XML中就使用了_parameter
。編寫測試代碼如下。
@Test
public void testUpdateByMap(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> map = new HashMap<String, Object>();
//查詢條件,同樣也是更新字段,必須保證該值存在
map.put("id", 1L);
//要更新的其他字段
map.put("user_email", "[email protected]");
map.put("user_password", "12345678");
//更新數據
userMapper.updateByMap(map);
//根據當前id查詢修改後的數據
SysUser user = userMapper.selectById(1L);
Assert.assertEquals("[email protected]", user.getUserEmail());
} finally {
//爲了不影響數據庫中的數據導致其他測試失敗,這裏選擇回滾
sqlSession.rollback();
//不要忘記關閉sqlSession
sqlSession.close();
}
}
測試代碼輸出日誌如下。
DEBUG [main] - ==> Preparing: update sys_user
set user_email = ? ,
user_password = ? ,
id = ?
where id = ?
DEBUG [main] - ==> Parameters: [email protected](String),
12345678(String),
1(Long),
1(Long)
DEBUG [main] - <== Updates: 1
DEBUG [main] - ==> Preparing: select * from sys_user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <== Columns: id, user_name, user_password, user_email,
user_info, head_img, create_time
TRACE [main] - <== Row: 1, admin, 12345678, [email protected],
<<BLOB>>, <<BLOB>>, 2016-06-07 00:00:00.0
DEBUG [main] - <== Total: 1
到這裏,foreach
的全部內容就介紹完了,下一節將介紹 bind
的用法。
4.5 bind
用法
bind
標籤可以使用 OGNL 表達式創建一個變量並將其綁定到上下文中。在前面的例子中,UserMapper.xml 有一個selectByUser
方法,這個方法用到了 like
查詢條件,部分代碼如下。
<if test="userName != null and userName != ''">
and user_name like concat('%', #{userName}, '%')
</if>
使用 concat
函數連接字符串,在 MySQL 中,這個函數支持多個參數,但在 Oracle 中只支持兩個參數。由於不同數據庫之間的語法差異,如果更換數據庫,有些 SQL 語句可能就需要重寫。針對這種情況,可以使用 bind
標籤來避免由於更換數據庫帶來的一些麻煩。將上面的方法改爲 bind
方式後,代碼如下。
<if test="userName != null and userName != ''">
<bind name="userNameLike" value="'%' + userName + '%'"/>
and user_name like #{userNameLike}
</if>
bind
標籤的兩個屬性都是必選項,name
爲綁定到上下文的變量名,value
爲 OGNL 表達式。創建一個 bind
標籤的變量後,就可以在下面直接使用,使用 bind
拼接字符串不僅可以避免因更換數據庫而去修改 SQL,也能預防 SQL 注入。大家可以根據需求,靈活使用 OGNL 表達式來實現功能,關於更多的 OGNL 常用的表達式,會在 4.7 節中詳細介紹。
聯繫作者
由衷地感謝大家購買此書,希望大家會喜歡,也希望這本書能夠爲各位讀者帶來所希望獲得的知識。雖然我已經非常細心地檢查書中所提到的所有內容,但仍有可能存在疏漏,若大家在閱讀過程中發現錯誤,在此我先表示歉意。歡迎各位讀者對本書的內容和相關源代碼發表意見和評論。大家可以通過我的個人郵箱[email protected] 與我取得聯繫,我會一一解答每個人的疑惑。
爲了方便交流,專門在GitHub創建了本書對應的項目。
歡迎大家通過GitHub,使用Issues方式進行交流:
公衆號
關注公衆號可以獲取更多 MyBatis 相關信息。