小言_互联网的博客

Mybatis系列第11篇:类型处理器,这个你得会玩!

233人阅读  评论(0)

Mybatis系列目标:从入门开始开始掌握一个高级开发所需要的Mybatis技能。

这是mybatis系列第11篇,源码位于文章尾部!

mybatis内部参数设置和结果的处理

先来看一个案例:

jdbc的方式插入用户信息,参数为UserModel:


   
  1. public class UserModel {
  2.     private Integer id;
  3.     private String name;
  4.     private Integer age;
  5.      //省略了get set方法
  6. }
  7. public static  int insert(UserModel userModel) throws SQLException {
  8.     Connection connection = null;
  9.     String sql =  "insert into t_user (id,name,age) values (?,?,?)";
  10.     PreparedStatement preparedStatement = connection.prepareStatement(sql);
  11.     preparedStatement.setInt( 1,userModel.getId());
  12.     preparedStatement.setString( 2,userModel.getName());
  13.     preparedStatement.setInt( 3,userModel.getAge());
  14.      return preparedStatement.executeUpdate();
  15. }

上面我们调用了preparedStatement的几个方法来设置参数,如下:


   
  1. preparedStatement.setInt( 1,userModel.getId());
  2. preparedStatement.setString( 2,userModel.getName());
  3. preparedStatement.setInt( 3,userModel.getAge());

当我们使用mybatis插入用户信息的时候,mybatis底层也会调用PreparedStatement的这些设置参数的方法,mybatis底层是如何判断调用哪个方法的呢?

是调用setInt方法还是setString方法的呢?

再来看一个jdbc查询的案例,查询出t_user表所有数据,将其转换为UserModel对象,核心代码如下:


   
  1. public static List<UserModel> getList() throws Exception {
  2.     List<UserModel> result =  new ArrayList<>();
  3.     Connection connection = null;
  4.     String sql =  "select id,name,age from t_user";
  5.     PreparedStatement preparedStatement = connection.prepareStatement(sql);
  6.     ResultSet rs = preparedStatement.executeQuery();
  7.     while (rs.next()) {
  8.         UserModel userModel =  new UserModel();
  9.         userModel.setId(rs.getInt( 1));
  10.         userModel.setName(rs.getString( 2));
  11.         userModel.setAge(rs.getInt( 3));
  12.         result.add(userModel);
  13.     }
  14.      return result;
  15. }

上面有几行从ResultSet获取数据的方法,如下:


   
  1. userModel.setId(rs.getInt( 1));
  2. userModel.setName(rs.getString( 2));
  3. userModel.setAge(rs.getInt( 3));

如果使用mybatis实现,mybatis可以将sql结果自动映射到UserModel中的属性中,mybatis内部给UserModel的id设置值的时候,mybatis内部是如何知道是调用rs.getInt还是调用rs.getString来获取id列的值的呢?

这些就是mybatis中类型转换器做的事情,类型转换器主要有2个功能:

  1. 参数设置,即设置参数的时候需要调用PreparedStatement中哪个set方法去设置参数,比如:插入用户信息id字段,id字段是java中的Integer类型的,mybatis内部知道需要调用setInt方法去给id字段设置参数。

  2. 将sql查询结果转换为对应的java类型,即调用ResultSet中的哪个get方法去获取参数,比如:id在数据中是int类型的,读取的时候会调用ResultSet的getInt方法去读取,而name字段在db中是varchar类型的,读取的时候会调用getString方法去读取,而不是调用getInt方法去读取。

mybatis中定义了一个类型转换器的接口:


   
  1. public  interface TypeHandler<T> {
  2.   void setParameter(PreparedStatement ps,  int i, T parameter, JdbcType jdbcType) throws SQLException;
  3.    /**
  4.    * @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
  5.    */
  6.   T getResult(ResultSet rs, String columnName) throws SQLException;
  7.   T getResult(ResultSet rs,  int columnIndex) throws SQLException;
  8.   T getResult(CallableStatement cs,  int columnIndex) throws SQLException;
  9. }

