面試官問:Mybatis中的TypeHandler你用過嗎?

愛撒謊的男孩發表於2020-09-16

持續原創輸出,點選上方藍字關注我吧

目錄

  • 前言
  • 環境配置
  • 什麼是TypeHandler?
    • 如何自定義?
    • 如何將其新增到Mybatis中?
    • XML檔案中如何指定TypeHandler?
  • 原始碼中如何執行TypeHandler?
    • 入參如何轉換?
    • 結果如何轉換?
    • 總結
  • 總結

前言

  • 相信大家用Mybatis這個框架至少一年以上了吧,有沒有思考過這樣一個問題:資料庫有自己的資料型別,Java有自己的資料型別,那麼Mybatis是如何把資料庫中的型別和Java的資料型別對應的呢?

  • 本篇文章就來講講Mybatis中的黑匣子TypeHandler(型別處理器),說它是黑匣子一點都不為過,總是在默默的奉獻著,但是不為人知。

環境配置

  • 本篇文章講的一切內容都是基於Mybatis3.5SpringBoot-2.3.3.RELEASE

什麼是TypeHandler?

  • 顧名思義,型別處理器,將入參和結果轉換為所需要的型別,Mybatis中對於內建了許多型別處理器,實際開發中已經足夠使用了,如下圖:

  • 型別處理器這個介面其實很簡單,總共四個方法,一個方法將入參的Java型別的資料轉換為JDBC型別,三個方法將返回結果轉換為Java型別。原始碼如下:

public interface TypeHandler<T> {
  //設定引數,java型別轉換為jdbc型別
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
  //將查詢的結果轉換為java型別
  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}

如何自定義並使用TypeHandler?

  • 實際應用開發中的難免會有一些需求要自定義一個TypeHandler,比如這樣一個需求:前端傳來的年齡是,,但是資料庫定義的欄位卻是int型別(1男2女)。此時可以自定義一個年齡的型別處理器,進行轉換。

如何自定義?

  • 自定義的方式有兩種,一種是實現TypeHandler這個介面,另一個就是繼承BaseTypeHandler這個便捷的抽象類。
  • 下面直接繼承BaseTypeHandler這個抽象類,定義一個年齡的型別處理器,如下:
@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes(String.class)
public class GenderTypeHandler extends BaseTypeHandler {

    //設定引數,這裡將Java的String型別轉換為JDBC的Integer型別
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, StringUtils.equals(parameter.toString(),"男")?1:2);
    }

    //以下三個引數都是將查詢的結果轉換
    @Override
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getInt(columnName)==1?"男":"女";
    }

    @Override
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getInt(columnIndex)==1?"男":"女";
    }

    @Override
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getInt(columnIndex)==1?"男":"女";
    }
}
  • 這裡涉及到兩個註解,如下:
    • @MappedTypes:指定與其關聯的 Java 型別列表。 如果在 javaType 屬性中也同時指定,則註解上的配置將被忽略。
    • @MappedJdbcTypes:指定與其關聯的 JDBC 型別列表。 如果在 jdbcType 屬性中也同時指定,則註解上的配置將被忽略。

如何將其新增到Mybatis中?

  • Mybatis在與SpringBoot整合之後一切都變得很簡單了,其實這裡有兩種配置方式,下面將會一一介紹。
  • 第一種:只需要在配置檔案application.properties中新增一行配置即可,如下:
## 設定自定義的Typehandler所在的包,啟動的時候會自動掃描配置到Mybatis中
mybatis.type-handlers-package=cn.cb.demo.typehandler
  • 第二種:其實任何框架與Springboot整合之後,只要配置檔案中能夠配置的,在配置類中都可以配置(除非有特殊定製,否則不要輕易覆蓋自動配置)。如下:
@Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATOIN));
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        // 自動將資料庫中的下劃線轉換為駝峰格式
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultFetchSize(100);
        configuration.setDefaultStatementTimeout(30);
        sqlSessionFactoryBean.setConfiguration(configuration);
        //將typehandler註冊到mybatis
        GenderTypeHandler genderTypeHandler = new GenderTypeHandler();
        TypeHandler[] typeHandlers=new TypeHandler[]{genderTypeHandler};
        sqlSessionFactoryBean.setTypeHandlers(typeHandlers);
        return sqlSessionFactoryBean.getObject();
    }
  • 第二種方式的思想其實就是重寫自動配置類MybatisAutoConfiguration中的方法。注意:除非自己有特殊定製,否則不要輕易重寫自動配置類中的方法

