前天,一个涉案人员(同事)提到,在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类型自行判断后去处理。
好了,到此为止,全案完结,须要阅读完整卷宗的,请自行取阅,源代码