mybatis偵探實錄:typehandler薛定諤之謎

一、案件背景

前天,一個涉案人員(同事)提到,在mysql的數據庫中,dba推薦的作法是全部的varchar字段都設置成不能爲空,而且默認值爲empty string,這樣對查詢性能有必定的幫助,設置的sql片斷是這樣的:java

`field_name` varchar(255) NOT NULL DEFAULT ''

問我在mybatis裏面這種狀況怎麼設置。我僞裝思考,而後飛快的打開谷歌,搜索答案,獲得了一個詞,typehandler。typehandler是mybatis用來針對java類型和數據庫類型對不上時作處理工做的類,當前的狀況就是若是我輸入的類型是null,那麼在數據庫要自動轉換成空字符串,不能直接把null塞到數據庫字段裏面。typehandler的作法是寫一個類來實現TypeHandler接口,因而我就寫一個簡單的:mysql

@MappedTypes(value = String.class)
public class NullToEmptyStringTypeHandler implements TypeHandler<String> {

	@Override
	public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
		System.out.println("into NullToEmptyStringTypeHandler");
		if(parameter == null && jdbcType == JdbcType.VARCHAR){//判斷傳入的參數值是否爲null
              ps.setString(i,"");//設置當前參數的值爲空字符串
          }else{
              ps.setString(i,parameter);//若是不爲null,則直接設置參數的值爲value
          }
		
	}

	@Override
	public String getResult(ResultSet rs, String columnName) throws SQLException {
		return rs.getString(columnName);
	}

	@Override
	public String getResult(ResultSet rs, int columnIndex) throws SQLException {
		return rs.getString(columnIndex);
	}

	@Override
	public String getResult(CallableStatement cs, int columnIndex) throws SQLException {
		return cs.getString(columnIndex);
	}

 
}

重點在於註解@MappedTypes(value = String.class)和setParameter方法,個人理解就是若是我傳進來的是String類型的字段,在setParameter的參數JdbcType 裏面判斷出來是VARCHAR的話,那就直接填一個空字符進去,完事大吉。git

這個類還須要配置一下,讓mybatis到哪裏去找到它,我用的是springboot,很簡單的配置,在application.properties裏面加這一句就行了:spring

mybatis.type-handlers-package=com.wphmoon.lesson.common.typehandler

com.wphmoon.lesson.common.typehandler就是NullToEmptyStringTypeHandler 所在的包名,這個包名下的TypeHandler都會被觸發執行。我覺得事情就這麼簡單,但實際上就出問題了。sql

二、案發現場

爲了驗證NullToEmptyStringTypeHandler是否可用,我寫了一個簡單的表來驗證,表結構以下數據庫

