Mybatis框架(1)---Mybatis入門
mybatis入門
MyBatis是什么?
MyBatis 本是apache的一個開源項目iBatis, 2010年這個項目由apache software foundation 遷移到了google code,並且改名為MyBatis,實質上Mybatis對ibatis進行一些改進。 目前mybatis在github上托管。 git(分布式版本控制,當前比較流程)
MyBatis是一個優秀的持久層框架,它對jdbc的操作數據庫的過程進行封裝,使開發者只需要關注 SQL 本身,而不需要花費精力去處理例如注冊驅動、創建connection、創建statement、手動設置參數、結果集檢索等jdbc繁雜的過程代碼。
Mybatis通過xml或注解的方式將要執行的各種statement(statement、preparedStatemnt、CallableStatement)配置起來,並通過java對象和statement中的sql進行映射生成最終執行的sql語句,最后由mybatis框架執行sql並將結果映射成java對象並返回。
mybatis架構

搭建開發環境
(1)導包

(2)導入配置文件

這里我在工程文件下新建了一個和src平級的文件,把有關mybatis配置文件和src文件分離,看去界面更加清晰,因為在ssh開發中你肯定還要配置其它配置文件
這里的log4j.properties主要是為了在后台輸出是更加看的清楚執行流程,這個可要可不要.
(3)配需相關文件屬性
User.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">
<!-- namespace命名空間,為了對sql語句進行隔離,方便管理 ,mapper開發dao方式,使用namespace有特殊作用 -->
<mapper namespace="test">
<!-- 在mapper.xml文件中配置很多的sql語句,執行每個sql語句時,封裝為MappedStatement對象
mapper.xml以statement為單位管理sql語句
-->
<!-- 根據id查詢用戶信息 -->
<!--
id:唯一標識 一個statement
#{}:表示 一個占位符,如果#{}中傳入簡單類型的參數,#{}中的名稱隨意
parameterType:輸入 參數的類型,通過#{}接收parameterType輸入 的參數
resultType:輸出結果 類型,不管返回是多條還是單條,指定單條記錄映射的pojo類型
-->
<select id="findUserById" parameterType="int" resultType="com.study.model.User">
SELECT * FROM USER WHERE id= #{id}
</select>
<!-- 根據用戶名稱查詢用戶信息,可能返回多條
${}:表示sql的拼接,通過${}接收參數,將參數的內容不加任何修飾拼接在sql中。
-->
<select id="findUserByName" parameterType="java.lang.String" resultType="com.study.model.User">
select * from user where username like '%${value}%'
</select>
<!-- 添加用戶
parameterType:輸入 參數的類型,User對象 包括 username,birthday,sex,address
#{}接收pojo數據,可以使用OGNL解析出pojo的屬性值
#{username}表示從parameterType中獲取pojo的屬性值
selectKey:用於進行主鍵返回,定義了獲取主鍵值的sql
order:設置selectKey中sql執行的順序,相對於insert語句來說
keyProperty:將主鍵值設置到哪個屬性
resultType:select LAST_INSERT_ID()的結果 類型
-->
<insert id="insertUser" parameterType="com.study.model.User">
<selectKey keyProperty="id" order="AFTER" resultType="int">
select LAST_INSERT_ID()
</selectKey>
INSERT INTO USER(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})
</insert>
<!-- 用戶刪除 -->
<delete id="deleteUser" parameterType="int">
delete from user where id=#{id}
</delete>
<!-- 用戶更新
要求:傳入的user對象中包括 id屬性值
-->
<update id="updateUser" parameterType="com.study.model.User">
update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id=#{id}
</update>
</mapper>
SqlMapConfig.xml
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 和spring整合后 environments配置將廢除-->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事務管理-->
<transactionManager type="JDBC" />
<!-- 數據庫連接池-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<!-- 加載mapper.xml -->
<mappers>
<mapper resource="sqlmap/User.xml" />
</mappers>
</configuration>
User.java
public class User {
private int id;
private String username;// 用戶姓名
private String sex;// 性別
private Date birthday;// 生日
private String address;// 地址
/*
*提供set和get方法,和toString方法
*
*/
}
MybatisFirst 測試類,進行增刪改查
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
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.guigu.model.User;
public class MybatisFirst {
// 會話工廠
private SqlSessionFactory sqlSessionFactory;
// 創建工廠
@Before //before在text標簽之前執行,所以會創建好sqlSessionFactory對象
public void init() throws IOException {
// 配置文件(SqlMapConfig.xml)
String resource = "SqlMapConfig.xml";
// 加載配置文件到輸入 流
InputStream inputStream = Resources.getResourceAsStream(resource);
// 創建會話工廠
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
// 根據id查詢用戶(得到單條記錄)
@Test
public void testFindUserById() {
// 通過sqlSessionFactory創建sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 通過sqlSession操作數據庫
// 第一個參數:statement的位置,等於namespace+statement的id
// 第二個參數:傳入的參數
User user = sqlSession.selectOne("test.findUserById", 16);
sqlSession.close();
System.out.println(user);
}
// 模糊查詢(可能是單條也可能是多條)
@Test
public void testFindUserByName() {
// 通過sqlSessionFactory創建sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
//selectList代表着返回是list集合
List<User> list = sqlSession.selectList("test.findUserByName", "小明");
sqlSession.close();
System.out.println(list.get(0).getUsername());
}
// 添加用戶
@Test
public void testInsertUser() {
// 通過sqlSessionFactory創建sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = new User();
user.setUsername("小小徐");
user.setAddress("杭州市余杭區未來科技城");
user.setBirthday(new Date());
user.setSex("1");
//insert代表插入
sqlSession.insert("test.insertUser", user);
//查看是不需要提交事物的,但是插入和修改是需要提交事物的
sqlSession.commit();
sqlSession.close();
//這里輸出的id竟然是0,有哪位大神解釋下嗎?
System.out.println("用戶的id=" + user.getId());
}
// 根據id刪除用戶
@Test
public void testDeleteUser() {
// 通過sqlSessionFactory創建sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// delete代表刪除用戶
sqlSession.delete("test.deleteUser", 29);
sqlSession.commit();
sqlSession.close();
}
// 根據id更新用戶
@Test
public void testUpdateUser() {
// 通過sqlSessionFactory創建sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 創建更新數據對象,要求必須包括 id
User user = new User();
user.setId(40);
user.setUsername("小小鍾");
user.setAddress("杭州余杭區東西大道");
//user.setBirthday(new Date());
user.setSex("1");
//update更新數據
sqlSession.update("test.updateUser", user);
sqlSession.commit();
sqlSession.close();
System.out.println("用戶的id=" + user.getId());
}
}
mybatis與hibernate重要區別
企業開發進行技術選型 ,考慮mybatis與hibernate適用場景。
mybatis:入門簡單,程序容易上手開發,節省開發成本 。mybatis需要程序員自己編寫sql語句,是一個不完全 的ORM框架,對sql修改和優化非常容易實現 。
mybatis適合開發需求變更頻繁的系統,比如:互聯網項目。
hibernate:入門門檻高,如果用hibernate寫出高性能的程序不容易實現。hibernate不用寫sql語句,是一個 ORM框架。
hibernate適合需求固定,對象數據模型穩定,中小型項目,比如:企業OA系統。
總之,企業在技術選型時根據項目實際情況,以降低成本和提高系統 可維護性為出發點進行技術選型。
總結
SqlMapConfig.xml
是mybatis全局配置文件,只有一個,名稱不固定的,主要mapper.xml,mapper.xml中配置 sql語句
mapper.xml
mapper.xml是以statement為單位進行配置。(把一個sql稱為一個statement),satatement中配置 sql語句、parameterType輸入參數類型(完成輸入映射)、resultType輸出結果類型(完成輸出映射)。還提供了parameterMap配置輸入參數類型(過期了,不推薦使用了),還提供resultMap配置輸出結果類型(完成輸出映射)
#{}
表示一個占位符,向占位符輸入參數,mybatis自動進行java類型和jdbc類型的轉換。程序員不需要考慮參數的類型,比如:傳入字符串,mybatis最終拼接好的sql就是參數兩邊加單引號。#{}接收pojo數據,可以使用OGNL解析出pojo的屬性值
${}
表示sql的拼接,通過${}接收參數,將參數的內容不加任何修飾拼接在sql中。${}也可以接收pojo數據,可以使用OGNL解析出pojo的屬性值
缺點:不能防止sql注入。
selectOne
用於查詢單條記錄,不能用於查詢多條記錄,否則異常:
org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 4
selectList
用於查詢多條記錄,可以用於查詢單條記錄的。
本文就講到這里,歡迎大家指點,本文引用了博客名叫:凌晨。。。三點,非常感謝!
Mybatis框架(2)---mapper代理方法
mapper代理方法
在我們在寫MVC設計的時候,都會寫dao層和daoimp實現層,但假如我們使用mapper代理的方法,我們就可以不用先daoimp實現類
當然這得需要遵守一些相應的規則:
(1)Usermapper.java接口必須和Usermapper.xml名稱相同,且要在同一目錄下:

(2)mapper.xml中namespace等於mapper接口的地址

(3)Usermapper.java接口中國的方法名和Usermapper.xml中statement的id一致
<!-- 7綜合查詢 -->
<select id="findUserCount" parameterType="com.study.model.User" resultType="int">
select count(*) from user where user.sex=#{userCustomer.sex} and user.username like '%${userCustomer.username}%'
</select>
如果你在Usermapper.xml配置上面這些屬性那么你所寫的接口就必須:
1 /*findUserCount接口的名字必須和id屬性一致 2 * 傳入的參數必須和parameterType是一致,前面是user這里也是user 3 * 返回類型resultType是int類型,那么這里也必須是int類型 4 */ 5 public int findUserCount(User user);
(4)SqlMapConfig.xml中加載mapper.xml
1 <mappers> 2 <!-- 這里是之前加載所寫的 --> 3 <!-- <mapper resource="sqlmap/User.xml" /> --> 4 <!-- 通過mapper接口 加載單個映射文件 必須遵循一些規范: 需要將mapper接口和mapper.xml映射文件 文件名必須一致 並且在同一個目錄下 --> 5 <mapper class="com.study.mapper.UserMapper" /> 6 7 </mappers>
(5)通過mapper代理方法進行增刪改查
a.編寫user對象
public class User {
private int id;
private String username;// 用戶姓名
private String sex;// 性別
private Date birthday;// 生日
private String address;// 地址
/*
*提供set和get方法和tostring方法
*
*/
}
b.配置SqlMapConfig.xml
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"></properties>
<!-- 定義 別名 -->
<typeAliases>
<!--
單個別名的定義
alias:別名,type:別名映射的類型 -->
<!-- <typeAlias type="com.study.model.User" alias="user"/> -->
<!-- 批量別名定義
指定包路徑,自動掃描包下邊的pojo,定義別名,別名默認為類名(首字母小寫或大寫)
-->
<package name="com.study.model"/>
</typeAliases>
<!-- 和spring整合后 environments配置將廢除-->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事務管理-->
<transactionManager type="JDBC" />
<!-- 數據庫連接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!--加載mapper映射
如果將和spring整合后,可以使用整合包中提供的mapper掃描器,此處的mappers不用配置了。
-->
<mappers>
<mapper class="com.study.mapper.UserMapper" />
</mappers>
</configuration>
在這里有兩個新的知識點:
1: <properties resource="db.properties"></properties>
之前在連接數據庫填寫配置文件直接把屬性(連接數據庫用戶名,密碼等)寫在里面,而這里是寫在外面的db.properties中,這樣更好的體現代碼的靈活性
2:<typeAliases>標簽,之前我們配置mapper.xml文件中的parameterType和resultType的屬性如果是對象一定要寫類的全名稱,而通過<typeAliases>標簽的配置我們只需要寫類的名字就好了
c.配置db.properties
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc\:mysql\://localhost\:3306/study jdbc.username=root jdbc.password=root
也就是這樣的

d.配置UserMapper.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">
<!-- namespace的屬性對應所在的UserMapper接口全名稱 -->
<mapper namespace="com.study.mapper.UserMapper">
<!-- 發現這里的resultType屬性我們可以不用寫類的全名稱com.study.model.User,因為在-->
<!--SqlMapConfig.xml屬性中我們配置了<typeAliases>標簽 -->
<!-- 根據id查詢用戶信息 -->
<select id="findUserById" parameterType="int" resultType="user">
SELECT * FROM USER WHERE id= #{id}
</select>
<!-- 根據用戶名稱查詢用戶信息,可能返回多條-->
<select id="findUserByName" parameterType="java.lang.String" resultType="user">
select * from user where username like '%${value}%'
</select>
<!-- 添加用戶-->
<insert id="insertUser" parameterType="user">
INSERT INTO USER(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})
</insert>
</mapper>
f.配置UserMapper.java對象
public interface UserMapper {
//根據用戶id查詢用戶信息
public User findUserById(int id) throws Exception;
//根據用戶名稱 查詢用戶信息
public List<User> findUserByName(String username) throws Exception;//插入用戶
public void insertUser(User user)throws Exception;
//刪除用戶
public void deleteUser(int id) throws Exception;
//修改用戶
public void updateUser(User user) throws Exception;
}
e.編寫UserMapperTest類進行 增刪改查
1 import java.io.IOException;
2 import java.io.InputStream;
3 import java.util.ArrayList;
4 import java.util.List;
5
6 import org.apache.ibatis.io.Resources;
7 import org.apache.ibatis.session.SqlSession;
8 import org.apache.ibatis.session.SqlSessionFactory;
9 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
10 import org.junit.Before;
11 import org.junit.Test;
12
13 import com.study.mapper.UserMapper;
14 import com.study.model.User;
15
16
17 public class UserMapperTest {
18 // 會話工廠
19 private SqlSessionFactory sqlSessionFactory;
20 // 創建工廠
21 @Before
22 public void init() throws IOException {
23 String resource = "SqlMapConfig.xml";
24 InputStream inputStream = Resources.getResourceAsStream(resource);
25 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
26 }
27 //通過用戶id查找對象
28 @Test
29 public void testFindUserById() throws Exception {
30 SqlSession sqlSession = sqlSessionFactory.openSession();
31 // 創建代理對象,這里就相當於有事先類了
32 UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
33 User user = userMapper.findUserById(1);
34 System.out.println(user);
35 }
36
37 //根據用戶相信模糊查詢
38 @Test
39 public void testFindUserByUsername() throws Exception {
40 SqlSession sqlSession = sqlSessionFactory.openSession();
41 UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
42 List<User> list = userMapper.findUserByName("小明");
43 System.out.println(list);
44 }
45
46 //添加用戶
47 @Test
48 public void testInsertUser() throws Exception {
49 SqlSession sqlSession = sqlSessionFactory.openSession();
50 UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
51 User user = new User();
52 user.setUsername("小小洪");
53 //我這里只添加了用戶名,其它信息沒有添加,默認為null
54 //Preparing: INSERT INTO USER(username,birthday,sex,address) VALUES(?,?,?,?)
55 //Parameters: 小小洪(String), null, null, null
56 userMapper.insertUser(user);
57 sqlSession.commit();
58 sqlSession.close();
59 }
60 }
61 /*
62 *刪除和修改我這里先不寫了,大家理解就好
63 */
本文就講到這,謝謝大家,歡迎大家指點謝謝!
Mybatis框架(3)---SqlMapConfig.xml解析
SqlMapConfig.xml
SqlMapConfig.xml是Mybatis的全局配置參數,關於他的具體用的有專門的MyBatis - API文檔,這里面講的非常清楚,所以我這里就挑幾個講下:
他的主要配置的屬性有如下:

