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