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类型自行判断后去处理。
好了,到此为止,全案完结,需要阅读完整卷宗的,请自行取阅,源代码