XML檔案中如何指定TypeHandler?

  • 上面的兩個步驟分別是自定義和注入到Mybatis中,那麼如何在XML檔案中使用呢?
  • 使用其實很簡單,分為兩種,一種是更新,一種查詢,下面將會一一介紹。
  • 更新:刪除自不必說了,這裡講的是updateinsert兩種,只需要在#{}中指定的屬性typeHandler為自定義的全類名即可,程式碼如下:
<insert id="insertUser">
        insert into user_info(user_id,his_id,name,gender,password,create_time)
        values(#{userId,jdbcType=VARCHAR},#{hisId,jdbcType=VARCHAR},#{name,jdbcType=VARCHAR},
        #{gender,jdbcType=INTEGER,typeHandler=cn.cb.demo.typehandler.GenderTypeHandler},#{password,jdbcType=VARCHAR},now())
    </insert>
  • 查詢:查詢的時候型別處理會將JDBC型別的轉化為Java型別,因此也是需要指定typeHandler,需要在resultMap中指定typeHandler這個屬性,值為全類名,如下:
<resultMap id="userResultMap" type="cn.cb.demo.domain.UserInfo">
        <id column="id" property="id"/>
        <result column="user_id" property="userId"/>
        <result column="his_id" property="hisId"/>
            <!-- 指定typeHandler屬性為全類名-->
        <result column="gender" property="gender" typeHandler="cn.cb.demo.typehandler.GenderTypeHandler"/>
        <result column="name" property="name"/>
        <result column="password" property="password"/>
    </resultMap>

    <select id="selectList" resultMap="userResultMap">
        select * from user_info where status=1
        and user_id in
        <foreach collection="userIds" item="item" open="(" separator="," close=")" >
            #{item}
        </foreach>
    </select>

原始碼中如何執行TypeHandler?

  • 既然會使用TypeHandler了,那麼肯定要知道其中的執行原理了,在Mybatis中型別處理器是如何在JDBC型別和Java型別進行轉換的,下面的將從原始碼角度詳細介紹。

入參如何轉換?

  • 這個肯定是發生在設定引數的過程中,詳細的程式碼在PreparedStatementHandler中的parameterize()方法中,這個方法就是設定引數的方法。原始碼如下:
 @Override
  public void parameterize(Statement statement) throws SQLException {
    //實際呼叫的是DefaultParameterHandler
    parameterHandler.setParameters((PreparedStatement) statement);
  }
  • 實際執行的是DefaultParameterHandler中的setParameters方法,如下:
public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    //獲取引數對映
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    //遍歷引數對映,一一設定
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          //獲取型別處理器,如果不存在,使用預設的
          TypeHandler typeHandler = parameterMapping.getTypeHandler();    
          //JdbcType
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            //呼叫型別處理器中的方法設定引數,將Java型別轉換為JDBC型別
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          } catch (SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }
  • 從上面的原始碼中可以知道這行程式碼typeHandler.setParameter(ps, i + 1, value, jdbcType);就是呼叫型別處理器中的設定引數的方法,將Java型別轉換為JDBC型別。

結果如何轉換?

  • 這一過程肯定是發生在執行查詢語句的過程中,之前也是介紹過Mybatis的六大劍客,其中的ResultSetHandler這個元件就是對查詢的結果進行處理的,那麼肯定是發生在這一元件中的某個方法。
  • PreparedStatementHandler執行查詢結束之後,呼叫的是ResultSetHandler中的handleResultSets()方法,對結果進行處理,如下:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //執行SQL
    ps.execute();
    //處理結果
    return resultSetHandler.handleResultSets(ps);
  }
  • 最終的在DefaultResultHandler中的getPropertyMappingValue()方法中呼叫了TypeHandler中的getResult()方法,如下:
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    if (propertyMapping.getNestedQueryId() != null) {
      return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    } else if (propertyMapping.getResultSet() != null) {
      addPendingChildRelation(rs, metaResultObject, propertyMapping);   // TODO is that OK?
      return DEFERRED;
    } else {
      final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      //執行typeHandler中的方法獲取結果並且轉換為對應的Java型別
      return typeHandler.getResult(rs, column);
    }
  }

總結

總結

  • 本文詳細的介紹了TypeHandler在Mybatis中的應用、自定義使用以及從原始碼角度分析了型別處理器的執行流程,如果覺得作者寫的不錯,有所收穫的話,不妨點點關注,分享一波。

相關文章