前天,一個涉案人員(同事)提到,在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,註解的規則以下:
案情雖然告破,但涉案人員(開始的那位提問題的同事)不樂意了,表示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類型自行判斷後去處理。
好了,到此爲止,全案完結,須要閱讀完整卷宗的,請自行取閱,源代碼