第一个方法用于通过PreparedStatement设置参数的,即内部会根据参数的类型,去调用PreparedStatement中对应的方法去设置参数的值,比如是调用setInt方法呢还是setString方法,每个类型转换器中实现的是不同的。其他3个方法是从结果集中读取数据的,内部具体是调用结果集的getInt方法还是getString方法或者是getObject方法,每个转换器内部实现也是不一样的。

mybatis内部默认实现了很多类型转换器,基本上够我们用了,比如IntegerTypeHandler转换器,IntegerTypeHandler主要用来处理java中的Integer、int类型参数的,所以会调用setInt方法设置参数;读取的时候,对应的jdbc中的类型是JdbcType.INTEGER,所以会调用getInt方法读取数据库返回的值,读取的结果是Integer类型的。我们看一下其源码:


   
  1. public class IntegerTypeHandler extends BaseTypeHandler<Integer> {
  2.   @Override
  3.   public void setNonNullParameter(PreparedStatement ps,  int i, Integer parameter, JdbcType jdbcType)
  4.       throws SQLException {
  5.     ps.setInt(i, parameter);
  6.   }
  7.   @Override
  8.   public Integer getNullableResult(ResultSet rs, String columnName)
  9.       throws SQLException {
  10.      int result = rs.getInt(columnName);
  11.      return result ==  0 && rs.wasNull() ? null : result;
  12.   }
  13.   @Override
  14.   public Integer getNullableResult(ResultSet rs,  int columnIndex)
  15.       throws SQLException {
  16.      int result = rs.getInt(columnIndex);
  17.      return result ==  0 && rs.wasNull() ? null : result;
  18.   }
  19.   @Override
  20.   public Integer getNullableResult(CallableStatement cs,  int columnIndex)
  21.       throws SQLException {
  22.      int result = cs.getInt(columnIndex);
  23.      return result ==  0 && cs.wasNull() ? null : result;
  24.   }
  25. }

mybatis内部默认实现了很多类型转换器,每种类型转换器能够处理哪些java类型以及能够处理的JdbcType的类型,这些都在TypeHandlerRegistry进行注册的,大家可以去看一下这个类源码,IntegerTypeHandler也是在这个类中进行注册的,代码如下:


   
  1. register(Integer.class,  new IntegerTypeHandler());
  2. register( int.class,  new IntegerTypeHandler());
  3. register(JdbcType.INTEGER,  new IntegerTypeHandler());

所以当我们参数是Integer或者int类型的时候,mybatis会调用IntegerTypeHandler转换器中的setInt方法去设置参数,上面我们的UserModel的id字段是Integer类型的,所以插入数据的时候会调用IntegerTypeHandler处理器中的setInt方法去设置参数,当mybatis将t_user表的数据自动映射为UserModel类型的时候,mybatis会发现id类型是Integer类型,然后会找到IntegerTypeHandler中对应的读取结果集的方法去读取数据,即调用ResultSet的getInt方法读取id字段的数据,然后赋值给UserModel中的id属性。

自定义类型转换器

我们在用户表新增一个性别字段sex,java中我们通过一个枚举来表示这个类型,db脚本如下:


   
  1. DROP DATABASE IF EXISTS  `javacode2018`;
  2. CREATE DATABASE  `javacode2018`;
  3. USE  `javacode2018`;
  4. DROP TABLE IF EXISTS t_user;
  5. CREATE TABLE t_user(
  6.   id  int AUTO_INCREMENT PRIMARY KEY COMMENT  '用户id',
  7.   name VARCHAR( 32) NOT NULL DEFAULT  '' COMMENT '用户名 ',
  8.   age SMALLINT NOT NULL DEFAULT 1 COMMENT '年龄 ',
  9.   sex SMALLINT DEFAULT 0 COMMENT '性别, 0:未知, 1:男, 2:女 '
  10. ) COMMENT '用户表 ';
  11. INSERT INTO t_user VALUES (1,'路人甲Java ',30,1),(2,'林志玲 ',45,2);

t_user对应的Model如下:


   
  1. @Getter
  2. @Setter
  3. @Builder
  4. @ToString
  5. @NoArgsConstructor
  6. @AllArgsConstructor
  7. public class UserModel implements Serializable {
  8.     private Integer id;
  9.     private String name;
  10.     private Integer age;
  11.     private SexEnum sex;
  12. }

