小林子:串哥
串一串:干哈啊,又来
小林子:如果MySQL一张表中一个字段存储的数据格式是"1,2,3,4,5",也就是逗号分隔的,我如何能让别的使用者在无感知的情况下,只用List<Integer>来传输和接收?持久层用的MyBatis。你滴明白我的意思吗?
串一串:不明白
小林子:…
串一串:你知道MyBatis中有一个类叫BaseTypeHandler吗?这个类可以满足你的需求。
小林子:具体要怎么做?我有点懵,没接触过这个类,它是干嘛的?
串一串:我们来看个例子
创建一张表待用:
1 2 3 4 5
   | create table qfant_message.demo ( 	id int auto_increment primary key, 	name varchar(10) null, 	hobbies varchar(100) null );
   | 
 
然后新建一个SpringBoot工程,在工程中引入mybatis-generator,我们使用它来生成Mapper文件,如果不会的话,自行谷歌,这里不做详细讲解,下一篇再说。
在生成Mapper文件之前,我们先定义一个处理字段hobbies的TypeHandler,命名为ListTypeHandler,这里问个问题:为什么不叫HobbiesTypeHandler呢?这样应该和字段更加贴合啊。原因是这个Handler不仅仅是能处理hobbies,它可以处理所有相同情况的任何表的任何字段。这个类继承自org.apache.ibatis.type.BaseTypeHandler,来看下简化后的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
   | public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
    @Override   public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {     if (parameter == null) {       ps.setNull(i, jdbcType.TYPE_CODE);     } else {       setNonNullParameter(ps, i, parameter, jdbcType);     }   }
    @Override   public T getResult(ResultSet rs, String columnName) throws SQLException {     return getNullableResult(rs, columnName);   }
    @Override   public T getResult(ResultSet rs, int columnIndex) throws SQLException {     return getNullableResult(rs, columnIndex);   }
    @Override   public T getResult(CallableStatement cs, int columnIndex) throws SQLException {     return getNullableResult(cs, columnIndex);   }
    public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
    public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;
    public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;
    public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
  }
  | 
 
我们可以看到,不论是通过哪一个getResult方法获取数据,都是去调用下面的几个抽象方法,MyBatis帮我们实现了很多常用的类型的Handler,都在org.apache.ibatis.type包里面,截图看下吧,免得以为在忽悠你
小林子:那这里面有没有能满足我这个需求的Handler?如果有的话我就直接用了
串一串:你去看看,这里我说一下怎么重复造轮子
根据上述内容,我们就可以来写ListTypeHandler了,在写之前先整理一下思路:
因为我们实体类中hobbies属性是java.util.List类型的,而数据库表中hobbies字段是varchar类型的,所以我们需要在更新(插入)之前和查询之后对数据进行一次转换
- 插入之前:将List中的数据转换为以逗号分隔的字符串
 
- 查询之后:将逗号分隔的字符串转换为List结构
 
思路理顺了,我们来看看具体的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
   | package cc.kevinlu.handler;
  import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors;
  import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes;
  import com.qfant.sms.data.model.DemoDO;
  @MappedJdbcTypes(value = { JdbcType.VARCHAR })
  public class ListTypeHandler extends BaseTypeHandler<List<Integer>> {     @Override     public void setNonNullParameter(PreparedStatement ps, int i, List<Integer> parameter, JdbcType jdbcType)             throws SQLException {         String d = parameter.stream().map(v -> String.valueOf(v)).collect(Collectors.joining(","));         ps.setString(i, d);     }
      @Override     public List<Integer> getNullableResult(ResultSet rs, String columnName) throws SQLException {         String values = rs.getString(columnName);         return getResults(values);     }
      @Override     public List<Integer> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {         String values = rs.getString(columnIndex);         return getResults(values);     }
      @Override     public List<Integer> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {         String values = cs.getString(columnIndex);         return getResults(values);     }
      private List<Integer> getResults(String values) {         if (StringUtils.isNotBlank(values)) {             String[] data = values.split(",");             return Arrays.stream(data).mapToInt(v -> Integer.parseInt(v)).boxed().collect(Collectors.toList());         }         return new ArrayList<>();     } }
   | 
 
