MyBatis 類型處理器
1. 前言
MyBatis 提供了諸多類型處理器,但是相較于豐富的數據庫類型仍然略顯不足,比如 MyBatis 只能將 JSON 數據類型當成普通的字符串處理。因此 MyBatis 提供了類型處理器接口,讓開發者可以根據具體的業務需求來自定義適合的類型處理器。
本小節,我們將以 JSON 類型處理器作為落腳點,來介紹類型處理器,并自定義 JSON 類型處理器。
2. JSON 數據類型
首先,我們需要為 MyBatis 內置類型處理器增加一個它無法處理的數據類型,這里我們選擇 MySQL5.7 中新增的 JSON 數據類型,這也是大家普遍使用的一個數據類型。在可用的數據庫環境中,我們運行如下腳本:
DROP TABLE IF EXISTS blog;
CREATE TABLE blog
(
id int(11) unsigned primary key auto_increment,
info json,
tags json
);
INSERT INTO blog(info, tags)
VALUES ('{"title": "世界更大", "content": "世界更大的內容", "rank": 1}', '["世界觀"]'),
('{"title": "人生更短", "content": "人生更短的內容", "rank": 2}', '["人文"]');
在這個腳本中,我們新建了一個 blog 數據表,blog 數據表除 id 外有 info 和 tags 兩個字段,這兩個字段都是 JSON 類型,并通過 insert 語句添加了兩條記錄。
3. 類型處理器
MyBatis 默認是無法很好處理 info 和 tags 這兩個字段的,只能將它們當成字符串類型來處理,但顯然這不是我們想要的效果。我們希望新增 json 類型處理器來處理好這兩個字段。
MyBatis 提供了 TypeHandler 接口,自定義類型處理器需要實現了這個接口才能工作。考慮到很多開發者不夠熟練,MyBatis 還提供了一個 BaseTypeHandler 抽象類來幫助我們做自定義類型處理器,只需繼承這個基類,然后實現它的方法即可。
3.1 JsonObject 處理器
JSON 可分為 object 和 array 兩大類,分別對應 info 和 tags 字段,這兩類需要分別實現類型處理器。由于需要對 JSON 進行處理,我們在 pom.xml 文件中添加上對應的依賴。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
這里,我們使用阿里巴巴開源的 fastjson庫。
在 com.imooc.mybatis 包下新建 handler 包,并向 handler 包中添加上 json object 的類型處理器 JsonObjectTypeHandler。如下:
package com.imooc.mybatis.handler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.ibatis.type.*;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@MappedJdbcTypes(JdbcType.VARCHAR) // 對應jdbc 類型
@MappedTypes({JSONObject.class}) // 對應處理后類型
public class JsonObjectTypeHandler extends BaseTypeHandler<JSONObject> {
// 當為 PreparedStatement 參數時,如何處理對象
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, JSONObject o, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, JSON.toJSONString(o));
}
// 當通過名稱從結果中取json字段時如何處理
@Override
public JSONObject getNullableResult(ResultSet resultSet, String s) throws SQLException {
String t = resultSet.getString(s);
return JSON.parseObject(t);
}
// 當通過序列號從結果中取json字段時如何處理
@Override
public JSONObject getNullableResult(ResultSet resultSet, int i) throws SQLException {
String t = resultSet.getString(i);
return JSON.parseObject(t);
}
// 當通過序列號從 CallableStatement 中取json字段時如何處理
@Override
public JSONObject getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
String t = callableStatement.getString(i);
return JSON.parseObject(t);
}
}
有了 BaseTypeHandler 作為基礎后,實現一個類型處理器就比較簡單了,我們只需要為其中 4 個方法添加上對應的實現即可。
類型處理器有兩個作用,第一處理 Java 對象到 JdbcType 類型的轉換,對應 setNonNullParameter 方法;第二處理 JdbcType 類型到 Java 類型的轉換,對應 getNullableResult 方法,getNullableResult 有 3 個重載方法。下面我們依次來說明這四個方法的作用:
- setNonNullParameter:處理 PreparedStatement 中的 JSONObject 參數,當調用 PreparedStatement 執行 SQL 語句時,調用該處理 JSONObject 類型的參數,這里我們通過 fastjson 的
JSON.toJSONString(o)
函數將 JSONObject 轉化為字符串類型即可。 - getNullableResult:從結果集中獲取字段,這里 CallableStatement 和 ResultSet 分別對應不同的執行方式,對于 JDBC 而言 JSON 類型也會當做字符串來處理,因此這里我們需要將字符串類型轉化為 JSONObject 類型,對應
JSON.parseObject(t)
代碼。
3.2 JsonArray 處理器
與 JsonObjectTypeHandler 一樣,在 handler 包下新建 JsonArrayTypeHandler 類,繼承 BaseTypeHandler 類,并將具體方法的實現從 JSON.parseObject 改變為 JSON.parseArray,如下:
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({JSONArray.class})
public class JsonArrayTypeHandler extends BaseTypeHandler<JSONArray> {
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, JSONArray o, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, JSON.toJSONString(o));
}
@Override
public JSONArray getNullableResult(ResultSet resultSet, String s) throws SQLException {
String t = resultSet.getString(s);
// // 變成了 parseArray
return JSON.parseArray(t);
}
@Override
public JSONArray getNullableResult(ResultSet resultSet, int i) throws SQLException {
String t = resultSet.getString(i);
// // 變成了 parseArray
return JSON.parseArray(t);
}
@Override
public JSONArray getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
String t = callableStatement.getString(i);
// 變成了 parseArray
return JSON.parseArray(t);
}
}
4. 注冊類型處理器
自定義類型處理器無法直接被 MyBatis 加載,我們需要增加相關的配置告訴 MyBatis 加載類型處理器。
4.1 全局注冊
在全局配置配置文件中可通過 typeHandlers 屬性來注冊類型處理器。如下:
<typeHandlers>
<package name="com.imooc.mybatis.handler"/>
</typeHandlers>
通過 package 項來指定類型處理器所在的包路徑,這樣 handler 包中的所有類型處理器都會注冊到全局。
當然如果你的類型處理器分散在其它地方,也可以通過如下方式來注冊。
<typeHandlers>
<typeHandler handler="com.imooc.mybatis.handler.JsonArrayTypeHandler"/>
</typeHandlers>
全局注冊的類型處理器會自動被 MyBatis 用來處理所有符合類型的參數。如 JsonArrayTypeHandler 通過 MappedJdbcTypes 注解表明了自己將會處理 JdbcType.VARCHAR 類型,MyBatis 會自動將字符串類型的參數交給 JsonArrayTypeHandler 來進行處理。
但是,這樣顯然有問題,因為 JsonObjectTypeHandler 注冊的類型也是 JdbcType.VARCHAR 類型,所以全局注冊是不推薦的,除非你需要對所有參數都做類型轉換。
4.2 局部注冊
由于全局注冊會對其它類型產生歧義和污染,因此我們選擇更加精準的局部注冊。在 BlogMapper 中,我們來注冊和使用類型處理器。
在 BlogMapper.xml 文件中,我們添加上如下配置。
<resultMap id="blogMap" type="com.imooc.mybatis.model.Blog">
<result column="id" property="id"/>
<result column="info" property="info" typeHandler="com.imooc.mybatis.handler.JsonObjectTypeHandler"/>
<result column="tags" property="tags" typeHandler="com.imooc.mybatis.handler.JsonArrayTypeHandler"/>
</resultMap>
<select id="selectById" resultMap="blogMap">
SELECT * FROM blog WHERE id = #{id}
</select>
我們定義了 名為 blogMap 的 resultMap 和名為 selectById 的查詢。在 result 映射中,我們注冊了相關的類型處理器,info 字段對應
JsonObjectTypeHandler 類型處理器,tags 字段對應 JsonArrayTypeHandler 類型處理器。
這樣自定義的類型處理器不會污染到其它數據,blogMap 的類型 com.imooc.mybatis.model.Blog 定義如下:
package com.imooc.mybatis.model;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
public class Blog {
private Long id;
private JSONObject info;
private JSONArray tags;
// 省略了 getter 和 setter 方法
}
4.3 處理 JDBC 類型
在對應的 BlogMapper.java 接口上添加上對應的 selectById 方法:
package com.imooc.mybatis.mapper;
import com.imooc.mybatis.model.Blog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BlogMapper {
Blog selectById(Integer id);
}
我們測試一下 selectById 方法:
BlogMapper blogMapper = session.getMapper(BlogMapper.class);
Blog blog = blogMapper.selectById(1);
System.out.println(blog.toString());
String title = blog.getInfo().getString("title");
System.out.println(title);
String tag = blog.getTags().getString(0);
System.out.println(tag);
輸出結果如下:
Blog{id=1, info={"rank":1,"title":"世界更大","content":".......****............"}, tags=["世界觀"]}
世界更大
世界觀
從結果中可以看出,類型處理器成功的處理了查詢的數據,info 和 tags 字段都能夠通過 fastjson 的 API 來獲取里面的內容。
4.4 處理 JSON 類型
在查詢可以工作的情況下,那么如何通過 insert 插入 JSON 對象了。
我們在 BlogMapper 中新增一個 insertBlog 方法,如下:
<insert id="insertBlog">
INSERT INTO blog(info,tags)
VALUES(#{info,typeHandler=com.imooc.mybatis.handler.JsonObjectTypeHandler},
#{tags,typeHandler=com.imooc.mybatis.handler.JsonArrayTypeHandler})
</insert>
public interface BlogMapper {
int insertBlog(@Param("info") JSONObject info, @Param("tags") JSONArray tags);
}
這樣 MyBatis 就可以處理 JSON 類型的參數了,我們再次測試一下:
JSONObject info = new JSONObject().fluentPut("title", "測試案例").fluentPut("rank", 1);
JSONArray tags = new JSONArray().fluentAdd("測試");
int rows = blogMapper.insertBlog(info, tags);
System.out.println(rows);
輸出結果:
1
可以看到類型處理器成為了 Java JSON 類型和 JDBC 類型轉換橋梁,在查詢的時候主動將數據庫類型轉化為了可用的 JSON 類型,而在插入的時候將 JSON 類型又轉化為了數據庫可識別的字符串類型。
5. 小結
- 自定義類型處理器并不難,MyBatis 已經幫我們做好了大多數工作,我們只需在適當的位置適當的配置就可以了。
- 數據庫 JSON 類型的用處會越來越廣泛,在 MyBatis 官方未內置處理器之前,我們也可以通過本小節的方式來提早的使用它。