1.properties 屬性
這些屬性都是可外部配置且可動態替換的,既可以在典型的 Java 屬性文件中配置,亦可通過 properties 元素的子元素來傳遞。
1 <!-- 加載屬性文件 --> 2 <properties resource="db.properties"> 3 <!-- 可以在配置相關的其他事項 --> 4 <!-- <property name="jdbc.driver" value="com.mysql.jdbc.Driver"/> --> 5 </properties> 6 <!-- 這里如果在db.properties和 name=""都配置了jdbc.driver那么優先執行name中的--> 7 <!-- 配置全局的參數信息 -->
如果屬性在不只一個地方進行了配置,那么 MyBatis 將按照下面的順序來加載:
- 在 properties 元素體內指定的屬性首先被讀取。
- 然后根據 properties 元素中的 resource 屬性讀取類路徑下屬性文件或根據 url 屬性指定的路徑讀取屬性文件,並覆蓋已讀取的同名屬性。
- 最后讀取作為方法參數傳遞的屬性,並覆蓋已讀取的同名屬性。
因此,通過方法參數傳遞的屬性具有最高優先級,resource/url 屬性中指定的配置文件次之,最低優先級的是 properties 屬性中指定的屬性。
2.settings全局的參數配置
這是 MyBatis 中極為重要的調整設置,它們會改變 MyBatis 的運行時行為。
具體的就不寫了只寫一個表達式:
<!-- 配置全局的參數信息 -->
<settings>
<setting name="" value=""/>
</settings>
3.typeAliases(別名)
制定別名最大的一個優勢就是方便我們的開發,因為我們如果沒有設置別名的情況下,在mapper.xml中
定義了很多Statement ,Statement需要parameterType指定輸入參數的類型或者指定輸出結果的類型比如:
<!-- 根據id查詢用戶信息 -->
<select id="findUserById" parameterType="int" resultType="com.study.model.User">
SELECT * FROM USER WHERE id= #{id}
</select>
這里的resultType如果是對象一定要類的全名稱,那我們能不能只寫了user就能達到同樣的效果,這樣是不是就有利於簡便我們的開發
有兩種方法:
1.單個別名的定義
1 <typeAliases> 2 <!-- 3 單個別名的定義 4 alias:別名,type:別名映射的類型 --> 5 <!-- <typeAlias type="com.study.model.User" alias="user"/> --> 6 </typeAliases> 7 <!--這樣的話只需輸入user就能起到一樣效果-->
2.批量定義別名(常用)
1 <typeAliases> 2 <!-- 批量別名定義 3 指定包路徑,自動掃描包下邊的pojo,定義別名,別名默認為類名(首字母小寫或大寫) 4 --> 5 <package name="com.study.model"/> 6 </typeAliases>
4.mappers(映射配置)
1.通過resource加載單個映射文件
1 <!-- 加載映射文件 --> 2 <mappers> 3 <mapper resource="sqlmap/User.xml" /> 4 </mappers>
2.通過mapper接口加載單個mapper
1 <mappers> 2 <!-- 通過mapper接口 加載單個映射文件 必須遵循一些規范: 需要將mapper接口和mapper.xml映射文件 文件名必須一致 並且在同一個目錄下 --> 3 <mapper class="com.study.mapper.UserMapper" /> 4 </mappers>
3.批量加載mapper(推薦使用)
1 <mappers> 2 <!-- 3 指定mapper接口的包名 mybatis會自動掃描這個包下所有的mapper接口 然后執行加載 4 --> 5 <package name="com.study.mapper"/> 6 </mappers>
其他的我就不講了,要詳細的可以找api,非常的詳細,歡迎大家指點,謝謝!
Mybatis框架(4)---輸入輸出映射
輸入輸出映射
通過parameterType制定輸入參數類型 類型可以是簡單類型(int String)也可以是POJO本身 或者包裝類
1輸入映射
關於輸入簡單類型和pojo本身的我就不寫了,因為比較簡單,下面我主要舉一個包裝類的例子:
使用包裝類POJO 將復雜的查詢條件封裝到POJO中
1 //當你繼承user屬性后,你就可以在user的基礎上添加自己的屬性了
2 public class UserCustomer extends User {
3
4 //用戶的基本信息
5 //可以擴展用戶的信息
6 //其他信息
7 }
查詢條件封裝的類
1 public class UserQueryVo {
2
3 //這里包裝需要查詢的條件
4
5 private UserCustomer userCustomer;
6
7 public UserCustomer getUserCustomer() {
8 return userCustomer;
9 }
10
11 public void setUserCustomer(UserCustomer userCustomer) {
12 this.userCustomer = userCustomer;
13 }
UserMapper.xml
1 <!--
2 #{userCustomer.sex} 取出pojo對象 中性別的屬性值
3 ${userCustomer.username}取出pojo中 用戶的名稱
4 -->
5 <select id="findUserList" parameterType="com.study.model.UserQueryVo" resultType="com.study.model.UserCustomer">
6 select * from user where user.sex=#{userCustomer.sex} and user.username like '%${userCustomer.username}%'
7 </select
UserMapper.java
//用戶綜合信息查詢
public List<UserCustomer> findUserList(UserQueryVo userQueryVo) throws Exception;
測試代碼
1 @Test
2 public void testFindUserList() throws Exception{
3 SqlSession sqlSession =sqlSessionFactory.openSession();
4 //創建UserMapper 對象 MyBatis自動生成代理對象
5 UserMapper userMapper =sqlSession.getMapper(UserMapper.class);
6
7 //創建包裝對象 設置查詢條件
8 UserQueryVo userQueryVo =new UserQueryVo();
9
10 UserCustomer userCustomer =new UserCustomer();
11 userCustomer.setSex("1");
12 userCustomer.setUsername("小明");
13 userQueryVo.setUserCustomer(userCustomer);
14
15 //完成查詢
16 List<UserCustomer> list =userMapper.findUserList(userQueryVo);
17 System.out.println(list);
18
}
2.輸出映射
(1)resultType
使用resultType進行輸出映射的時候 只有查詢出來的列名和pojo 對應的屬性名完全一致 才可以映射
如果查詢出來的列名和pojo中的屬性完全不一致 沒有創建pojo對象
如果查詢出來的列名和pojo中的屬性只有部分一致 ,就會創建pojo對象 ,不一致的屬性值為null
舉例:查詢用戶總人數
mapper.xml
1 <!-- 7綜合查詢 -->
2 <select id="findUserCount" parameterType="com.guigu.model.UserQueryVo" resultType="int">
3 select count(*) from user where user.sex=#{userCustomer.sex} and user.username like '%${userCustomer.username}%'
4 </select>
mapper.java
public int findUserCount(UserQueryVo userQueryVo);
測試代碼
@Test
public void testFindUserCount() throws Exception{
SqlSession sqlSession =sqlSessionFactory.openSession();
//創建UserMapper 對象 MyBatis自動生成代理對象
UserMapper userMapper =sqlSession.getMapper(UserMapper.class);
//創建包裝對象 設置查詢條件
UserQueryVo userQueryVo =new UserQueryVo();
UserCustomer userCustomer =new UserCustomer();
userCustomer.setSex("1");
userCustomer.setUsername("小明");
userQueryVo.setUserCustomer(userCustomer);
int count =userMapper.findUserCount(userQueryVo);
System.out.println(count);
}
在輸出參數中,不論你返回的是單個對象還是對象的集合,在resulttype中都只需要寫該對象的全名稱就可以了
(2)resultMap
resultMap到底做什么用的呢?下面我來舉個例子:

比如有下面的mapper.xml配置
<!-- mapper執行語句 -->
<!--
#{userCustomer.sex} 取出pojo對象 中性別的屬性值
${userCustomer.username}取出pojo中 用戶的名稱
-->
<select id="findUserList" parameterType="com.guigu.model.UserQueryVo" resultType="com.guigu.model.UserCustomer">
select id id_,username username_,birthday birthday_,address from user where user.sex=#{userCustomer.sex} and user.username like '%${userCustomer.username}%'
</select>
那么運行的輸出結果:會發現只有地址能夠完成賦值,而其它因為采用別名無法賦值:

得出結論:
如果查詢出來的列名和pojo中的屬性完全不一致 沒有創建pojo對象
如果查詢出來的列名和pojo中的屬性只有部分一致 ,就會創建pojo對象 ,不一致的屬性值為null
上面的問題那如何解決,其實也很簡單就是配置resultMap:
<!--
type resultMap最終映射的java對象類型 可以使用別名,因為本來是要寫類的全名稱,這里輸入的就是別名
id 對resultMap唯一的標識符,這里的id要和下面的resultMap中的內容一致
-->
<resultMap type="user" id="userResultMap">
<!--
id表示查詢結果集中唯一的標識 主鍵
column 查詢出來的列名
property type pojo中對應的屬性名
-->
<id column="id_" property="id"/>
<!--
result對普通名的映射
column 查詢出來的列名
property type pojo中對應的屬性名
-->
<result column="username_" property="username"/>
<result column="birthday_" property="birthday"/>
</resultMap>
<!-- 配置結果集類型 -->
<select id="findUserByIdResultMap" parameterType="int" resultMap="userResultMap">
select id id_,username username_,birthday birthday_,address from user where id=#{value}
</select>
總結:
使用resultType 進行輸出映射 只有查詢出來的列名 和pojo中的屬性名一致的時候才可以映射成功 。
如果查詢出來的列名和pojo屬性名不一致 可以通過定義一個resultMap對列名和pojo屬性之間做一個映射。
本文就講到這里,歡迎大家多多指點,哪里需要修正或者補充,歡迎留言,謝謝!
Mybatis框架(5)---動態sql
那么,問題來了: 什么是動態SQL? 動態SQL有什么作用?
傳統的使用JDBC的方法,相信大家在組合復雜的的SQL語句的時候,需要去拼接,稍不注意哪怕少了個空格,都會導致錯誤。Mybatis的動態SQL功能正是為了解決這種問題, 其通過 if, choose, when, otherwise, trim, where, set, foreach標簽,可組合成非常靈活的SQL語句,從而提高開發人員的效率。下面就去感受Mybatis動態SQL的魅力吧:
1. if: 你們能判斷,我也能判斷!
作為程序猿,誰不懂 if ! 在mybatis中也能用 if 啦:
<select id="findUserById" resultType="user">
select * from user where
<if test="id != null">
id=#{id}
</if>
and deleteFlag=0;
</select>
上面例子: 如果傳入的id 不為空, 那么才會SQL才拼接id = #{id}。 這個相信大家看一樣就能明白,不多說。
細心的人會發現一個問題:“你這不對啊! 要是你傳入的id為null, 那么你這最終的SQL語句不就成了 select * from user where and deleteFlag=0, 這語句有問題!”
是啊,這時候,mybatis的 where 標簽就該隆重登場啦:
2. where, 有了我,SQL語句拼接條件神馬的都是浮雲!
咱們通過where改造一下上面的例子:
<select id="findUserById" resultType="user">
select * from user
<where>
<if test="id != null">
id=#{id}
</if>
and deleteFlag=0;
</where>
</select>
有些人就要問了: “你這都是些什么玩意兒! 跟上面的相比, 不就是多了個where標簽嘛! 那這個還會不會出現 select * from user where and deleteFlag=0 ?”
的確,從表面上來看,就是多了個where標簽而已, 不過實質上, mybatis是對它做了處理,當它遇到AND或者OR這些,它知道怎么處理。其實我們可以通過 trim 標簽去自定義這種處理規則。
3. trim : 我的地盤,我做主!
上面的where標簽,其實用trim 可以表示如下:
<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>
它的意思就是: 當WHERE后緊隨AND或則OR的時候,就去除AND或者OR。 除了WHERE以外, 其實還有一個比較經典的實現,那就是SET。
4. set: 信我,不出錯!
<update id="updateUser" parameterType="com.dy.entity.User">
update user set
<if test="name != null">
name = #{name},
</if>
<if test="password != null">
password = #{password},
</if>
<if test="age != null">
age = #{age}
</if>
<where>
<if test="id != null">
id = #{id}
</if>
and deleteFlag = 0;
</where>
</update>
問題又來了: “如果我只有name不為null, 那么這SQL不就成了 update set name = #{name}, where ........ ? 你那name后面那逗號會導致出錯啊!”
是的,這時候,就可以用mybatis為我們提供的set 標簽了。下面是通過set標簽改造后:
<update id="updateUser" parameterType="com.dy.entity.User">
update user
<set>
<if test="name != null">name = #{name},</if>
<if test="password != null">password = #{password},</if>
<if test="age != null">age = #{age},</if>
</set>
<where>
<if test="id != null">
id = #{id}
</if>
and deleteFlag = 0;
</where>
</update>
這個用trim 可表示為:
<trim prefix="SET" suffixOverrides=","> ... </trim>
WHERE是使用的 prefixOverrides(前綴), SET是使用的 suffixOverrides (后綴), 看明白了吧!
5. foreach: 你有for, 我有foreach, 不要以為就你才屌!
java中有for, 可通過for循環, 同樣, mybatis中有foreach, 可通過它實現循環,循環的對象當然主要是java容器和數組。
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
將一個 List 實例或者數組作為參數對象傳給 MyBatis,當這么做的時候,MyBatis 會自動將它包裝在一個 Map 中並以名稱為鍵。List 實例將會以“list”作為鍵,而數組實例的鍵將是“array”。同樣, 當循環的對象為map的時候,index其實就是map的key。
6. choose: 我選擇了你,你選擇了我!
Java中有switch, mybatis有choose。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
以上例子中: 當title和author都不為null的時候, 那么選擇二選一(前者優先), 如果都為null, 那么就選擇 otherwise中的, 如果tilte和author只有一個不為null, 那么就選擇不為null的那個。
縱觀mybatis的動態SQL, 強大而簡單, 相信大家簡單看一下就能使用了。
好啦,本次就寫到這!下篇文章將結合mybatis的源碼分析一次sql語句執行的整個過程。
Mybatis框架(6)---Mybatis插入數據后獲取自增主鍵
Mybatis插入數據后獲取自增主鍵
首先理解這就話的意思:就是在往數據庫表中插入一條數據的同時,返回該條數據在數據庫表中的自增主鍵值。
有什么用呢,舉個例子:
你編輯一條新聞,同時需要給該新聞打上標簽(可以一個或者多個:比如:錢等等),然后存儲到數據庫中。怎么存,肯定涉及到三張表,新聞表,標簽表,新聞標簽id關聯表
新聞表插入數據簡單,標簽表插入數據簡單。那新聞標簽表呢,如何關聯,那是不是需要新聞表和標簽表插入數據的時候,返回它們的主鍵Id然后再存儲到新聞標簽表中。
這種場景還是蠻常見的。下面主要針對的MySQL數據庫進行操作。
1.TLivePressOriginDOMapper.xml插入語句添加配置
<!-- 主要講新添加的兩個屬性:useGeneratedKeys和keyProperty--> <!--useGeneratedKeys="true" 默認值是:false。 含義:設置是否使用JDBC的getGenereatedKeys方法獲取主鍵並賦值到keyProperty設置的領域模型屬性中。--> <!--keyProperty="autoId" 就很好理解了,就是把主鍵值賦值給TLivePressOriginDO實體的autoId屬性中--> <insert id="insertSelective" parameterType="com.jincou.dlo.TLivePressOriginDO" useGeneratedKeys="true" keyProperty="autoId">
2.查看TLivePressOriginDO實體
有個屬性autoId

3.在看TLivePressOriginDOMapper

4、在看實際效果
我們看到這里數據的主鍵值是10,是通過賦值給bean實體中的autoId屬性的。

那到底數據庫存儲該條數據的主鍵是不是10呢?
5、看數據庫該條記錄
數據庫中該條數據的自增主鍵Id果然是10,那就說明達到了插入數據的同時獲得了該條數據在數據庫表中的主鍵值的目的。

注意:以上操作只針對MySQL數據庫哦。
Mybatis框架(7)---Mybatis逆向工程
逆向工程的目的就是縮減了我們的開發時間。所謂Mybatis逆向工程,就是Mybatis會根據我們設計好的數據表,自動生成pojo、mapper以及mapper.xml。
接下來就是項目搭建過程。github源碼:mybatis逆向工程代碼
一、pom.xml文件
<!--連接mysql驅動--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.41</version> </dependency> <!--mapper--> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>1.2.4</version> </dependency> <!-- mybatis 逆向生成工具 --> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> <scope>compile</scope> <optional>true</optional> </dependency>
二、generatorConfig.xml
這是配置逆向工程配置信息。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!--一個context對應一個數據庫--> <context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <!--設置父類mapper,這樣一來所有逆向工程生成的mapper都會繼承該mapper--> <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <property name="mappers" value="com.binron.mapper.base.BaseMapper"/> </plugin> <!--連接數據庫信息--> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://47.99.888.55:3306/binron" userId="root" password="root"> </jdbcConnection> <!-- 對於生成的pojo所在包 --> <javaModelGenerator targetPackage="com.binron.pojo" targetProject="src/main/java"/> <!-- 對於生成的mapper所在目錄 --> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/> <!-- 配置mapper對應的java映射 --> <javaClientGenerator targetPackage="com.binron.mapper" targetProject="src/main/java" type="XMLMAPPER"/> <!--需要逆向工程的表--> <table tableName="users"></table> <table tableName="my_friends"></table> <table tableName="friends_request"></table> <table tableName="chat_msg"></table> </context> </generatorConfiguration>
三、父類BaseMapper
在配置信息中,父類mapper不是必須的,不過一般企業開發中,肯定是有父類的,因為你不可能每個mapper都寫增刪改查方法,完全可以抽離。
/** * @author 基礎的Mapper 所有業務表都繼承之該mapper */ public interface BaseMapper<T> extends Mapper<T>, MySqlMapper<T> { //FIXME 特別注意,該接口不能被掃描到,否則會出錯 /** * 通過主鍵刪除 */ int deleteByPrimaryKey(String id); /** * 插入對象 */ int insert(T record); /** * 通過K 查找對象 */ T selectByPrimaryKey(String id); /** * 查找所有 */ List<T> selectAll(); /** * 更新 對象 */ int updateByPrimaryKey(T record); }
四、GeneratorDisplay(啟動類)
public class GeneratorDisplay { public void generator() throws Exception{ List<String> warnings = new ArrayList<String>(); boolean overwrite = true; //指定 逆向工程配置文件 File configFile = new File("generatorConfig.xml"); ConfigurationParser cp = new ConfigurationParser(warnings); Configuration config = cp.parseConfiguration(configFile); DefaultShellCallback callback = new DefaultShellCallback(overwrite); MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings); myBatisGenerator.generate(null); } public static void main(String[] args) throws Exception { try { GeneratorDisplay generatorSqlmap = new GeneratorDisplay(); generatorSqlmap.generator(); } catch (Exception e) { e.printStackTrace(); } } }
總體就是這么的簡單。
注意點
1、對於一張表已經完成逆向工程后,不能再執行一次main方法,除非在generatorConfig.xml刪除這個table,因為如果依次執行兩次main,那么mapper.xml 內容會疊加。 2、如果你的表有變動,那么你可以選擇先刪除該表所以創建好的逆向工程,重新生成,要么手動修改內容。
Mybatis框架(8)---Mybatis插件原理(代理+責任鏈)
在實際開發過程中,我們經常使用的Mybaits插件就是分頁插件了,通過分頁插件我們可以在不用寫count語句和limit的情況下就可以獲取分頁后的數據,給我們開發帶來很大
的便利。除了分頁,插件使用場景主要還有更新數據庫的通用字段,分庫分表,加解密等的處理。
這篇博客主要講Mybatis插件原理,下一篇博客會設計一個Mybatis插件實現的功能就是每當新增數據的時候不用數據庫自增ID而是通過該插件生成雪花ID,作為每條數據的主鍵。
一、JDK動態代理+責任鏈設計模式
Mybatis的插件其實就是個攔截器功能。它利用JDK動態代理和責任鏈設計模式的綜合運用。采用責任鏈模式,通過動態代理組織多個攔截器,通過這些攔截器你可以做一些
你想做的事。所以在講Mybatis攔截器之前我們先說說JDK動態代理+責任鏈設計模式。有關JDK動態代理的原理,可以參考我之前寫的一篇博客:【java設計模式】---代理模式
1、JDK動態代理案例
public class MyProxy { /** * 一個接口 */ public interface HelloService{ void sayHello(); } /** * 目標類實現接口 */ static class HelloServiceImpl implements HelloService{ @Override public void sayHello() { System.out.println("sayHello......"); } } /** * 自定義代理類需要實現InvocationHandler接口 */ static class HWInvocationHandler implements InvocationHandler { /** * 目標對象 */ private Object target; public HWInvocationHandler(Object target){ this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("------插入前置通知代碼-------------"); //執行相應的目標方法 Object rs = method.invoke(target,args); System.out.println("------插入后置處理代碼-------------"); return rs; } public static Object wrap(Object target) { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),new HWInvocationHandler(target)); } } public static void main(String[] args) { HelloService proxyService = (HelloService) HWInvocationHandler.wrap(new HelloServiceImpl()); proxyService.sayHello(); } }
運行結果
------插入前置通知代碼------------- sayHello...... ------插入后置處理代碼-------------
2、優化
上面代理的功能是實現了,但是有個很明顯的缺陷,就是HWInvocationHandler是動態代理類,也可以理解成是個工具類,我們不可能會把業務代碼寫到寫到到invoke方法里,
不符合面向對象的思想,可以抽象一下處理。可以設計一個Interceptor接口,需要做什么攔截處理實現接口就行了。
public interface Interceptor { /** * 具體攔截處理 */ void intercept(); }
intercept() 方法就可以處理各種前期准備了
public class LogInterceptor implements Interceptor { @Override public void intercept() { System.out.println("------插入前置通知代碼-------------"); } } public class TransactionInterceptor implements Interceptor { @Override public void intercept() { System.out.println("------插入后置處理代碼-------------"); } }
代理對象也做一下修改
public class HWInvocationHandler implements InvocationHandler { private Object target; private List<Interceptor> interceptorList = new ArrayList<>(); public TargetProxy(Object target,List<Interceptor> interceptorList) { this.target = target; this.interceptorList = interceptorList; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //處理多個攔截器 for (Interceptor interceptor : interceptorList) { interceptor.intercept(); } return method.invoke(target, args); } public static Object wrap(Object target,List<Interceptor> interceptorList) { HWInvocationHandler targetProxy = new HWInvocationHandler(target, interceptorList); return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),targetProxy); } }
現在可以根據需要動態的添加攔截器了,在每次執行業務代碼sayHello()之前都會攔截,看起來高級一點,來測試一下
public class Test { public static void main(String[] args) { List<Interceptor> interceptorList = new ArrayList<>(); interceptorList.add(new LogInterceptor()); interceptorList.add(new TransactionInterceptor()); HelloService target = new HelloServiceImpl(); Target targetProxy = (Target) TargetProxy.wrap(target,interceptorList); targetProxy.sayHello(); } }
運行結果
------插入前置通知代碼------------- ------插入后置處理代碼------------- sayHello......
3、再優化
上面的動態代理確實可以把代理類中的業務邏輯抽離出來,但是我們注意到,只有前置代理,無法做到前后代理,所以還需要在優化下。所以需要做更一步的抽象,
把攔截對象信息進行封裝,作為攔截器攔截方法的參數,把攔截目標對象真正的執行方法放到Interceptor中完成,這樣就可以實現前后攔截,並且還能對攔截
對象的參數等做修改。設計一個Invocation 對象。
public class Invocation { /** * 目標對象 */ private Object target; /** * 執行的方法 */ private Method method; /** * 方法的參數 */ private Object[] args; //省略getset public Invocation(Object target, Method method, Object[] args) { this.target = target; this.method = method; this.args = args; } /** * 執行目標對象的方法 */ public Object process() throws Exception{ return method.invoke(target,args); } }
Interceptor攔截接口做修改
public interface Interceptor { /** * 具體攔截處理 */ Object intercept(Invocation invocation) throws Exception; }
Interceptor實現類
public class TransactionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Exception{ System.out.println("------插入前置通知代碼-------------"); Object result = invocation.process(); System.out.println("------插入后置處理代碼-------------"); return result; } }
Invocation 類就是被代理對象的封裝,也就是要攔截的真正對象。HWInvocationHandler修改如下:
public class HWInvocationHandler implements InvocationHandler { private Object target; private Interceptor interceptor; public TargetProxy(Object target,Interceptor interceptor) { this.target = target; this.interceptor = interceptor; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Invocation invocation = new Invocation(target,method,args); return interceptor.intercept(invocation); } public static Object wrap(Object target,Interceptor interceptor) { HWInvocationHandler targetProxy = new HWInvocationHandler(target, interceptor); return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),targetProxy); } }
測試類
public class Test { public static void main(String[] args) { HelloService target = new HelloServiceImpl(); Interceptor transactionInterceptor = new TransactionInterceptor(); HelloService targetProxy = (Target) TargetProxy.wrap(target,transactionInterceptor); targetProxy.sayHello(); } }
運行結果
------插入前置通知代碼------------- sayHello...... ------插入后置處理代碼-------------
4、再再優化
上面這樣就能實現前后攔截,並且攔截器能獲取攔截對象信息。但是測試代碼的這樣調用看着很別扭,對應目標類來說,只需要了解對他插入了什么攔截就好。
再修改一下,在攔截器增加一個插入目標類的方法。
public interface Interceptor { /** * 具體攔截處理 */ Object intercept(Invocation invocation) throws Exception; /** * 插入目標類 */ Object plugin(Object target); } public class TransactionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Exception{ System.out.println("------插入前置通知代碼-------------"); Object result = invocation.process(); System.out.println("------插入后置處理代碼-------------"); return result; } @Override public Object plugin(Object target) { return TargetProxy.wrap(target,this); } }
這樣目標類僅僅需要在執行前,插入需要的攔截器就好了,測試代碼:
public class Test { public static void main(String[] args) { HelloService target = new HelloServiceImpl(); Interceptor transactionInterceptor = new TransactionInterceptor(); //把事務攔截器插入到目標類中 target = (HelloService) transactionInterceptor.plugin(target); target.sayHello(); } }
運行結果
------插入前置通知代碼------------- sayHello...... ------插入后置處理代碼-------------
5、多個攔截器如何處理
到這里就差不多完成了,那我們再來思考如果要添加多個攔截器呢,怎么搞?
public class Test { public static void main(String[] args) { HelloService target = new HelloServiceImpl(); Interceptor transactionInterceptor = new TransactionInterceptor(); target = (HelloService) transactionInterceptor.plugin(target); LogInterceptor logInterceptor = new LogInterceptor(); target = (HelloService)logInterceptor.plugin(target); target.sayHello(); } }
運行結果
------插入前置通知代碼------------- ------插入前置通知代碼------------- sayHello...... ------插入后置處理代碼------------- ------插入后置處理代碼-------------
6、責任鏈設計模式
其實上面已經實現的沒問題了,只是還差那么一點點,添加多個攔截器的時候不太美觀,讓我們再次利用面向對象思想封裝一下。我們設計一個InterceptorChain 攔截器鏈類
public class InterceptorChain { private List<Interceptor> interceptorList = new ArrayList<>(); /** * 插入所有攔截器 */ public Object pluginAll(Object target) { for (Interceptor interceptor : interceptorList) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptorList.add(interceptor); } /** * 返回一個不可修改集合,只能通過addInterceptor方法添加 * 這樣控制權就在自己手里 */ public List<Interceptor> getInterceptorList() { return Collections.unmodifiableList(interceptorList); } }
其實就是通過pluginAll() 方法包一層把所有的攔截器插入到目標類去而已。測試代碼:
public class Test { public static void main(String[] args) { HelloService target = new HelloServiceImpl(); Interceptor transactionInterceptor = new TransactionInterceptor(); LogInterceptor logInterceptor = new LogInterceptor(); InterceptorChain interceptorChain = new InterceptorChain(); interceptorChain.addInterceptor(transactionInterceptor); interceptorChain.addInterceptor(logInterceptor); target = (Target) interceptorChain.pluginAll(target); target.sayHello(); } }
這里展示的是JDK動態代理+責任鏈設計模式,那么Mybatis攔截器就是基於該組合進行開發。
二、Mybatis Plugin 插件概念
1、原理
Mybatis的攔截器實現機制跟上面最后優化后的代碼非常的相似。它也有個代理類Plugin(就是上面的HWInvocationHandler)這個類同樣也會實現了InvocationHandler接口,
當我們調用ParameterHandler,ResultSetHandler,StatementHandler,Executor的對象的時候,,就會執行Plugin的invoke方法,Plugin在invoke方法中根據
@Intercepts的配置信息(方法名,參數等)動態判斷是否需要攔截該方法.再然后使用需要攔截的方法Method封裝成Invocation,並調用Interceptor的proceed方法。
這樣我們就達到了攔截目標方法的結果。例如Executor的執行大概是這樣的流程:
攔截器代理類對象->攔截器->目標方法 Executor.Method->Plugin.invoke->Interceptor.intercept->Invocation.proceed->method.invoke。
2、如何自定義攔截器?
1) Interceptor接口
首先Mybatis官方早就想到我們開發會有這樣的需求,所以開放了一個org.apacheibatis.plugin.Interceptor這樣一個接口。這個接口就是和上面Interceptor性質是一樣的
public interface Interceptor { //當plugin函數返回代理,就可以對其中的方法進行攔截來調用intercept方法 Object intercept(Invocation invocation) throws Throwable; //plugin方法是攔截器用於封裝目標對象的,通過該方法我們可以返回目標對象本身,也可以返回一個它的代理。 Object plugin(Object target); //在Mybatis配置文件中指定一些屬性 void setProperties(Properties properties); }
2)自定義攔截器
這里的ExamplePlugin和上面的LogInterceptor和TransactionInterceptor性質是一樣的
@Intercepts({@Signature( type= Executor.class, method = "update", args ={MappedStatement.class,Object.class})}) public class ExamplePlugin implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { return invocation.proceed(); } public Object plugin(Object target) { return Plugin.wrap(target, this); } public void setProperties(Properties properties) { } }
3)、全局xml配置
最后如果你使用的是Mybatis.xml也就是Mybatis本身單獨的配置,你可以需要在這里配置相應的攔截器名字等。
如果你使用的是spring管理的Mybatis,那么你需要在Spring配置文件里面配置注冊相應的攔截器。
這樣一個自定義mybatis插件流程大致就是這樣了。
3、Mybatis四大接口
竟然Mybatis是對四大接口進行攔截的,那我們要先要知道Mybatis的四大接口對象 Executor, StatementHandle, ResultSetHandler, ParameterHandler。
1.Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) MyBatis的執行器,用於執行增刪改查操作; 2.ParameterHandler (getParameterObject, setParameters) 處理SQL的參數對象; 3.ResultSetHandler (handleResultSets, handleOutputParameters) 處理SQL的返回結果集; 4.StatementHandler (prepare, parameterize, batch, update, query) 攔截Sql語法構建的處理

上圖Mybatis框架的整個執行過程。
三、Mybatis Plugin 插件源碼
經過上面的分析,再去看Mybastis Plugin 源碼的時候就很輕松了。

這幾個也就對應上面的幾個,只不過添加了注解,來判斷是否攔截指定方法。
1、攔截器鏈InterceptorChain
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) { //循環調用每個Interceptor.plugin方法 for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
這個就和我們上面實現的是一樣的。定義了攔截器鏈
2、Configuration
通過初始化配置文件把所有的攔截器添加到攔截器鏈中。
public class Configuration { protected final InterceptorChain interceptorChain = new InterceptorChain(); //創建參數處理器 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { //創建ParameterHandler ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); //插件在這里插入 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } //創建結果集處理器 public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { //創建DefaultResultSetHandler ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); //插件在這里插入 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } //創建語句處理器 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { //創建路由選擇語句處理器 StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); //插件在這里插入 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } public Executor newExecutor(Transaction transaction) { return newExecutor(transaction, defaultExecutorType); } //產生執行器 public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; //這句再做一下保護,囧,防止粗心大意的人將defaultExecutorType設成null? executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; //然后就是簡單的3個分支,產生3種執行器BatchExecutor/ReuseExecutor/SimpleExecutor if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } //如果要求緩存,生成另一種CachingExecutor(默認就是有緩存),裝飾者模式,所以默認都是返回CachingExecutor if (cacheEnabled) { executor = new CachingExecutor(executor); } //此處調用插件,通過插件可以改變Executor行為 executor = (Executor) interceptorChain.pluginAll(executor); return executor; } }
從代碼可以看出Mybatis 在實例化Executor、ParameterHandler、ResultSetHandler、StatementHandler四大接口對象的時候調用interceptorChain.pluginAll() 方法插入
進去的。其實就是循環執行攔截器鏈所有的攔截器的plugin() 方法,mybatis官方推薦的plugin方法是Plugin.wrap() 方法,這個類就是我們上面的TargetProxy類。
3、Plugin
這里的Plugin就是我們上面的自定義代理類TargetProxy類
public class Plugin implements InvocationHandler { public static Object wrap(Object target, Interceptor interceptor) { //從攔截器的注解中獲取攔截的類名和方法信息 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); //取得要改變行為的類(ParameterHandler|ResultSetHandler|StatementHandler|Executor) Class<?> type = target.getClass(); //取得接口 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); //產生代理,是Interceptor注解的接口的實現類才會產生代理 if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //獲取需要攔截的方法 Set<Method> methods = signatureMap.get(method.getDeclaringClass()); //是Interceptor實現類注解的方法才會攔截處理 if (methods != null && methods.contains(method)) { //調用Interceptor.intercept,也即插入了我們自己的邏輯 return interceptor.intercept(new Invocation(target, method, args)); } //最后還是執行原來邏輯 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } //取得簽名Map,就是獲取Interceptor實現類上面的注解,要攔截的是那個類(Executor,ParameterHandler, ResultSetHandler,StatementHandler)的那個方法 private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { //取Intercepts注解,例子可參見ExamplePlugin.java Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251 //必須得有Intercepts注解,沒有報錯 if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } //value是數組型,Signature的數組 Signature[] sigs = interceptsAnnotation.value(); //每個class里有多個Method需要被攔截,所以這么定義 Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } //取得接口 private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { for (Class<?> c : type.getInterfaces()) { //攔截其他的無效 if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]); } }
4、Interceptor接口
public interface Interceptor { //攔截 Object intercept(Invocation invocation) throws Throwable; //插入 Object plugin(Object target); //設置屬性(擴展) void setProperties(Properties properties); }
思路 這么下來思路就很清晰了,我們通過實現Interceptor類實現自定義攔截器,然后把它放入InterceptorChain(攔截器鏈)中,然后通過JDK動態代理來實現依次攔截處理。
Mybatis框架(9)---Mybatis自定義插件生成雪花ID做為表主鍵項目
先附上項目項目GitHub地址 spring-boot-mybatis-interceptor
有關Mybatis雪花ID主鍵插件前面寫了兩篇博客作為該項目落地的鋪墊。
該插件項目可以直接運用於實際開發中,作為分布式數據庫表主鍵ID使用。
一、項目概述
1、項目背景
在生成表主鍵ID時,我們可以考慮主鍵自增 或者 UUID,但它們都有很明顯的缺點
主鍵自增:1、自增ID容易被爬蟲遍歷數據。2、分表分庫會有ID沖突。
UUID: 1、太長,並且有索引碎片,索引多占用空間的問題 2、無序。
雪花算法就很適合在分布式場景下生成唯一ID,它既可以保證唯一又可以排序,該插件項目的原理是
通過攔截器攔截Mybatis的insert語句,通過自定義注解獲取到主鍵,並為該主鍵賦值雪花ID,插入數據庫中。
2、技術架構
項目總體技術選型
SpringBoot2.1.7 + Mybatis + Maven3.5.4 + Mysql + lombok(插件)
3、使用方式
在你需要做為主鍵的屬性上添加@AutoId注解,那么通過插件可以自動為該屬性賦值主鍵ID。
public class TabUser { /** * id(添加自定義注解) */ @AutoId private Long id; /** * 姓名 */ private String name; //其它屬性 包括get,set方法 }
4、項目測試
配置好數據庫連接信息,直接啟動Springboot啟動類Application.java,訪問localhost:8080/save-foreach-user就可以看到數據庫數據已經有雪花ID了。
如圖

二、項目代碼說明
在正式環境中只要涉及到插入數據的操作都被該插件攔截,並發量會很大。所以該插件代碼即要保證線程安全又要保證高可用。所以在代碼設計上做一些說明。
1、線程安全
這里的線程安全主要是考慮產生雪花ID的時候必須是線程安全的,不能出現同一台服務器同一時刻出現了相同的雪花ID,這里是通過
靜態內部類單例模式 + synchronized
來保證線程安全的,具體有關生成雪花ID的代碼這里就不粘貼。
2、高可用
我們去思考消耗性能比較大的地方可能出要出現在兩個地方
1)雪花算法生成雪花ID的過程。 2)通過類的反射機制找到哪些屬性帶有@AutoId注解的過程。
第一點
其實在靜態內部類實現雪花算法這篇博客已經簡單測試過,生成20萬條數據,大約在1.7秒能滿足實際開發中我們的需要。
第二點
這里是有比較好的解決方案的,可以通過兩點去改善它。
1)、在插件中添加了一個Map處理器
/** * key值為Class對象 value可以理解成是該類帶有AutoId注解的屬性,只不過對屬性封裝了一層。 * 它是非常能夠提高性能的處理器 它的作用就是不用每一次一個對象經來都要看下它的哪些屬性帶有AutoId注解 * 畢竟類的反射在性能上並不友好。只要key包含該Class,那么下次同樣的class進來,就不需要檢查它哪些屬性帶AutoId注解。 */ private Map<Class, List<Handler>> handlerMap = new ConcurrentHashMap<>();
插件部分源碼
public class AutoIdInterceptor implements Interceptor { /** * Map處理器 */ private Map<Class, List<Handler>> handlerMap = new ConcurrentHashMap<>(); /** * 某某方法 */ private void process(Object object) throws Throwable { Class handlerKey = object.getClass(); List<Handler> handlerList = handlerMap.get(handlerKey); //先判斷handlerMap是否已存在該class,不存在先找到該class有哪些屬性帶有@AutoId if (handlerList == null) { handlerMap.put(handlerKey, handlerList = new ArrayList<>()); // 通過反射 獲取帶有AutoId注解的所有屬性字段,並放入到handlerMap中 } //為帶有@AutoId賦值ID for (Handler handler : handlerList) { handler.accept(object); } } }
2)添加break label(標簽)
這個就比較細節了,因為上面的process方法不是線程安全的,也就是說可能存在同一時刻有N個線程進入process方法,那么這里可以優化如下:
//添加了SYNC標簽 SYNC: if (handlerList == null) { //此時handlerList確實為null,進入這里 synchronized (this) { handlerList = handlerMap.get(handlerKey); //但到這里發現它已經不是為null了,因為可能被其它線程往map中插入數據,那說明其實不需要在執行下面的邏輯了,直接跳出if體的SYNC標簽位置。 //那么也就不會執行 if (handlerList == null) {}里面的邏輯。 if (handlerList != null) { break SYNC; } } }
這里雖然很細節,但也是有必要的,畢竟這里並發量很大,這樣設計能一定程度提升性能。