sex是一个枚举类型,枚举定义如下:


   
  1. /**
  2.  * 性别
  3.  */
  4. public enum SexEnum {
  5.     UNKNOW( 0"未知"),
  6.     MAN( 1"男"),
  7.     WOMAN( 2"女");
  8.     private Integer value;
  9.     private String name;
  10.     SexEnum(Integer value, String name) {
  11.         this.value = value;
  12.         this.name = name;
  13.     }
  14.     public Integer getValue() {
  15.          return value;
  16.     }
  17.     public String getName() {
  18.          return name;
  19.     }
  20.     public static SexEnum getByValue(Integer value) {
  21.          for (SexEnum item : values()) {
  22.              if (item.getValue().equals(value)) {
  23.                  return item;
  24.             }
  25.         }
  26.          return null;
  27.     }
  28. }

我们来写一个查询如下:


   
  1. < select id= "getList1" resultType= "com.javacode2018.chat05.demo10.model.UserModel">
  2. SELECT id,name,age,sex FROM t_user
  3. </ select>

这个大家都很熟悉了,查询t_user表,每行结果转换为一个UserModel对象,最后返回一个List集合,这个查询运行会不会有什么问题?

确实会报错,如下:


   
  1. ### Cause: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column  'sex' from result set.  Cause: java.lang.IllegalArgumentException: No enum constant com.javacode2018.chat05.demo10.enums.SexEnum .1
  2.     at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java: 30)
  3.     at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java: 149)
  4.     at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java: 140)
  5.     at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java: 147)
  6.     at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java: 80)
  7.     at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java: 93)

sex字段在t_user中是数字类型的,最后通过mybatis查询,需要赋值给UserModel中的sex字段,这个字段是一个枚举类型的,mybatis不知道这两者之间如何进行转换,所以报错了。

此时需要我们指定一个转换规则,来告知mybatis如何进行转换,需要我们用到自定义类型转换器了。

自定义一个类型转换器SexEnumTypeHandle,用来处理sex字段和SexEnum枚举之间的相互转换,代码如下:


   
  1. package com.javacode2018.chat05.demo10.typehandle;
  2. import com.javacode2018.chat05.demo10.enums.SexEnum;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.apache.ibatis. type.BaseTypeHandler;
  5. import org.apache.ibatis. type.JdbcType;
  6. import java.sql.CallableStatement;
  7. import java.sql.PreparedStatement;
  8. import java.sql.ResultSet;
  9. import java.sql.SQLException;
  10. @Slf4j
  11. public class SexEnumTypeHandle extends BaseTypeHandler<SexEnum> {
  12.     @Override
  13.     public void setNonNullParameter(PreparedStatement ps,  int i, SexEnum parameter, JdbcType jdbcType) throws SQLException {
  14.         ps.setInt(i, parameter.getValue());
  15.         log.info( "{}", parameter);
  16.     }
  17.     @Override
  18.     public SexEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
  19.         log.info( "{}", columnName);
  20.         Object object = rs.getObject(columnName);
  21.         Integer sex = object != null && object instanceof Integer ? (Integer) object : null;
  22.          return SexEnum.getByValue(sex);
  23.     }
  24.     @Override
  25.     public SexEnum getNullableResult(ResultSet rs,  int columnIndex) throws SQLException {
  26.         log.info( "{}", columnIndex);
  27.         Object object = rs.getObject(columnIndex);
  28.         Integer sex = object != null && object instanceof Integer ? (Integer) object : null;
  29.          return SexEnum.getByValue(sex);
  30.     }
  31.     @Override
  32.     public SexEnum getNullableResult(CallableStatement cs,  int columnIndex) throws SQLException {
  33.         log.info( "{}", columnIndex);
  34.         Object object = cs.getObject(columnIndex);
  35.         Integer sex = object != null && object instanceof Integer ? (Integer) object : null;
  36.          return SexEnum.getByValue(sex);
  37.     }
  38. }

自定义类型转换器可以直接继承BaseTypeHandler,后面有个泛型,泛型类型为需要处理的java类型,即SexEnum。

