当前位置:   article > 正文

mybatis侦探实录:typehandler薛定谔之谜

typehandler 没有添加依旧执行了

1、案件背景

前天,一个涉案人员(同事)提到,在mysql的数据库中,dba推荐的做法是所有的varchar字段都设置成不能为空,并且默认值为empty string,这样对查询性能有一定的帮助,设置的sql片段是这样的:

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

问我在mybatis里面这种情况怎么设置。我假装思考,然后飞快的打开谷歌,搜索答案,得到了一个词,typehandler。typehandler是mybatis用来针对java类型和数据库类型对不上时做处理工作的类,当前的情况就是如果我输入的类型是null,那么在数据库要自动转换成空字符串,不能直接把null塞到数据库字段里面。typehandler的做法是写一个类来实现TypeHandler接口,于是我就写一个简单的:

  1. @MappedTypes(value = String.class)
  2. public class NullToEmptyStringTypeHandler implements TypeHandler<String> {
  3. @Override
  4. public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
  5. System.out.println("into NullToEmptyStringTypeHandler");
  6. if(parameter == null && jdbcType == JdbcType.VARCHAR){//判断传入的参数值是否为null
  7. ps.setString(i,"");//设置当前参数的值为空字符串
  8. }else{
  9. ps.setString(i,parameter);//如果不为null,则直接设置参数的值为value
  10. }
  11. }
  12. @Override
  13. public String getResult(ResultSet rs, String columnName) throws SQLException {
  14. return rs.getString(columnName);
  15. }
  16. @Override
  17. public String getResult(ResultSet rs, int columnIndex) throws SQLException {
  18. return rs.getString(columnIndex);
  19. }
  20. @Override
  21. public String getResult(CallableStatement cs, int columnIndex) throws SQLException {
  22. return cs.getString(columnIndex);
  23. }
  24. }

重点在于注解@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是否可用,我写了一个简单的表来验证,表结构如下

  1. CREATE TABLE `my_user` (
  2. `id` bigint(20) NOT NULL AUTO_INCREMENT ,
  3. `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '姓名' ,
  4. `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '昵称' ,
  5. `age` int(11) NULL DEFAULT NULL COMMENT '年龄' ,
  6. `birthday` datetime NULL DEFAULT NULL COMMENT '生日' ,
  7. `memo` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '备注' ,
  8. PRIMARY KEY (`id`)
  9. )
  10. ENGINE=InnoDB;

我又弄了个配套的数据对象和mapper类:

  1. public class MyUser implements Serializable{
  2. public long id;
  3. public String name;
  4. public String nickname;
  5. public int age;
  6. public Date birthday;
  7. public String memo;
  8. //get,set.......
  9. }
  1. @Mapper
  2. public interface MyUserMapper {
  3. @Select("SELECT * FROM MY_USER WHERE NAME = #{name}")
  4. MyUser findByName(@Param("name") String name);
  5. @Select("SELECT * FROM MY_USER WHERE ID = #{id}")
  6. MyUser findById(@Param("id") Long id);
  7. @Insert("INSERT INTO MY_USER(NAME, NICKNAME,AGE,BIRTHDAY,MEMO) VALUES(#{name},#{nickname},#{age},#{birthday},#{memo})")
  8. @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
  9. void insert(MyUser myUser);
  10. }

最后,我搞了一个controller来执行:

  1. @RestController
  2. @RequestMapping("/my")
  3. public class MyController {
  4. @Autowired
  5. private MyUserMapper myUserMapper;
  6. @RequestMapping(path="/insert2MyUser")
  7. public String insert2Myuser(MyUser myUser) {
  8. myUserMapper.insert(myUser);
  9. return "";
  10. }
  11. }

执行http://localhost:8080/my/insert2MyUser?age=1后的结果有喜有忧,得到的console输出是这样的:

  1. into NullToEmptyStringTypeHandler,jdbcType=OTHER
  2. into NullToEmptyStringTypeHandler,jdbcType=OTHER
  3. into NullToEmptyStringTypeHandler,jdbcType=OTHER

这是什么鬼,jdbcType完全不是我以为的VARCHAR类型。不过好歹NullToEmptyStringTypeHandler 被触发执行了,如果我不需要检验jdbcType的话,这个功能算是实现了,我把所有的null值直接替换成空字符串就行了。