CREATE TABLE `my_user` (
`id`  bigint(20) NOT NULL AUTO_INCREMENT ,
`name`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '姓名' ,
`nickname`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '暱稱' ,
`age`  int(11) NULL DEFAULT NULL COMMENT '年齡' ,
`birthday`  datetime NULL DEFAULT NULL COMMENT '生日' ,
`memo`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '備註' ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB;

我又弄了個配套的數據對象和mapper類:springboot

public class MyUser implements Serializable{
	public long id;
	public String name;
	public String nickname;
	public int age;
	public Date birthday;
	public String memo;
    //get,set.......
}
@Mapper
public interface MyUserMapper {

    @Select("SELECT * FROM MY_USER WHERE NAME = #{name}")
    MyUser findByName(@Param("name") String name);
    
    @Select("SELECT * FROM MY_USER WHERE ID = #{id}")
    MyUser findById(@Param("id") Long id);

    @Insert("INSERT INTO MY_USER(NAME, NICKNAME,AGE,BIRTHDAY,MEMO) VALUES(#{name},#{nickname},#{age},#{birthday},#{memo})")
    @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
    void insert(MyUser myUser);

}

最後,我搞了一個controller來執行:mybatis

@RestController
@RequestMapping("/my")
public class MyController {
@Autowired
	private MyUserMapper myUserMapper;
	@RequestMapping(path="/insert2MyUser")
	public String insert2Myuser(MyUser myUser) {
		myUserMapper.insert(myUser);
		return "";
	}
}

執行http://localhost:8080/my/insert2MyUser?age=1後的結果有喜有憂,獲得的console輸出是這樣的:app

into NullToEmptyStringTypeHandler,jdbcType=OTHER
into NullToEmptyStringTypeHandler,jdbcType=OTHER
into NullToEmptyStringTypeHandler,jdbcType=OTHER

這是什麼鬼,jdbcType徹底不是我覺得的VARCHAR類型。不過好歹NullToEmptyStringTypeHandler 被觸發執行了,若是我不須要檢驗jdbcType的話,這個功能算是實現了,我把全部的null值直接替換成空字符串就好了。less

但我好死不死,想看下若是我是用xml來配置mybatis的sql狀況會不會有所不一樣,我搞過了一個表,用xml的方式來實現,表的結構以下:

CREATE TABLE `my_task` (
`id`  bigint(20) NOT NULL AUTO_INCREMENT ,
`title`  varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' ,
`description`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' ,
`user_id`  bigint(20) NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB;

mapper文件和數據對象文件以下:

@Mapper
public interface MyTaskMapper {
    long countByExample(MyTaskExample example);
    int deleteByExample(MyTaskExample example);
    int deleteByPrimaryKey(Long id);
    int insert(MyTask record);
    int insertSelective(MyTask record);
    MyTask selectOneByExample(MyTaskExample example);
    List<MyTask> selectByExample(MyTaskExample example);
    MyTask selectByPrimaryKey(Long id);
    int updateByExampleSelective(@Param("record") MyTask record, @Param("example") MyTaskExample example);
    int updateByExample(@Param("record") MyTask record, @Param("example") MyTaskExample example);
    int updateByPrimaryKeySelective(MyTask record);
    int updateByPrimaryKey(MyTask record);
}
public class MyTask implements Serializable{
    private Long id;
    private String title;
    private String description;
    private Long userId;
    //get,set.......

還有mapper的xml文件,這個太長了,我就只列insert語句的部分

<insert id="insert" parameterType="com.wphmoon.lesson.domain.MyTask">

    <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
      SELECT LAST_INSERT_ID()
    </selectKey>
    insert into my_task (title, description, user_id
      )
    values (#{title,jdbcType=VARCHAR}, #{description,jdbcType=VARCHAR}, #{userId,jdbcType=BIGINT}
      )
  </insert>

我一樣在controller中寫了一段新增記錄的代碼:

@RequestMapping(path="/insert2MyTask")
	public String insert2MyTask(MyTask myTask) {
		return String.valueOf(myTaskMapper.insert(myTask));
	}

執行http://localhost:8080/my/insert2MyTask?title=test2&userId=2後滿心歡喜的等待NullToEmptyStringTypeHandler的觸發,結果慘案發生了,NullToEmptyStringTypeHandler並無被觸發,毫無動靜。難道是xml的配置方式和註解的方式有啥不一樣?或者有什麼地方出錯了,是性格的扭曲仍是人性的喪失啥緣由呢,讓咱們再縷一遍案情:

1)NullToEmptyStringTypeHandler在被MyUserMapper(註解方式)執行的時候被觸發了,可是參數jdbcType爲OTHER類型,而不是咱們覺得的VARCHAR類型

2)NullToEmptyStringTypeHandler在MyTaskMapper(xml方式)執行的時候沒有被觸發。

這是爲啥呢,讓咱們開始破案。

三、案情追查

我一開始是被兩種mapper不一樣的實現方式所迷惑,一種用註解@Insert,一種用xml配置insert,難道他們的實現方法有很大不一樣,我經過兩種方法來追查,一種是DEBUG,我設置斷點,從myUserMapper.insert()到MapperMethod.execute(),SqlSessionTemplate.invoke(),而後就走到NullToEmptyStringTypeHandler裏面去了,而myTaskMapper則徹底忽略了NullToEmptyStringTypeHandler,看來debug走不通。

我又啓動了B計劃,把日誌開到TRACE級別,對比二者的日誌,一行行作對比,但很是的絕望,二者並沒有不一樣。你們欣賞下這個日誌:

這是執行MyUserMapper.insert

2019-05-27 12:20:22.390 DEBUG 13836 --- [nio-8080-exec-3] c.w.lesson.mapper.MyUserMapper.insert    : ==>  Preparing: INSERT INTO MY_USER(NAME, NICKNAME,AGE,BIRTHDAY,MEMO) VALUES(?,?,?,?,?) 
into NullToEmptyStringTypeHandler,jdbcType=OTHER
into NullToEmptyStringTypeHandler,jdbcType=OTHER
into NullToEmptyStringTypeHandler,jdbcType=OTHER
2019-05-27 12:20:22.392 DEBUG 13836 --- [nio-8080-exec-3] c.w.lesson.mapper.MyUserMapper.insert    : ==> Parameters: null, null, 1(Integer), null, null

 這是執行MyTaskMapper.insert的日誌,完美的略過了NullToEmptyStringTypeHandler,徹底沒有觸發

2019-05-27 12:23:08.226 DEBUG 1628 --- [nio-8080-exec-3] c.w.lesson.mapper.MyTaskMapper.insert    : ==>  Preparing: insert into my_task (title, description, user_id ) values (?, ?, ? ) 
2019-05-27 12:23:08.227 DEBUG 1628 --- [nio-8080-exec-3] c.w.lesson.mapper.MyTaskMapper.insert    : ==> Parameters: test2(String), null, 2(Long)

此路不通後,我開始轉換了一個探案思惟,考慮到xml配置的mapper也仍是須要用到typeHandler,那麼它須要的時候是怎麼辦的呢,我再次動用了偵探大腦(google),發現了在xml裏面配置以下:

<insert id="insert" parameterType="com.wphmoon.lesson.domain.MyTask">
    <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
      SELECT LAST_INSERT_ID()
    </selectKey>
    insert into my_task (title, description, user_id
      )
    values (#{title,jdbcType=VARCHAR,typeHandler=com.wphmoon.lesson.common.typehandler.NullToEmptyStringTypeHandler}, #{description,jdbcType=VARCHAR}, #{userId,jdbcType=BIGINT}
      )
  </insert>

能夠直接在字段裏面配置typeHandler,我嘗試在title字段裏面配置NullToEmptyStringTypeHandler,而後試下能不能觸發NullToEmptyStringTypeHandler。

2019-05-27 17:15:09.546 DEBUG 17400 --- [nio-8080-exec-4] c.w.lesson.mapper.MyTaskMapper.insert    : ==>  Preparing: insert into my_task (title, description, user_id ) values (?, ?, ? ) 
into NullToEmptyStringTypeHandler,jdbcType=VARCHAR

結論是能夠觸發,但我敏銳(cidun)的偵探嗅覺發現居然連jdbcType均可以正確拿到,難道是覺得個人xml裏面寫了

#{title,jdbcType=VARCHAR......

也就是說,若是我把以前的註解裏面也把jdbcType寫上去,應該也是能夠的。我當即行動,改了下MyUserMapper的註解代碼

@Insert("INSERT INTO MY_USER(NAME, NICKNAME,AGE,BIRTHDAY,MEMO) VALUES(#{name,jdbcType=VARCHAR},#{nickname},#{age},#{birthday},#{memo})")
    @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
    void insert(MyUser myUser);

我在註解的name字段後面加上了jdbcType=VARCHAR,看看NullToEmptyStringTypeHandler能不能取到:

......

結果是不能夠,如今就很尷尬了,不加jdbcType,能夠觸發NullToEmptyStringTypeHandler,加了jdbcType,反而不能觸發了,我仿照xml的樣子,把NullToEmptyStringTypeHandler寫到註解的sql裏面去試下:

@Insert("INSERT INTO MY_USER(NAME, NICKNAME,AGE,BIRTHDAY,MEMO) VALUES(#{name,jdbcType=VARCHAR,typeHandler=com.wphmoon.lesson.common.typehandler.NullToEmptyStringTypeHandler},#{nickname},#{age},#{birthday},#{memo})")

這下觸發了NullToEmptyStringTypeHandler,而且可以獲得jdbcType的值爲VARCHAR。

到這裏,我獲得的結論是,若是在字段裏面寫上去typeHandler具體處理類(NullToEmptyStringTypeHandler),那麼不管寫不寫jdbcType都會觸發具體TypeHandler處理類,若是不在字段裏面寫,那麼寫了jdbcType反而不會觸發。這是爲何呢?

我繼續打開個人偵探直覺。此次不是去google,而是去看了下@MappedTypes(NullToEmptyStringTypeHandler頭頂上的)註解的源代碼,結果源代碼平平無奇(此處有古天樂的臉),但在同一個包下,發現了另一個註解,@MappedJdbcTypes,這不就是觸發jdbcType用的嗎,我激動了,把這個註解加到了NullToEmptyStringTypeHandler上面:

@MappedTypes(value = String.class)
@MappedJdbcTypes(value=JdbcType.VARCHAR)
public class NullToEmptyStringTypeHandler implements TypeHandler<String> {
......

把註解的sql和xml的sql的jdbcType加上去,把手寫的typeHandler去掉,結果是MyUserMapper(註解方式)和MyTaskMapper(xml方式)都可以觸發。自此,此案告破。

四、結案陳詞

在mybatis中,須要自定義控制字段的轉換,能夠本身實現TypeHandler<T>接口,這樣在執行sql語句的時候,就會自動觸發TypeHandler的實現類,實TypeHandler的實現類有兩個註解,@MappedTypes和@MappedJdbcTypes,註解的規則以下:

  • 這兩個註解是觸發TypeHandler的條件,MappedTypes是輸入字段的java類型,好比String,Integer等
  • MappedJdbcTypes是數據字段的數據庫類型,好比VARCHR,INT等,可是這個字段類型須要自行在mybatis的sql裏面自行配置,mybatis並不會本身從數據庫讀取。
  • 這兩個條件取的是並集關係,若是配置了MappedTypes和MappedJdbcTypes,必須符合這二者的條件纔會觸發TypeHandler實現類。
  • 若是在字段的配置上面寫明瞭typeHandler=TypeHandler實現類,那麼就會無視上面註解的條件,觸發該TypeHandler實現類

五、案情擴展

    案情雖然告破,但涉案人員(開始的那位提問題的同事)不樂意了,表示xml文件的還好辦,能夠用mybatis generator來搞定(mybatis generator後續會有專門的教程,先挖個坑),但若是是用註解,並不想每一個字段都標記jdbcType,那怎麼搞?其實有個辦法的,看代碼:

@MappedTypes(value = MyUser.class)
public class MyUserTypeHandler implements TypeHandler<MyUser> {

	@Override
	public void setParameter(PreparedStatement ps, int i, MyUser parameter, JdbcType jdbcType) throws SQLException {
		System.out.println("into MyUserTypeHandler,parameter="+parameter+",jdbcType="+jdbcType);
	}

	......
 
}

MappedTypes可不僅是能夠傳String,Integer這些單字段的類型,能夠直接報對象的類型傳進來,這樣,每一個對象屬性都會觸發TypeHandler實現類,這樣,就不須要每一個字段都標記jdbcType了,而能夠根據對象屬性的java類型自行判斷後去處理。

好了,到此爲止,全案完結,須要閱讀完整卷宗的,請自行取閱,源代碼

相關文章
相關標籤/搜索