类型转换器定义好了,需要将其注册到mybatis中,需要在mybatis配置文件中加入:


   
  1. <typeHandlers>
  2.     <typeHandler handler= "com.javacode2018.chat05.demo10.typehandle.SexEnumTypeHandle"/>
  3. </typeHandlers>

再次运行一下上面的查询,对应测试用例:


   
  1. com.javacode2018.chat05.demo10.Demo10Test#getModelList
  2. @Test
  3. public void getModelList() throws IOException {
  4.     try (SqlSession sqlSession = this.sqlSessionFactory.openSession( true);) {
  5.         UserMapper mapper = sqlSession.getMapper(UserMapper.class);
  6.         Map<String, Object> paramMap =  new HashMap<>();
  7.         List<UserModel> userModelList = mapper.getList1(paramMap);
  8.         log.info( "{}", userModelList);
  9.     }
  10. }

运行结果:


   
  1. 26: 26.144 [main] DEBUG c.j.c.d.mapper.UserMapper.getList1 - ==>  Preparing: SELECT id,name,age,sex FROM t_user 
  2. 26: 26.167 [main] DEBUG c.j.c.d.mapper.UserMapper.getList1 - ==> Parameters: 
  3. 26: 26.187 [main] INFO  c.j.c.d.typehandle.SexEnumTypeHandle - sex
  4. 26: 26.189 [main] INFO  c.j.c.d.typehandle.SexEnumTypeHandle - sex
  5. 26: 26.189 [main] DEBUG c.j.c.d.mapper.UserMapper.getList1 - <==      Total:  2
  6. 26: 26.190 [main] INFO  c.j.chat05.demo10.Demo10Test - [UserModel(id= 1, name=路人甲Java, age= 30, sex=MAN), UserModel(id= 2, name=林志玲, age= 45, sex=WOMAN)]

这下正常了,看一下输出中的sex属性,对应的为枚举的名称。

在此案例代码的基础上,我们再来一个案例,向用户表插入用户信息,参数为一个map,每个字段的值丢在map中,key为字段的名称,sex我们传递SexEnum枚举类型。

对应的java测试用例代码如下:


   
  1. com.javacode2018.chat05.demo10.Demo10Test#insert1
  2. @Test
  3. public void insert1() throws IOException {
  4.     try (SqlSession sqlSession = this.sqlSessionFactory.openSession( true);) {
  5.         UserMapper mapper = sqlSession.getMapper(UserMapper.class);
  6.         Map<String, Object>  map =  new HashMap<>();
  7.         Integer id = Integer.valueOf(Calendar.getInstance().getTime().getTime() /  1000 +  "");
  8.          map.put( "id", id);
  9.          map.put( "name", id.toString());
  10.          map.put( "age"30);
  11.          map.put( "sex", SexEnum.WOMAN);
  12.          int result = mapper.insert1( map);
  13.         log.info( "{}", result);
  14.     }
  15. }

