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类型自行判断后去处理。

好了,到此为止,全案完结,需要阅读完整卷宗的,请自行取阅,源代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值