MyBatis 快速入門和重點詳解
1.定義
MyBatis 是一款優秀的持久層框架,它支援定製化 SQL、儲存過程以及高階對映。MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。MyBatis 可以使用簡單的 XML 或註解來配置和對映原生資訊,將介面和 Java 的 POJOs(Plain Old Java Objects,普通的 Java物件)對映成資料庫中的記錄。
官網
2.使用 MyBatis
3.作用域(Scope)和生命週期
類名稱 | SCOPE |
---|---|
SqlSessionFactoryBuilder | method |
SqlSessionFactory | application |
SqlSession | request/method (可以認為是執行緒級) |
Mapper | method |
4.mybatis config檔案
- typeAliases 類型別名是為 Java 型別設定一個短的名字。它只和 XML 配置有關,存在的意義僅在於用來減少類完全限定名的冗餘。例如:
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog" />
<typeAlias alias="Comment" type="domain.blog.Comment"/>
<typeAlias alias="Post" type="domain.blog.Post"/>
<typeAlias alias="Section" type="domain.blog.Section"/>
<typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>
2.typeHandlers
無論是 MyBatis在預處理語句(PreparedStatement)中設定一個引數時,還是從結果集中取出一個值時, 都會用型別處理器將獲取的值以合適的方式轉換成 Java 型別。
你可以重寫型別處理器或建立你自己的型別處理器來處理不支援的或非標準的型別。 具體做法為:實現 org.apache.ibatis.type.TypeHandler 介面, 或繼承一個很便利的類 org.apache.ibatis.type.BaseTypeHandler, 然後可以選擇性地將它對映到一個 JDBC 型別。比如:
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JunliTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, String s, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, s + "LIJUN");
}
@Override
public String getNullableResult(ResultSet resultSet, String s) throws SQLException {
return resultSet.getString(s)+"LIJUN";
}
@Override
public String getNullableResult(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString(i);
}
@Override
public String getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
return callableStatement.getString(i);
}
}
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="com.junli.mybatis.demo.mybatis.JunliTypeHandler"/>
</typeHandlers>
@MappedJdbcTypes(JdbcType.VARCHAR) 使用這個的型別處理器將會覆蓋已經存在的處理 Java 的 String 型別屬性和 VARCHAR 引數及結果的型別處理器。preparedStatement.setString(i, s + “LIJUN”); 表示在所有String型別後面加上 LIJUN 但是有時候我們只是想特定的欄位加上LIJUN。可以如下配置(mybatis-config.xml 就不需要了):
//插入
insert into test (id, nums, name
)
values (#{id,jdbcType=INTEGER}, #{nums,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR,typeHandler=com.junli.mybatis.demo.mybatis.JunliTypeHandler}
)
//返回
<resultMap id="BaseResultMap" type="com.junli.mybatis.beans.Test">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="nums" jdbcType="INTEGER" property="nums" />
<result column="name" jdbcType="VARCHAR" property="name" typeHandler="com.junli.mybatis.demo.mybatis.JunliTypeHandler"/>
</resultMap>
3.外掛(plugins)
MyBatis 允許你在已對映語句執行過程中的某一點進行攔截呼叫。預設情況下,MyBatis 允許使用外掛來攔截的方法呼叫包括:
- Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
下面通過自定義外掛來打印出查詢的sql語句:
@Intercepts({@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class JunliPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
System.out.println(String.format("plugin output sql = %s , param=%s", boundSql.getSql(),boundSql.getParameterObject()));
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o,this);
}
@Override
public void setProperties(Properties properties) {
}
配置外掛:
<plugins>
<plugin interceptor="com.junli.mybatis.demo.mybatis.JunliPlugin"/>
</plugins>
4.對映器(mappers)
既然 MyBatis 的行為已經由上述元素配置完了,我們現在就要定義 SQL 對映語句了。但是首先我們需要告訴 MyBatis 到哪裡去找到這些語句。 Java 在自動查詢這方面沒有提供一個很好的方法,所以最佳的方式是告訴 MyBatis 到哪裡去找對映檔案。你可以使用相對於類路徑的資源引用, 或完全限定資源定位符(包括 file:/// 的 URL),或類名和包名等。例如:
<mappers>
<mapper resource="xml/TestMapper.xml"/>
<mapper resource="xml/PostsMapper.xml"/>
</mappers>
5.Mapper XML 檔案 解讀
MyBatis 的真正強大在於它的對映語句,也是它的魔力所在。由於它的異常強大,對映器的 XML 檔案就顯得相對簡單。如果拿它跟具有相同功能的 JDBC 程式碼進行對比,你會立即發現省掉了將近 95% 的程式碼。MyBatis 就是針對 SQL 構建的,並且比普通的方法做的更好。
SQL對映檔案有很少的幾個頂級元素(按照它們應該被定義的順序):
- cache – 給定名稱空間的快取配置。
- cache-ref – 其他名稱空間快取配置的引用。
- resultMap
- – 是最複雜也是最強大的元素,用來描述如何從資料庫結果集中來載入物件。
- sql – 可被其他語句引用的可重用語句塊。
- insert – 對映插入語句
- update – 對映更新語句
- delete – 對映刪除語句
- select – 對映查詢語句
主要說一下resultMap
resultMap 元素有很多子元素和一個值得討論的結構。
resultMap
- constructor - 用於在例項化類時,注入結果到構造方法中
idArg - ID 引數;標記出作為 ID 的結果可以幫助提高整體效能
arg - 將被注入到構造方法的一個普通結果 - id – 一個 ID 結果;標記出作為 ID 的結果可以幫助提高整體效能
- result – 注入到欄位或 JavaBean 屬性的普通結果
- association – 一個複雜型別的關聯;許多結果將包裝成這種型別
- 巢狀結果對映 – 關聯可以指定為一個 resultMap 元素,或者引用一個
- collection – 一個複雜型別的集合巢狀結果對映 – 集合可以指定為一個 resultMap 元素,或者引用一個
discriminator – 使用結果值來決定使用哪個 resultMap.
resultMap 關聯查詢.
關聯查詢分為兩種:- 關聯的巢狀查詢
例子
<resultMap id="BaseResultMap" type="com.junli.mybatis.beans.Blog">
<result column="bid" jdbcType="INTEGER" property="bid"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="author_id" jdbcType="INTEGER" property="authorId"/>
<association property="author" column="author_id" javaType="com.junli.mybatis.beans.Author" select="selectAuthor" />
</resultMap>
<resultMap id="BaseResultMap_Author" type="com.junli.mybatis.beans.Author">
<result column="aid" jdbcType="INTEGER" property="aid"/>
<result column="author_name" jdbcType="VARCHAR" property="authorName"/>
</resultMap>
<select id="selectAuthor" resultType="com.junli.mybatis.beans.Author" resultMap="BaseResultMap_Author">
SELECT * FROM AUTHOR WHERE aid = #{author_id}
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM blog WHERE bid = #{id} ;
</select>
我們有兩個查詢語句:一個來載入部落格,另外一個來載入作者,而且部落格的結果對映描 述了“BaseResultMap_Author”語句應該被用來載入它的 author 屬性。
其他所有的屬性將會被自動載入,假設它們的列和屬性名相匹配。
這種方式很簡單, 但是對於大型資料集合和列表將不會表現很好。 問題就是我們熟知的 “N+1 查詢問題”。概括地講,N+1 查詢問題可以是這樣引起的:
1. 你執行了一個單獨的 SQL 語句來獲取結果列表(就是“+1”)。
2. 對返回的每條記錄,你執行了一個查詢語句來為每個載入細節(就是“N”)。
這個問題會導致成百上千的 SQL 語句被執行。這通常不是期望的。
MyBatis 能延遲載入這樣的查詢就是一個好處,因此你可以分散這些語句同時執行的消 耗。然而,如果你載入一個列表,之後迅速迭代來訪問巢狀的資料,你會呼叫所有的延遲加 載,這樣的行為可能是很糟糕的。所以還有另外一種方法。
- 關聯的巢狀結果
重新上面的例子
<resultMap id="blogResult" type="com.junli.mybatis.beans.Blog">
<result column="bid" jdbcType="INTEGER" property="bid"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="author_id" jdbcType="INTEGER" property="authorId"/>
<association property="author" column="author_id" javaType="com.junli.mybatis.beans.Author" resultMap="BaseResultMap_Author2"/>
</resultMap>
<resultMap id="BaseResultMap_Author2" type="com.junli.mybatis.beans.Author">
<result column="aid" jdbcType="INTEGER" property="aid"/>
<result column="author_name" jdbcType="VARCHAR" property="authorName"/>
</resultMap>
<select id="selectBlogById" resultMap="blogResult">
SELECT
B.bid,
B.`name`,
B.author_id,
A.aid,
A.author_name
FROM
Blog B
LEFT OUTER JOIN Author A ON B.author_id = A.aid
WHERE
B.bid = #{id}
</select>
集合的巢狀查詢
我們來繼續上面的示例,一個部落格只有一個作者。但是部落格有很多文章。在部落格類中, 這可以由下面這樣的寫法來表示:
private List<Posts> posts;
例項:
<resultMap id="blogResultAndPosts" type="com.junli.mybatis.beans.Blog">
<collection property="posts" javaType="ArrayList" column="bid"
ofType="com.junli.mybatis.beans.Posts" select="selectPostsForBlog"/>
</resultMap>
<resultMap id="PostsForBlogResult" type="com.junli.mybatis.beans.Posts">
<result column="pid" jdbcType="INTEGER" property="pid" />
<result column="post_name" jdbcType="VARCHAR" property="postName" />
<result column="blog_id" jdbcType="INTEGER" property="blogId" />
</resultMap>
<select id="selectBlogAndPosts" resultMap="blogResultAndPosts">
SELECT * FROM BLOG WHERE BID = #{id}
</select>
<select id="selectPostsForBlog" resultMap="PostsForBlogResult">
SELECT * FROM POSTS WHERE BLOG_ID = #{bid}
</select>
集合的巢狀結果實現:
<resultMap id="blogResultAndPostsResultQuery" type="com.junli.mybatis.beans.Blog">
<result column="bid" jdbcType="INTEGER" property="bid"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="author_id" jdbcType="INTEGER" property="authorId"/>
<collection property="posts" javaType="ArrayList" column="bid" ofType="com.junli.mybatis.beans.Posts">
<result column="pid" jdbcType="INTEGER" property="pid" />
<result column="post_name" jdbcType="VARCHAR" property="postName" />
<result column="blog_id" jdbcType="INTEGER" property="blogId" />
</collection>
</resultMap>
<select id="selectBlogAndPostsResultQuery" resultMap="blogResultAndPostsResultQuery">
SELECT
B.bid,
B.`name`,
B.author_id,
p.pid,
p.post_name,
p.blog_id
FROM
Blog B
LEFT JOIN posts p ON p.blog_id = b.bid
WHERE
B.bid = #{id}
</select>
快取:
Mybatis中有一級快取和二級快取,預設情況下一級快取是開啟的,而且是不能關閉的。一級快取是指SqlSession級別的快取,當在同一個SqlSession中進行相同的SQL語句查詢時,第二次以後的查詢不會從資料庫查詢,而是直接從快取中獲取,一級快取最多快取1024條SQL。二級快取是指可以跨SqlSession的快取。
Mybatis中進行SQL查詢是通過org.apache.ibatis.executor.Executor介面進行的,總體來講,它一共有兩類實現,一類是BaseExecutor,一類是CachingExecutor。前者是非啟用二級快取時使用的,而後者是採用的裝飾器模式,在啟用了二級快取時使用,當二級快取沒有命中時,底層還是通過BaseExecutor來實現的。
- 一級快取
一級快取是預設啟用的,在BaseExecutor的query()方法中實現,底層預設使用的是PerpetualCache實現,PerpetualCache採用HashMap儲存資料。一級快取會在進行增、刪、改操作時進行清除。 - 二級快取
二級快取是預設啟用的,如想取消,則可以通過Mybatis配置檔案中的元素下的子元素來指定cacheEnabled為false。
<settings>
<setting name="cacheEnabled" value="false" />
</settings>
我們要想使用二級快取,是需要在對應的Mapper.xml檔案中定義其中的查詢語句需要使用哪個cache來快取資料的。這有兩種方式可以定義,一種是通過cache元素定義,一種是通過cache-ref元素來定義。但是需要注意的是對於同一個Mapper來講,它只能使用一個Cache,當同時使用了和時使用定義的優先順序更高。Mapper使用的Cache是與我們的Mapper對應的namespace繫結的,一個namespace最多隻會有一個Cache與其繫結。
- 自定義cache
前面提到Mybatis的Cache預設會使用PerpetualCache儲存資料,如果我們不想按照它的邏輯實現,或者我們想使用其它快取框架來實現,比如使用Ehcache、Redis等,這個時候我們就可以使用自己的Cache實現,Mybatis是給我們留有對應的介面,允許我們進行自定義的。要想實現自定義的Cache我們必須定義一個自己的類來實現Mybatis提供的Cache介面,實現對應的介面方法。
**
* 自定義快取
*/
public class JunliCache implements Cache {
private ReadWriteLock lock = new ReentrantReadWriteLock();
private ConcurrentHashMap<Object, Object> cache = new ConcurrentHashMap<Object, Object>();
private String id;
public JunliCache() {
System.out.println("初始化-1!");
}
/**
* 必須有該建構函式
*/
public JunliCache(String id) {
System.out.println("初始化-2!");
this.id = id;
}
/**
* 獲取快取編號
*/
@Override
public String getId() {
System.out.println("得到ID:" + id);
return id;
}
/***
* 獲取快取物件的大小
* @return int
*/
@Override
public int getSize() {
System.out.println("獲取快取大小!");
return 0;
}
/**
* 儲存key值快取物件
*
* @param key key
* @param value value
*/
@Override
public void putObject(Object key, Object value) {
System.out.println("往快取中新增元素:key=" + key + ",value=" + value);
cache.put(key, value);
}
/**
* 通過KEY
*
* @param key key
* @return Object
*/
@Override
public Object getObject(Object key) {
System.out.println("通過kEY獲取值:" + key);
System.out.println("OVER");
System.out.println("=======================================================");
System.out.println("值為:" + cache.get(key));
System.out.println("=====================OVER==============================");
return cache.get(key);
}
/**
* 通過key刪除快取物件
*
* @param key key
* @return
*/
@Override
public Object removeObject(Object key) {
System.out.println("移除快取物件:" + key);
return null;
}
/**
* 清空快取
*/
@Override
public void clear() {
System.out.println("清除快取!");
cache.clear();
}
/**
* 獲取快取的讀寫鎖
*
* @return ReadWriteLock
*/
@Override
public ReadWriteLock getReadWriteLock() {
System.out.println("獲取鎖物件!!!");
return lock;
}
}
5.動態 SQL
MyBatis 的強大特性之一便是它的動態 SQL。如果你有使用 JDBC 或其它類似框架的經驗,你就能體會到根據不同條件拼接 SQL 語句的痛苦。例如拼接時要確保不能忘記新增必要的空格,還要注意去掉列表最後一個列名的逗號。利用動態 SQL 這一特性可以徹底擺脫這種痛苦。動態 SQL 元素和 JSTL 或基於類似 XML 的文字處理器相似.MyBatis 採用功能強大的基於 OGNL 的表示式來淘汰其它大部分元素。
- if
<if test="title != null">
AND title like #{title}
</if>
- choose (when, otherwise)
<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>
- trim (where, set)
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
- foreach
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
文章稍微有點長,本文中涉及的原始碼參考我的原始碼mybatis-demo模組。