Mybatis TypeHandler

本文对Mybatis的TypeHandler进行研究并结合实际经历来更好理解。本文参考文章 TypeHandler源码分析 Date类型问题 源码分析


Mybatis作为ORM,最重要的功能就是将Java中的对象跟数据库中的表关联起来,需要解决Java数据类型和DB数据类型的转换。

Whenever MyBatis sets a parameter on a PreparedStatement or retrieves a value from a ResultSet, a TypeHandler is used to retrieve the value in a means appropriate to the Java type.

对于数据类型的转换,Mybatis提供了很多默认的TypeHandler。其类图结构如下:

TypeHandler接口提供一个set方法和3个get方法,set方法将Java数据类型转化为JDBC类型,get方法将JDBC类型转化为Java类型,基于ResultSet的column name/index和基于CallableStatement的column index。

BaseTypeHandler为抽象类,初步实现接口TypeHandler,并提供了4个处理null(一个set,3个get)的函数。

DateTypeHandler实现抽象类中提供的处理null的4个函数,用来对日期进行处理。

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setTimestamp(i, new Timestamp((parameter).getTime()));
  }

  @Override
  public Date getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    Timestamp sqlTimestamp = rs.getTimestamp(columnName);
    if (sqlTimestamp != null) {
      return new Date(sqlTimestamp.getTime());
    }
    return null;
  }

  @Override
  public Date getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    Timestamp sqlTimestamp = rs.getTimestamp(columnIndex);
    if (sqlTimestamp != null) {
      return new Date(sqlTimestamp.getTime());
    }
    return null;
  }

  @Override
  public Date getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    Timestamp sqlTimestamp = cs.getTimestamp(columnIndex);
    if (sqlTimestamp != null) {
      return new Date(sqlTimestamp.getTime());
    }
    return null;
  }

DateTypeHandler的set方法调用Date类型参数的getTime()函数将时间转化为毫秒数,然后将毫秒数转化为Timestamp类型。所以Java Date类型会被转化为JDBC Timestamp类型传递给DB。get方法从DB中获得数据转换为JDBC Timestamp类型,调用其getTime()方法转化为毫秒数,然后将毫秒数转化为Java Date类型。

导致问题:

对于java.util.Date数据类型,在TypeHandlerRegistry中默认注册的TypeHandler为DateTypeHnadler。这样java Date数据类型就被转换为JDBC Timestamp。而我们在Oracle DB中定义的column类型为Date,并且用该column定义index。由于where Date = Timestamp,oracle会隐式的将左边的Date类型调用函数转化为Timestamp类型,即where to_timestamp(Date) = Timestamp, 从而导致index失效,查询效率变低。

解决方法:

  1. 指定typeHandler为DateOnlyTypeHnadler
  2. 指定jdbcType为DATE

上面2种方法会将时间截取时分秒,只留年月日,出现精度问题。这时,使用精确时间查询会出现问题。

Note: 其实主要问题是java.util.Date为高精度是时间(represents a specific instant
in time, with millisecond precision),而java.sql.Date为低精度的时间,只有年月日(To conform with the definition of SQL <code>DATE</code>, the millisecond values wrapped by a <code>java.sql.Date</code> instance must be 'normalized' by setting the hours, minutes, seconds, and milliseconds to zero in the particular time zone with which the instance is associated.)。

不影响时间精度的方法有如下3种:

1.将数据库column类型改为Timestamp;

2. 将java数据类型改为String,然后在SQL中将String转化为Date: where Date=to_date(#{String}, ''YYYY-MM-DD HH24:MI:SS)

3. 将Timestamp+0会自动转化为Date: where Date = #{Timestamp}+0

4. 实现自己的TypeHandler来处理类型为Date的情况,具体方法也是将Date转化为String,并类似方法2在SQL中将String转化为Date类型。

方法1在我们只需要Date类型时不适用;方法2将程序中Date显式转化为String感觉怪怪的;方法3比较简单,改动比较少,但感觉有点tricky。

方法4提供TypeHandler来处理Date类型,为比较标准的方法。实现的TypeHandler只需要修改DateTypeHandler的set方法,get方法完全相同。set方法将Date类型转化为String,然后在SQL中使用时,将String转化为JDBC Date类型,这样,对使用接口的人是透明的,提供参数Date,而且不会进行类型转化,不影响性能。

实现TypeHandler:

public class DateTimeTypeHandler extends BaseTypeHandler<Date> {  
public void setNonNullParameter(PreparedStatement ps, int i,  
        Date parameter, JdbcType jdbcType) throws SQLException {  
ps.setString(i, dateToString(parameter));  
}  
public Date getNullableResult(ResultSet rs, String columnName)  
        throws SQLException {  
java.sql.Timestamp sqlTimestamp = rs.getTimestamp(columnName);  
if (sqlTimestamp != null) {  
return new java.util.Date(sqlTimestamp.getTime());  
}  
return null;  
}  
public Date getNullableResult(CallableStatement cs, int columnIndex)  
        throws SQLException {  
java.sql.Timestamp sqlTimestamp = cs.getTimestamp(columnIndex);  
if (sqlTimestamp != null) {  
return new java.util.Date(sqlTimestamp.getTime());  
}  
return null;  
}  
public String dateToString(Date date){  
SimpleDateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:MM:ss");  
return df.format(date);  
}  
}  

在SQL中使用handler:

where create_time >= to_date(
#{v_time,typeHandler=com.mybatisTypehandle.DateTimeTypeHandler},'yyyy-mm-dd hh24:mi:ss')

我们使用TypeHandler解决了我上面的问题。下面我们将对TypeHandler进行详细研究。

下面是一些TypeHandler的处理类型:

You can override the type handlers or create your own to deal with unsupported or non-standard types.

实现自己的TypeHandler,只需要实现实现接口TypeHandler或扩展BaseTypeHandler。上面,我们已经提供了实现TypeHandler的例子,这里不再累赘。

创了了TypeHandler之后,需要将其配置到Mybatis的配置文件中,让Mybatis能够识别并使用它。

2种方式:

1. 在Mybatis配置文件中定义typeHandlers元素的子元素typeHandler来注册;这种方式只能注册一个TypeHandler。同时可以使用javaType和jdbcType属性来指定处理的java type和jdbc type。

<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandle

2. 在Mybatis配置文件中定义typeHandlers元素的子元素package来注册;这种方式会将package下的所有TypeHandler识别出来。

<!-- mybatis-config.xml -->
<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

方法2设定javaType和jdbcType只能在TypeHandler上使用annotaton来实现。@MappedJdbcTypes({JdbcType.VARCHAR}) 和@MappedTypes({String[].class})

自动寻找Typehandler: 若在mapper.xml文件中提供了javaType和jdbcType,则Mybatis会从注册好的TypeHandler中寻找合适的handler进行处理。

编辑于 2018-09-02