然后生成对应的Mapper和DO实体类,刚才说了我们使用的是mybatis-generator,这里直接贴上<table>的相关配置
1 2 3 4 5 6 7 8 9
   | <table tableName="demo" domainObjectName="DemoDO" mapperName="DemoMapper"        enableCountByExample="true"        enableDeleteByExample="true" enableInsert="true" enableSelectByExample="true"        enableUpdateByExample="true"        selectByExampleQueryId="true" enableSelectByPrimaryKey="true">   <generatedKey column="id" sqlStatement="MySql" identity="true"/>   <columnOverride column="hobbies" property="hobbies" jdbcType="VARCHAR" javaType="java.util.List"                   typeHandler="cc.kevinlu.handler.ListTypeHandler"/> </table>
   | 
 
注意这里我们使用标签<columnOverride>重写了column的定义,这里一定要指明javaType和typeHandler,javaType的目的是让生成的DemoDO的属性hobbies声明为java.util.List,如果不加该字段的话,默认会根据jdbcType="VARCHAR"生成java.lang.String类型,然后typeHandler指向我们刚创建的ListTypeHandler,这样在生成DemoMapper.xml的时候,会在对应的字段上加上typeHandler,否则需要我们挨个儿位置的去修改,xml中的内容如下:
1 2 3 4 5 6 7 8 9 10
   | <insert id="insert" parameterType="cc.kevinlu.data.model.DemoDO">   <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">     SELECT LAST_INSERT_ID()   </selectKey>   insert into demo (name, hobbies)   values (   		#{name,jdbcType=VARCHAR},   		#{hobbies,jdbcType=VARCHAR,typeHandler=com.qfant.sms.handler.ListTypeHandler}   ) </insert>
   | 
 
小林子:是不是这样就可以直接使用了?
串一串:你有没有注意到ListTypeHandler上有一个被注释掉的注解,把那个注释打开,然后value指向DO实体类即可,这个注释的意思是指定该Handler映射的java类,value是一个数组,可以指定一组映射类,当然也可以不指定。即使指定了,也可以用于其他类型,然后@MappedJdbcTypes映射的是jdbc的类型
小林子:那现在是不是可以测试啦?走一波~
1 2 3 4 5 6 7 8
   | @Resource private DemoMapper demoMapper;
  @Test public void index() {   List<DemoDO> data = demoMapper.selectByExample(new DemoDOExample());   data.forEach(System.out::println); }
   | 
 
输出:
Demo1DO [Hash = 3112387, id=1, name=123, hobbies=[1, 2, 3], serialVersionUID=1]
Demo1DO [Hash = 3294608, id=2, name=456, hobbies=[4, 5, 6], serialVersionUID=1]
我们来总结一下:
- MyBatis之所以能解决MySQL字段和Java属性之间的匹配,全都依赖于
org.apache.ibatis.type.BaseTypeHandler<T>抽象类,在该类中定义了3个获取结果的方法、1个更新的方法和4个抽象方法,我们可以自定义该抽象类来实现这个4个抽象方法进行Java类的属性和表字段的映射,可以做一些相关的处理。 
- MyBatis在
org.apache.ibatis.type包中定义了常用的字段映射Handler,并且在服务启动的时候会在TypeHandlerRegistry构造方法中将其注册到一个Map中,而TypeHandlerRegistry是在MyBatis的核心类Configuration中进行的实例化 
- 自定义的
Handler可以全局通用,不受限于某一个字段或某一个Java类 
- 在生成Mapper时使用
<columnOverride>重写column声明,然后需要指定jdbcType和typeHandler