1. 程式人生 > >mybatis偵探實錄:typehandler薛定諤之謎

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

1、案件背景

前天,一個涉案人員(同事)提到,在mysql的資料庫中,dba推薦的做法是所有的varchar欄位都設定成不能為空,並且預設值為empty string,這樣對查詢效能有一定的幫助,設定的sql片段是這樣的:

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

問我在mybatis裡面這種情況怎麼設定。我假裝思考,然後飛快的開啟谷歌,搜尋答案,得到了一個詞,typehandler。typehandler是mybatis用來針對java型別和資料庫型別對不上時做處理工作的類,當前的情況就是如果我輸入的型別是null,那麼在資料庫要自動轉換成空字串,不能直接把null塞到資料庫欄位裡面。typehandler的做法是寫一個類來實現TypeHandler介面,於是我就寫一個簡單的:

@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的話,那就直接填一個空字元進去,完事大吉。

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

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

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

2、案發現場

為了驗證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類:

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來執行:

@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輸出是這樣的:

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

這是什麼鬼,jdbcType完全不是我以為的VARCHAR型別。不過好歹NullToEmptyStringTypeHandler 被觸發執行了,如果我不需要檢驗jdbcType的話,這個功能算是實現了,我把所有的null值直接替換成空字串就行了。

但我好死不死,想看下如果我是用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方式)執行的時候沒有被觸發。

這是為啥呢,讓我們開始破案。

3、案情追查

我一開始是被兩種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方式)都能夠觸發。自此,此案告破。

4、結案陳詞

在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實現類

5、案情擴充套件

    案情雖然告破,但涉案人員(開始的那位提問題的同事)不樂意了,表示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型別自行判斷後去處理。

好了,到此為止,全案完結,需要閱讀完整卷宗的,請自行取閱,

相關推薦

mybatis偵探實錄typehandler

1、案件背景 前天,一個涉案人員(同事)提到,在mysql的資料庫中,dba推薦的做法是所有的varchar欄位都設定成不能為

書接上文的貓是如何誕生的?

編輯手記:注重細節,是DBA必要的基本素質要求。 上一篇文章討論了非空欄位中如果存在空值對於查詢的影響,這裡描述一下導致問題的原因。 書接上文(參考:空與非空 – 資料庫中也有薛定諤的貓?),其實CBO的判斷本身是沒有問題的,問題在於,為什麼一個空值會存在非空約束的欄位中。 SQL> sel

MIT量子力學公開課第6講方程筆記

Youtube視訊 https://www.youtube.com/watch?v=TWpyhsPAK14 幾個基本假設 粒子的運動軌跡由波動函式\(\psi(x,t)\)完全描述,其中\(|\psi(x,t)|^2\)表示\(t\)時刻,粒子出現在\(x\)的概率 任意一種測量都對應一個

的貓,把妹法。用科學的辦法把妹,解決程式設計師終身大事

標題 薛定諤把妹法 “薛定諤把妹法”其靈感來自著名的物理學假設“薛定諤的貓”。“薛定諤把妹法”中心思想是:事件在被觀察以前,一直處在一個所謂“概率雲”的狀態下,一旦受到觀察,則坍縮為實體。通俗一些,就是要給女生神祕感。 中文名 薛定諤把妹法 外文名 Chase girls with Sc

的貓跳進了生物學界 化學家:沒有我可能辦不到

“90後”女大學生放棄北大保送復旦!她到底有多厲害?  東北網12月6日訊(記者 姜姍姍) 在東北農業大學有這樣一個自強不息的女大學生,她放棄北大直博被保送到復旦大學藥學院。她本科期間獲得國家獎學金、國家勵志獎學金、新東方自強獎學金、第一屆全國大學生生命聯賽國家二等獎……被評為黑龍江省“三好學生”。她就是生命

神經網路與方程

定態薛定諤方程是 如果是定態的自由粒子,這個方程的解是 因為是定態的波函式與時間無關,這個粒子的能量E不隨時間變化 假設E=1,讓t→0 所以波函式變成 讓A和都等於1   讓神經網路裡的節點都是在位形空間中  