对应mapper xml我们按照下面这种写法,如:


   
  1. <insert id= "insert1" parameterType= "map">
  2.     insert into t_user (id,name,age,sex)
  3.     VALUE (#{id},#{name},#{age},#{sex})
  4. </insert>

这么写会不会有问题?我们来运行一下:


   
  1. 32: 36.343 [main] DEBUG c.j.c.d.mapper.UserMapper.insert1 - ==>  Preparing: insert into t_user (id,name,age,sex) VALUE (?,?,?,?) 
  2. 32: 36.378 [main] INFO  c.j.c.d.typehandle.SexEnumTypeHandle - WOMAN
  3. 32: 36.381 [main] DEBUG c.j.c.d.mapper.UserMapper.insert1 - ==> Parameters:  1578889956(Integer),  1578889956(String),  30(Integer),  2(Integer)
  4. 32: 36.387 [main] DEBUG c.j.c.d.mapper.UserMapper.insert1 - <==    Updates:  1
  5. 32: 36.387 [main] INFO  c.j.chat05.demo10.Demo10Test -  1

插入成功了,map中sex对应的是SexEnum.WOMAN,mybatis中判断map中sex对应的是SexEnum类型的,所以会找到其对应的类型转换器SexEnumTypeHandle,注意上面日志中的第二行,这个就是SexEnumTypeHandle转换器中输出的日志,说明设置sex参数的时候执行了SexEnumTypeHandle中的代码。

但是当我们不传递sex的值的时候,或者说sex传递为null的时候,此时mybatis是无法判断map中sex对应的具体类型的,mybatis内部将无法判断sex参数的设置的时候,无法找到具体用哪个类型转换器给sex设置参数,这个在mysql中不会报错,但是在oracle中会报错,大家机器上有oracle的,可以去试试下面代码运行结果:


   
  1. @Test
  2. public void insert1() throws IOException {
  3.     try (SqlSession sqlSession = this.sqlSessionFactory.openSession( true);) {
  4.         UserMapper mapper = sqlSession.getMapper(UserMapper.class);
  5.         Map<String, Object>  map =  new HashMap<>();
  6.         Integer id = Integer.valueOf(Calendar.getInstance().getTime().getTime() /  1000 +  "");
  7.          map.put( "id", id);
  8.          map.put( "name", id.toString());
  9.          map.put( "age"30);
  10.          map.put( "sex", null);
  11.          int result = mapper.insert1( map);
  12.         log.info( "{}", result);
  13.     }
  14. }

上面代码oracle中执行会报错,需要修改对应的mapper xml中的代码,需要让mybatis明确知道sex字段需要用哪个类型转换器进行处理,有2种写法。

第一种,通过typeHandler属性明确指定参数的类型转换器,如下


   
  1. <insert id= "insert1" parameterType= "map">
  2.     insert into t_user (id,name,age,sex)
  3.     VALUE (#{id},#{name},#{age},#{sex,typeHandler=com.javacode2018.chat05.demo10.typehandle.SexEnumTypeHandle})
  4. </insert>

第二种,可以指定sex字段的类型,需要通过javaType属性来指定参数的具体类型,如下:


   
  1. <insert id= "insert1" parameterType= "map">
  2.     insert into t_user (id,name,age,sex)
  3.     VALUE (#{id},#{name},#{age},#{sex,javaType=com.javacode2018.chat05.demo10.enums.SexEnum})
  4. </insert>

mybatis通过java的类型就可以找到对应的类型转换器,所以方式2也是正常的。

再来总结一下自定义类型转换器的使用步骤:

  1. 自定义类型转换器,继承BaseTypeHandler

  2. 将自定义类型转换器注册到mybatis中,需要在mybatis配置文件中通过typeHandler元素进行引入

  3. mapper xml中就可以使用了。

案例代码


   
  1. 链接:https: //pan.baidu.com/s/1vt-MAX3oJOu9gyxZAhKkbg 
  2. 提取码:i8op

MyBatis系列

  1. MyBatis系列第1篇:MyBatis未出世之前我们那些痛苦的经历

  2. MyBatis系列第2篇:入门篇,带你感受一下mybatis独特的魅力!

  3. MyBatis系列第3篇:Mybatis使用详解(1)

  4. MyBatis系列第4篇:Mybatis使用详解(2)

  5. Mybatis系列第5篇:Mapper接口多种方式传参详解、原理、源码解析

  6. Mybatis系列第6篇:恕我直言,mybatis增删改你未必玩得转!

  7. Mybatis系列第7篇:各种查询详解

  8. Mybatis系列第8篇:自动映射,使用需谨慎!

  9. Mybatis系列第9篇:延迟加载、鉴别器、继承怎么玩?

  10. Mybatis系列第10篇:动态SQL,这么多种你都会?

更多好文章

  1. Java高并发系列(共34篇)

  2. MySql高手系列(共27篇)

  3. Maven高手系列(共10篇)

  4. 聊聊db和缓存一致性常见的实现方式

  5. 接口幂等性这么重要,它是什么?怎么实现?

感谢大家的阅读,也欢迎您把这篇文章分享给更多的朋友一起阅读!谢谢!

路人甲java

▲长按图片识别二维码关注

路人甲Java:工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!坚信用技术改变命运,让家人过上更体面的生活!


转载:https://blog.csdn.net/likun557/article/details/103966425
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场