文章轉(zhuǎn)載自:http://www.itsoku.com/article/256
Mybatis系列目標(biāo):從入門開始開始掌握一個(gè)高級(jí)開發(fā)所需要的Mybatis技能六剥。
這是mybatis系列第11篇,源碼位于文章尾部峰伙!
mybatis內(nèi)部參數(shù)設(shè)置和結(jié)果的處理
先來看一個(gè)案例:
jdbc的方式插入用戶信息仗考,參數(shù)為UserModel:
public class UserModel {
private Integer id;
private String name;
private Integer age;
//省略了get set方法
}
public static int insert(UserModel userModel) throws SQLException {
Connection connection = null;
String sql = "insert into t_user (id,name,age) values (?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1,userModel.getId());
preparedStatement.setString(2,userModel.getName());
preparedStatement.setInt(3,userModel.getAge());
return preparedStatement.executeUpdate();
}
上面我們調(diào)用了preparedStatement
的幾個(gè)方法來設(shè)置參數(shù),如下:
preparedStatement.setInt(1,userModel.getId());
preparedStatement.setString(2,userModel.getName());
preparedStatement.setInt(3,userModel.getAge());
當(dāng)我們使用mybatis插入用戶信息的時(shí)候词爬,mybatis底層也會(huì)調(diào)用PreparedStatement
的這些設(shè)置參數(shù)的方法秃嗜,mybatis底層是如何判斷調(diào)用哪個(gè)方法的呢?
是調(diào)用setInt
方法還是setString
方法的呢顿膨?
再來看一個(gè)jdbc查詢的案例锅锨,查詢出t_user表所有數(shù)據(jù),將其轉(zhuǎn)換為UserModel對象恋沃,核心代碼如下:
public static List<UserModel> getList() throws Exception {
List<UserModel> result = new ArrayList<>();
Connection connection = null;
String sql = "select id,name,age from t_user";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
UserModel userModel = new UserModel();
userModel.setId(rs.getInt(1));
userModel.setName(rs.getString(2));
userModel.setAge(rs.getInt(3));
result.add(userModel);
}
return result;
}
上面有幾行從ResultSet
獲取數(shù)據(jù)的方法必搞,如下:
userModel.setId(rs.getInt(1));
userModel.setName(rs.getString(2));
userModel.setAge(rs.getInt(3));
如果使用mybatis實(shí)現(xiàn),mybatis可以將sql結(jié)果自動(dòng)映射到UserModel中的屬性中囊咏,mybatis內(nèi)部給UserModel的id設(shè)置值的時(shí)候恕洲,mybatis內(nèi)部是如何知道是調(diào)用rs.getInt
還是調(diào)用rs.getString
來獲取id
列的值的呢?
這些就是mybatis中類型轉(zhuǎn)換器做的事情梅割,類型轉(zhuǎn)換器主要有2個(gè)功能:
- 參數(shù)設(shè)置霜第,即設(shè)置參數(shù)的時(shí)候需要調(diào)用PreparedStatement中哪個(gè)set方法去設(shè)置參數(shù),比如:插入用戶信息id字段户辞,id字段是java中的Integer類型的泌类,mybatis內(nèi)部知道需要調(diào)用setInt方法去給id字段設(shè)置參數(shù)。
- 將sql查詢結(jié)果轉(zhuǎn)換為對應(yīng)的java類型底燎,即調(diào)用ResultSet中的哪個(gè)get方法去獲取參數(shù)刃榨,比如:id在數(shù)據(jù)中是int類型的弹砚,讀取的時(shí)候會(huì)調(diào)用ResultSet的getInt方法去讀取,而name字段在db中是varchar類型的枢希,讀取的時(shí)候會(huì)調(diào)用getString方法去讀取桌吃,而不是調(diào)用getInt方法去讀取。
mybatis中定義了一個(gè)類型轉(zhuǎn)換器的接口:
public interface TypeHandler<T> {
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
/**
* @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
*/
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
第一個(gè)方法用于通過PreparedStatement設(shè)置參數(shù)的苞轿,即內(nèi)部會(huì)根據(jù)參數(shù)的類型茅诱,去調(diào)用PreparedStatement
中對應(yīng)的方法去設(shè)置參數(shù)的值,比如是調(diào)用setInt方法呢還是setString方法呕屎,每個(gè)類型轉(zhuǎn)換器中實(shí)現(xiàn)的是不同的让簿。其他3個(gè)方法是從結(jié)果集中讀取數(shù)據(jù)的,內(nèi)部具體是調(diào)用結(jié)果集的getInt方法還是getString方法或者是getObject方法秀睛,每個(gè)轉(zhuǎn)換器內(nèi)部實(shí)現(xiàn)也是不一樣的尔当。
mybatis內(nèi)部默認(rèn)實(shí)現(xiàn)了很多類型轉(zhuǎn)換器,基本上夠我們用了蹂安,比如IntegerTypeHandler轉(zhuǎn)換器椭迎,IntegerTypeHandler主要用來處理java中的Integer、int類型參數(shù)的田盈,所以會(huì)調(diào)用setInt方法設(shè)置參數(shù)畜号;讀取的時(shí)候,對應(yīng)的jdbc中的類型是JdbcType.INTEGER允瞧,所以會(huì)調(diào)用getInt方法讀取數(shù)據(jù)庫返回的值简软,讀取的結(jié)果是Integer類型的。我們看一下其源碼:
public class IntegerTypeHandler extends BaseTypeHandler<Integer> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter);
}
@Override
public Integer getNullableResult(ResultSet rs, String columnName)
throws SQLException {
int result = rs.getInt(columnName);
return result == 0 && rs.wasNull() ? null : result;
}
@Override
public Integer getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
int result = rs.getInt(columnIndex);
return result == 0 && rs.wasNull() ? null : result;
}
@Override
public Integer getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
int result = cs.getInt(columnIndex);
return result == 0 && cs.wasNull() ? null : result;
}
}
mybatis內(nèi)部默認(rèn)實(shí)現(xiàn)了很多類型轉(zhuǎn)換器述暂,每種類型轉(zhuǎn)換器能夠處理哪些java類型以及能夠處理的JdbcType的類型痹升,這些都在TypeHandlerRegistry
進(jìn)行注冊的,大家可以去看一下這個(gè)類源碼畦韭,IntegerTypeHandler也是在這個(gè)類中進(jìn)行注冊的疼蛾,代碼如下:
register(Integer.class, new IntegerTypeHandler());
register(int.class, new IntegerTypeHandler());
register(JdbcType.INTEGER, new IntegerTypeHandler());
所以當(dāng)我們參數(shù)是Integer或者int類型的時(shí)候,mybatis會(huì)調(diào)用IntegerTypeHandler轉(zhuǎn)換器中的setInt方法去設(shè)置參數(shù)艺配,上面我們的UserModel的id字段是Integer類型的察郁,所以插入數(shù)據(jù)的時(shí)候會(huì)調(diào)用IntegerTypeHandler處理器中的setInt方法去設(shè)置參數(shù),當(dāng)mybatis將t_user表的數(shù)據(jù)自動(dòng)映射為UserModel類型的時(shí)候转唉,mybatis會(huì)發(fā)現(xiàn)id類型是Integer類型皮钠,然后會(huì)找到IntegerTypeHandler中對應(yīng)的讀取結(jié)果集的方法去讀取數(shù)據(jù),即調(diào)用ResultSet的getInt方法讀取id字段的數(shù)據(jù)酝掩,然后賦值給UserModel中的id屬性鳞芙。
自定義類型轉(zhuǎn)換器
我們在用戶表新增一個(gè)性別字段sex,java中我們通過一個(gè)枚舉來表示這個(gè)類型期虾,db腳本如下:
DROP DATABASE IF EXISTS `javacode2018`;
CREATE DATABASE `javacode2018`;
USE `javacode2018`;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user(
id int AUTO_INCREMENT PRIMARY KEY COMMENT '用戶id',
name VARCHAR(32) NOT NULL DEFAULT '' COMMENT '用戶名',
age SMALLINT NOT NULL DEFAULT 1 COMMENT '年齡',
sex SMALLINT DEFAULT 0 COMMENT '性別原朝,0:未知,1:男镶苞,2:女'
) COMMENT '用戶表';
INSERT INTO t_user VALUES (1,'路人甲Java',30,1),(2,'林志玲',45,2);
t_user對應(yīng)的Model如下:
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserModel implements Serializable {
private Integer id;
private String name;
private Integer age;
private SexEnum sex;
}
sex是一個(gè)枚舉類型喳坠,枚舉定義如下:
/**
* 性別
*/
public enum SexEnum {
UNKNOW(0, "未知"),
MAN(1, "男"),
WOMAN(2, "女");
private Integer value;
private String name;
SexEnum(Integer value, String name) {
this.value = value;
this.name = name;
}
public Integer getValue() {
return value;
}
public String getName() {
return name;
}
public static SexEnum getByValue(Integer value) {
for (SexEnum item : values()) {
if (item.getValue().equals(value)) {
return item;
}
}
return null;
}
}
我們來寫一個(gè)查詢?nèi)缦拢?/p>
<select id="getList1" resultType="com.javacode2018.chat05.demo10.model.UserModel">
SELECT id,name,age,sex FROM t_user
</select>
這個(gè)大家都很熟悉了,查詢t_user表茂蚓,每行結(jié)果轉(zhuǎn)換為一個(gè)UserModel對象壕鹉,最后返回一個(gè)List集合,這個(gè)查詢運(yùn)行會(huì)不會(huì)有什么問題聋涨?
確實(shí)會(huì)報(bào)錯(cuò)晾浴,如下:
## Cause: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'sex' from result set. Cause: java.lang.IllegalArgumentException: No enum constant com.javacode2018.chat05.demo10.enums.SexEnum.1
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:149)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:147)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:80)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:93)
sex字段在t_user中是數(shù)字類型的,最后通過mybatis查詢牍白,需要賦值給UserModel中的sex字段脊凰,這個(gè)字段是一個(gè)枚舉類型的,mybatis不知道這兩者之間如何進(jìn)行轉(zhuǎn)換茂腥,所以報(bào)錯(cuò)了狸涌。
此時(shí)需要我們指定一個(gè)轉(zhuǎn)換規(guī)則,來告知mybatis如何進(jìn)行轉(zhuǎn)換最岗,需要我們用到自定義類型轉(zhuǎn)換器了帕胆。
自定義一個(gè)類型轉(zhuǎn)換器SexEnumTypeHandle,用來處理sex字段和SexEnum枚舉之間的相互轉(zhuǎn)換般渡,代碼如下:
package com.javacode2018.chat05.demo10.typehandle;
import com.javacode2018.chat05.demo10.enums.SexEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@Slf4j
public class SexEnumTypeHandle extends BaseTypeHandler<SexEnum> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, SexEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getValue());
log.info("{}", parameter);
}
@Override
public SexEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
log.info("{}", columnName);
Object object = rs.getObject(columnName);
Integer sex = object != null && object instanceof Integer ? (Integer) object : null;
return SexEnum.getByValue(sex);
}
@Override
public SexEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
log.info("{}", columnIndex);
Object object = rs.getObject(columnIndex);
Integer sex = object != null && object instanceof Integer ? (Integer) object : null;
return SexEnum.getByValue(sex);
}
@Override
public SexEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
log.info("{}", columnIndex);
Object object = cs.getObject(columnIndex);
Integer sex = object != null && object instanceof Integer ? (Integer) object : null;
return SexEnum.getByValue(sex);
}
}
自定義類型轉(zhuǎn)換器可以直接繼承BaseTypeHandler懒豹,后面有個(gè)泛型,泛型類型為需要處理的java類型驯用,即SexEnum脸秽。
類型轉(zhuǎn)換器定義好了,需要將其注冊到mybatis中晨汹,需要在mybatis配置文件中加入:
<typeHandlers>
<typeHandler handler="com.javacode2018.chat05.demo10.typehandle.SexEnumTypeHandle"/>
</typeHandlers>
再次運(yùn)行一下上面的查詢裙戏,對應(yīng)測試用例:
com.javacode2018.chat05.demo10.Demo10Test#getModelList
@Test
public void getModelList() throws IOException {
try (SqlSession sqlSession = this.sqlSessionFactory.openSession(true);) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> paramMap = new HashMap<>();
List<UserModel> userModelList = mapper.getList1(paramMap);
log.info("{}", userModelList);
}
}
運(yùn)行結(jié)果:
26:26.144 [main] DEBUG c.j.c.d.mapper.UserMapper.getList1 - ==> Preparing: SELECT id,name,age,sex FROM t_user
26:26.167 [main] DEBUG c.j.c.d.mapper.UserMapper.getList1 - ==> Parameters:
26:26.187 [main] INFO c.j.c.d.typehandle.SexEnumTypeHandle - sex
26:26.189 [main] INFO c.j.c.d.typehandle.SexEnumTypeHandle - sex
26:26.189 [main] DEBUG c.j.c.d.mapper.UserMapper.getList1 - <== Total: 2
26:26.190 [main] INFO c.j.chat05.demo10.Demo10Test - [UserModel(id=1, name=路人甲Java, age=30, sex=MAN), UserModel(id=2, name=林志玲, age=45, sex=WOMAN)]
這下正常了奶赔,看一下輸出中的sex屬性,對應(yīng)的為枚舉的名稱。
在此案例代碼的基礎(chǔ)上狈惫,我們再來一個(gè)案例,向用戶表插入用戶信息皂甘,參數(shù)為一個(gè)map恋追,每個(gè)字段的值丟在map中,key為字段的名稱曙聂,sex我們傳遞SexEnum枚舉類型晦炊。
對應(yīng)的java測試用例代碼如下:
com.javacode2018.chat05.demo10.Demo10Test#insert1
@Test
public void insert1() throws IOException {
try (SqlSession sqlSession = this.sqlSessionFactory.openSession(true);) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> map = new HashMap<>();
Integer id = Integer.valueOf(Calendar.getInstance().getTime().getTime() / 1000 + "");
map.put("id", id);
map.put("name", id.toString());
map.put("age", 30);
map.put("sex", SexEnum.WOMAN);
int result = mapper.insert1(map);
log.info("{}", result);
}
}
對應(yīng)mapper xml我們按照下面這種寫法,如:
<insert id="insert1" parameterType="map">
insert into t_user (id,name,age,sex)
VALUE (#{id},#{name},#{age},#{sex})
</insert>
這么寫會(huì)不會(huì)有問題?我們來運(yùn)行一下:
32:36.343 [main] DEBUG c.j.c.d.mapper.UserMapper.insert1 - ==> Preparing: insert into t_user (id,name,age,sex) VALUE (?,?,?,?)
32:36.378 [main] INFO c.j.c.d.typehandle.SexEnumTypeHandle - WOMAN
32:36.381 [main] DEBUG c.j.c.d.mapper.UserMapper.insert1 - ==> Parameters: 1578889956(Integer), 1578889956(String), 30(Integer), 2(Integer)
32:36.387 [main] DEBUG c.j.c.d.mapper.UserMapper.insert1 - <== Updates: 1
32:36.387 [main] INFO c.j.chat05.demo10.Demo10Test - 1
插入成功了断国,map中sex對應(yīng)的是SexEnum.WOMAN
贤姆,mybatis中判斷map中sex對應(yīng)的是SexEnum類型的,所以會(huì)找到其對應(yīng)的類型轉(zhuǎn)換器SexEnumTypeHandle
稳衬,注意上面日志中的第二行霞捡,這個(gè)就是SexEnumTypeHandle
轉(zhuǎn)換器中輸出的日志,說明設(shè)置sex
參數(shù)的時(shí)候執(zhí)行了SexEnumTypeHandle
中的代碼薄疚。
但是當(dāng)我們不傳遞sex的值的時(shí)候碧信,或者說sex傳遞為null的時(shí)候,此時(shí)mybatis是無法判斷map中sex對應(yīng)的具體類型的街夭,mybatis內(nèi)部將無法判斷sex參數(shù)的設(shè)置的時(shí)候砰碴,無法找到具體用哪個(gè)類型轉(zhuǎn)換器給sex設(shè)置參數(shù),這個(gè)在mysql中不會(huì)報(bào)錯(cuò)板丽,但是在oracle中會(huì)報(bào)錯(cuò)呈枉,大家機(jī)器上有oracle的,可以去試試下面代碼運(yùn)行結(jié)果:
@Test
public void insert1() throws IOException {
try (SqlSession sqlSession = this.sqlSessionFactory.openSession(true);) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> map = new HashMap<>();
Integer id = Integer.valueOf(Calendar.getInstance().getTime().getTime() / 1000 + "");
map.put("id", id);
map.put("name", id.toString());
map.put("age", 30);
map.put("sex", null);
int result = mapper.insert1(map);
log.info("{}", result);
}
}
上面代碼oracle中執(zhí)行會(huì)報(bào)錯(cuò)檐什,需要修改對應(yīng)的mapper xml中的代碼碴卧,需要讓mybatis明確知道sex字段需要用哪個(gè)類型轉(zhuǎn)換器進(jìn)行處理,有2種寫法乃正。
第一種住册,通過typeHandler屬性明確指定參數(shù)的類型轉(zhuǎn)換器,如下
<insert id="insert1" parameterType="map">
insert into t_user (id,name,age,sex)
VALUE (#{id},#{name},#{age},#{sex,typeHandler=com.javacode2018.chat05.demo10.typehandle.SexEnumTypeHandle})
</insert>
第二種瓮具,可以指定sex字段的類型荧飞,需要通過javaType屬性來指定參數(shù)的具體類型,如下:
<insert id="insert1" parameterType="map">
insert into t_user (id,name,age,sex)
VALUE (#{id},#{name},#{age},#{sex,javaType=com.javacode2018.chat05.demo10.enums.SexEnum})
</insert>
mybatis通過java的類型就可以找到對應(yīng)的類型轉(zhuǎn)換器名党,所以方式2也是正常的叹阔。
再來總結(jié)一下自定義類型轉(zhuǎn)換器的使用步驟:
- 自定義類型轉(zhuǎn)換器,繼承BaseTypeHandler
- 將自定義類型轉(zhuǎn)換器注冊到mybatis中传睹,需要在mybatis配置文件中通過typeHandler元素進(jìn)行引入
- mapper xml中就可以使用了耳幢。
案例代碼
鏈接:https://pan.baidu.com/s/1vt-MAX3oJOu9gyxZAhKkbg
提取碼:i8op