Matlab方程工具箱系列——GUI初步美化

function h1 = main_gui_export() % This is the machine-generated representation of a Handle Graphics object % and its children. Note that handle values may

從“的貓”聯想到“好奇害死貓”

    喜歡物理學尤其是量子力學的朋友一定對薛定諤的貓不會陌生,至於那些不大懂的小夥伴建議可以網上搜索瞭解下,對你的人生觀、價值觀可能會有所改變(不說笑,真的哦)。     對於量子論從愛因斯坦、波爾時代至今,一直是人們討論的焦點。因為量子論只存在於理論和思維實驗,理論上

物理學四大神獸,除了“的貓”, 你還知道哪幾個?

物理學是一門研究物質運動最一般規律和物質基本結構的學科。分為純物理學和多學科物理學,其中的純物理

當Python中混進一隻的貓……

本文原創並首發於公眾號【Python貓】,未經授權,請勿轉載。 原文地址:https://mp.weixin.qq.com/s/

《哈利·波特霍格沃茨》邀請玩家為魔法世界的聖誕節裝飾大廳

12月6日至12日,粉絲可在“和霍格沃茨之謎一起過聖誕”活動中為最喜愛的假日裝飾投票   --(美國商業資訊)--Jam City:      人物:    

MyBatis初窺自定義TypeHandler處理列舉

TypeHandler簡介 在MyBatis中,StatementHandler負責對需要執行的SQL語句進行預編譯處理,主要完成以下兩項工作:1.呼叫引數處理器(ParameterHandler)來設定需要傳入SQL的引數;2.呼叫結果集處理器(ResultSetHand

Java程式設計基礎Mybatis註解中使用typeHandler實現Java列舉與資料庫int值的自動轉換

概述 在專案開發過程中經常會遇到資料庫儲存的是數值,在Java程式碼列舉表示的欄位。這些欄位在儲存和查詢時需要做一個轉換:寫資料庫的時候將列舉轉換為數字,讀資料庫時將數字轉換為列舉。 下面介紹一種通過mybatis註解實現資料型別自動轉換的方式。該方式能處理

MyBatis攔截器給參數對象屬性賦值

是否 tle dsta ref 截器 throws dev ndt pri 1 package com.development; 2 3 import java.lang.reflect.InvocationTargetException; 4 impo

spark使用說明

建表 pan schemardd 特性 -s map data div popu 背景 spark-shell是一個scala編程解釋運行環境,能夠通過編程的方式處理邏輯復雜的計算,但對於簡單的類似sql的數據處理,比方分組求和,sql為”selec

Mybatis框架三DAO層開發、Mapper動態代理開發

urn 測試類 new post 綁定 def 情況下 asstream implement 這裏是最基本的搭建:http://www.cnblogs.com/xuyiqing/p/8600888.html 接下來做到了簡單的增刪改查:http://www.cnblogs.

實錄我被緩存TP的緩存文件坑了一晚上!

thinkphp 緩存 實錄:我被緩存TP的緩存文件坑了一晚上!2018年3月21日晚上9點左右。自己開發的項目,為了優化系統性能,我盡量在數據查詢時添加緩存,如:M(‘tbale_name’) -> cache(‘cache_name’) -> select();為了讓緩存數據與最新數據

surface安裝linux采坑實錄grub 無法安裝

記不清 室友 記不清了 int class 界面 clas overflow over 最近準備面試,很想拓展下自己的技術棧,於是就找上了linux。 然後安裝linux當然用的是自己的sp3啦,反正平時也就當上網本用用,終於可以發掘一下潛力了233 然而夢想是美好

Netty3分隔符和長解碼器

cef 協議 設置 object buffer nod 疑問 示例 HR 回顧TCP粘包/拆包問題解決方案 上文詳細說了TCP粘包/拆包問題產生的原因及解決方式,並以LineBasedFrameDecoder為例演示了粘包/拆包問題的實際解決方案,本文再介紹兩種粘包/拆包問