但我好死不死,想看下如果我是用xml来配置mybatis的sql情况会不会有所不同,我搞过了一个表,用xml的方式来实现,表的结构如下:

  1. CREATE TABLE `my_task` (
  2. `id` bigint(20) NOT NULL AUTO_INCREMENT ,
  3. `title` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' ,
  4. `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' ,
  5. `user_id` bigint(20) NULL DEFAULT NULL ,
  6. PRIMARY KEY (`id`)
  7. )
  8. ENGINE=InnoDB;

mapper文件和数据对象文件如下:

  1. @Mapper
  2. public interface MyTaskMapper {
  3. long countByExample(MyTaskExample example);
  4. int deleteByExample(MyTaskExample example);
  5. int deleteByPrimaryKey(Long id);
  6. int insert(MyTask record);
  7. int insertSelective(MyTask record);
  8. MyTask selectOneByExample(MyTaskExample example);
  9. List<MyTask> selectByExample(MyTaskExample example);
  10. MyTask selectByPrimaryKey(Long id);
  11. int updateByExampleSelective(@Param("record") MyTask record, @Param("example") MyTaskExample example);
  12. int updateByExample(@Param("record") MyTask record, @Param("example") MyTaskExample example);
  13. int updateByPrimaryKeySelective(MyTask record);
  14. int updateByPrimaryKey(MyTask record);
  15. }
  1. public class MyTask implements Serializable{
  2. private Long id;
  3. private String title;
  4. private String description;
  5. private Long userId;
  6. //get,set.......

还有mapper的xml文件,这个太长了,我就只列insert语句的部分

  1. <insert id="insert" parameterType="com.wphmoon.lesson.domain.MyTask">
  2. <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
  3. SELECT LAST_INSERT_ID()
  4. </selectKey>
  5. insert into my_task (title, description, user_id
  6. )
  7. values (#{title,jdbcType=VARCHAR}, #{description,jdbcType=VARCHAR}, #{userId,jdbcType=BIGINT}
  8. )
  9. </insert>

我同样在controller中写了一段新增记录的代码:

  1. @RequestMapping(path="/insert2MyTask")
  2. public String insert2MyTask(MyTask myTask) {
  3. return String.valueOf(myTaskMapper.insert(myTask));
  4. }

执行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

  1. 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(?,?,?,?,?)
  2. into NullToEmptyStringTypeHandler,jdbcType=OTHER
  3. into NullToEmptyStringTypeHandler,jdbcType=OTHER
  4. into NullToEmptyStringTypeHandler,jdbcType=OTHER
  5. 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,完全没有触发

  1. 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 (?, ?, ? )
  2. 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里面配置如下:

  1. <insert id="insert" parameterType="com.wphmoon.lesson.domain.MyTask">
  2. <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
  3. SELECT LAST_INSERT_ID()
  4. </selectKey>
  5. insert into my_task (title, description, user_id
  6. )
  7. values (#{title,jdbcType=VARCHAR,typeHandler=com.wphmoon.lesson.common.typehandler.NullToEmptyStringTypeHandler}, #{description,jdbcType=VARCHAR}, #{userId,jdbcType=BIGINT}
  8. )
  9. </insert>

可以直接在字段里面配置typeHandler,我尝试在title字段里面配置NullToEmptyStringTypeHandler,然后试下能不能触发NullToEmptyStringTypeHandler。

  1. 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 (?, ?, ? )
  2. into NullToEmptyStringTypeHandler,jdbcType=VARCHAR

结论是可以触发,但我敏锐(cidun)的侦探嗅觉发现竟然连jdbcType都可以正确拿到,难道是以为我的xml里面写了

#{title,jdbcType=VARCHAR......

也就是说,如果我把之前的注解里面也把jdbcType写上去,应该也是可以的。我立即行动,改了下MyUserMapper的注解代码

  1. @Insert("INSERT INTO MY_USER(NAME, NICKNAME,AGE,BIRTHDAY,MEMO) VALUES(#{name,jdbcType=VARCHAR},#{nickname},#{age},#{birthday},#{memo})")
  2. @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
  3. 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上面:

  1. @MappedTypes(value = String.class)
  2. @MappedJdbcTypes(value=JdbcType.VARCHAR)
  3. public class NullToEmptyStringTypeHandler implements TypeHandler<String> {
  4. ......

把注解的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,那怎么搞?其实有个办法的,看代码:

  1. @MappedTypes(value = MyUser.class)
  2. public class MyUserTypeHandler implements TypeHandler<MyUser> {
  3. @Override
  4. public void setParameter(PreparedStatement ps, int i, MyUser parameter, JdbcType jdbcType) throws SQLException {
  5. System.out.println("into MyUserTypeHandler,parameter="+parameter+",jdbcType="+jdbcType);
  6. }
  7. ......
  8. }

MappedTypes可不只是可以传String,Integer这些单字段的类型,可以直接报对象的类型传进来,这样,每个对象属性都会触发TypeHandler实现类,这样,就不需要每个字段都标记jdbcType了,而可以根据对象属性的java类型自行判断后去处理。

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

转载于:https://my.oschina.net/u/173343/blog/3054695

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/569334
推荐阅读
相关标签
  

闽ICP备14008679号