通过前面的分析 ,我们可以看到MyBatis在进行参数设置与结果取值的时候,都应用到了各种TypeHandler。这里我们回顾一下设置参数与获取结果的代码。
为参数赋值如下图所示:
从结果取值之DefaultResultSetHandler.getPropertyMappingValue方法如下所示
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 DEFERED; } else { final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler(); final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); return typeHandler.getResult(rs, column); } }
【1】 类型处理器(typeHandlers)
MyBatis 在设置预处理语句(
PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型
。下表描述了一些默认的类型处理器。
提示 从 3.4.5 开始,MyBatis 默认支持 JSR-310(日期和时间 API) 。
类型处理器 | Java 类型 | JDBC 类型 |
BooleanTypeHandler | java.lang.Boolean, boolean | 数据库兼容的 BOOLEAN |
ByteTypeHandler | java.lang.Byte, byte | 数据库兼容的 NUMERIC 或 BYTE |
ShortTypeHandler | java.lang.Short, short | 数据库兼容的 NUMERIC 或 SMALLINT |
IntegerTypeHandler | java.lang.Integer, int | 数据库兼容的 NUMERIC 或 INTEGER |
LongTypeHandler | java.lang.Long, long | 数据库兼容的 NUMERIC 或 BIGINT |
FloatTypeHandler | java.lang.Float, float | 数据库兼容的 NUMERIC 或 FLOAT |
DoubleTypeHandler | java.lang.Double, double | 数据库兼容的 NUMERIC 或 DOUBLE |
BigDecimalTypeHandler | java.math.BigDecimal | 数据库兼容的 NUMERIC 或 DECIMAL |
StringTypeHandler | java.lang.String | CHAR, VARCHAR |
ClobReaderTypeHandler | java.io.Reader | - |
ClobTypeHandler | java.lang.String | CLOB, LONGVARCHAR |
NStringTypeHandler | java.lang.String | NVARCHAR, NCHAR |
NClobTypeHandler | java.lang.String | NCLOB |
BlobInputStreamTypeHandler | java.io.InputStream | - |
ByteArrayTypeHandler | byte[] | 数据库兼容的字节流类型 |
BlobTypeHandler | byte[] | BLOB, LONGVARBINARY |
DateTypeHandler | java.util.Date | TIMESTAMP |
DateOnlyTypeHandler | java.util.Date | DATE |
TimeOnlyTypeHandler | java.util.Date | TIME |
SqlTimestampTypeHandler | java.sql.Timestamp | TIMESTAMP |
SqlDateTypeHandler | java.sql.Date | DATE |
SqlTimeTypeHandler | java.sql.Time | TIME |
ObjectTypeHandler | Any | OTHER 或未指定类型 |
EnumTypeHandler | Enumeration Type | VARCHAR 或任何兼容的字符串类型,用来存储枚举的名称(而不是索引序数值) |
EnumOrdinalTypeHandler | Enumeration Type | 任何兼容的 NUMERIC 或 DOUBLE 类型,用来存储枚举的序数值(而不是名称)。 |
SqlxmlTypeHandler | java.lang.String | SQLXML |
InstantTypeHandler | java.time.Instant | TIMESTAMP |
LocalDateTimeTypeHandler | java.time.LocalDateTime | TIMESTAMP |
LocalDateTypeHandler | java.time.LocalDate | DATE |
LocalTimeTypeHandler | java.time.LocalTime | TIME |
OffsetDateTimeTypeHandler | java.time.OffsetDateTime | TIMESTAMP |
OffsetTimeTypeHandler | java.time.OffsetTime | TIME |
ZonedDateTimeTypeHandler | java.time.ZonedDateTime | TIMESTAMP |
YearTypeHandler | java.time.Year | INTEGER |
MonthTypeHandler | java.time.Month | INTEGER |
YearMonthTypeHandler | java.time.YearMonth | VARCHAR 或 LONGVARCHAR |
JapaneseDateTypeHandler | java.time.chrono.JapaneseDate | DATE |
你可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。
具体做法为:实现 org.apache.ibatis.type.TypeHandler
接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler
, 并且可以(可选地)将它映射到一个 JDBC 类型。
实例代码如下:
// ExampleTypeHandler.java @MappedJdbcTypes(JdbcType.VARCHAR) public class ExampleTypeHandler extends BaseTypeHandler<String> { @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); } } <!-- mybatis-config.xml --> <typeHandlers> <typeHandler handler="org.mybatis.example.ExampleTypeHandler" /> </typeHandlers> <!-- typeHandler指定JavaType jdbcType --> <typeHandler handler="org.mybatis.example.ExampleTypeHandler" javaType="java.lang.String" jdbcType="VARCHAR" />
使用上述的类型处理器将会覆盖已有的处理 Java String 类型的属性以及 VARCHAR 类型的参数和结果的类型处理器。
要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型, 以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。
通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型,不过这种行为可以通过两种方法改变:
在类型处理器的配置元素(typeHandler 元素)上增加一个 javaType 属性(比如:javaType="String");
在类型处理器的类上增加一个 @MappedTypes 注解指定与其关联的 Java 类型列表。 如果在 javaType 属性中也同时指定,则注解上的配置将被忽略。
可以通过两种方式来指定关联的 JDBC 类型:
在类型处理器的配置元素上增加一个 jdbcType 属性(比如:jdbcType="VARCHAR");
在类型处理器的类上增加一个 @MappedJdbcTypes 注解指定与其关联的 JDBC 类型列表。 如果在 jdbcType 属性中也同时指定,则注解上的配置将被忽略。
当在 ResultMap 中决定使用哪种类型处理器时,此时 Java 类型是已知的(从结果类型中获得),但是 JDBC 类型是未知的。 因此 Mybatis 使用 javaType=[Java 类型], jdbcType=null 的组合来选择一个类型处理器。
这意味着使用 @MappedJdbcTypes 注解可以限制类型处理器的作用范围,并且可以确保,除非显式地设置,否则类型处理器在 ResultMap 中将不会生效。 如果希望能在 ResultMap 中隐式地使用类型处理器,那么设置 @MappedJdbcTypes 注解的 includeNullJdbcType=true 即可。
然而从 Mybatis 3.4.0 开始,如果某个 Java 类型只有一个注册的类型处理器,即使没有设置 includeNullJdbcType=true,那么这个类型处理器也会是 ResultMap 使用 Java 类型时的默认处理器。
最后,可以让 MyBatis 帮你查找类型处理器:
<!-- mybatis-config.xml --> <typeHandlers> <package name="org.mybatis.example"/> </typeHandlers>
注意在使用自动发现功能的时候,只能通过注解方式来指定 JDBC 的类型。
你可以创建能够处理多个类的泛型类型处理器。为了使用泛型类型处理器, 需要增加一个接受该类的 class
作为参数的构造器,这样 MyBatis
会在构造一个类型处理器实例的时候传入一个具体的类。
//GenericTypeHandler.java public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> { private Class<E> type; public GenericTypeHandler(Class<E> type) { if (type == null) throw new IllegalArgumentException("Type argument cannot be null"); this.type = type; } //...
EnumTypeHandler 和 EnumOrdinalTypeHandler
都是泛型类型处理器,我们将会在接下来的部分详细探讨。
日期和时间的处理
JDK1.8以前一直是个头疼的问题。我们通常使用JSR310规范领导者Stephen Colebourne创建的Joda-Time来操作。1.8已经实现全部的JSR310规范了。日期时间处理上,我们可以使用MyBatis基于JSR310(Date and Time API)编写的各种日期时间类型处理器。MyBatis3.4以前的版本需要我们手动注册这些处理器,以后的版本都是自动注册的。
【2】实现TypeHandler的两种方式
① 继承BaseTypeHandler抽象类
BaseTypeHanler extends TypeReference<T> implements TypeHandler<T>
,我们只需要实现BaseTypeHanler 四个抽象方法即可。
如下所示,是mybatis提供的BigInteger类型转换。
public class BigIntegerTypeHandler extends BaseTypeHandler<BigInteger> { @Override public void setNonNullParameter(PreparedStatement ps, int i, BigInteger parameter, JdbcType jdbcType) throws SQLException { ps.setBigDecimal(i, new BigDecimal(parameter)); } @Override public BigInteger getNullableResult(ResultSet rs, String columnName) throws SQLException { BigDecimal bigDecimal = rs.getBigDecimal(columnName); return bigDecimal == null ? null : bigDecimal.toBigInteger(); } @Override public BigInteger getNullableResult(ResultSet rs, int columnIndex) throws SQLException { BigDecimal bigDecimal = rs.getBigDecimal(columnIndex); return bigDecimal == null ? null : bigDecimal.toBigInteger(); } @Override public BigInteger getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { BigDecimal bigDecimal = cs.getBigDecimal(columnIndex); return bigDecimal == null ? null : bigDecimal.toBigInteger(); } }
② 实现TypeHandler接口
TypeHandler接口如下所示,这里的T
表示参数类型。
public interface TypeHandler<T> { void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; T getResult(ResultSet rs, String columnName) throws SQLException; T getResult(ResultSet rs, int columnIndex) throws SQLException; T getResult(CallableStatement cs, int columnIndex) throws SQLException; }
自定义实现实例如下:
public class MyEnumEmpStatusTypeHandler implements TypeHandler<EmpStatus> { //定义当前数据如何保存到数据库中 @Override public void setParameter(PreparedStatement ps, int i, EmpStatus parameter, JdbcType jdbcType) throws SQLException { System.out.println("要保存的状态码:"+parameter.getCode()); ps.setString(i, parameter.getCode().toString()); } @Override public EmpStatus getResult(ResultSet rs, String columnName) throws SQLException { //需要根据从数据库中拿到的枚举的状态码返回一个枚举对象 int code = rs.getInt(columnName); System.out.println("从数据库中获取的状态码:"+code); EmpStatus status = EmpStatus.getEmpStatusByCode(code); return status; } @Override public EmpStatus getResult(ResultSet rs, int columnIndex) throws SQLException { int code = rs.getInt(columnIndex); System.out.println("从数据库中获取的状态码:"+code); EmpStatus status = EmpStatus.getEmpStatusByCode(code); return status; } @Override public EmpStatus getResult(CallableStatement cs, int columnIndex) throws SQLException { int code = cs.getInt(columnIndex); System.out.println("从数据库中获取的状态码:"+code); EmpStatus status = EmpStatus.getEmpStatusByCode(code); return status; } }
③ 配置TypeHandler
第一种,在全局配置文件配置
<typeHandlers> <typeHandler handler="com.mybatis.typehandler.MyEnumEmpStatusTypeHandler" javaType="com.mybatis.bean.EmpStatus"/> </typeHandlers>
第二种,结果映射时配置具体属性
<resultMap type="com.mybatis.bean.Employee" id="MyEmp"> <id column="id" property="id"/> <result column="empStatus" property="empStatus" typeHandler="XXXXX"/> </resultMap>
第三种,保存数据时具体参数配置
<insert id="addEmp" useGeneratedKeys="true" keyProperty="id"> insert into tbl_employee(last_name,email,gender,empStatus) values (#{lastName},#{email},#{gender},#{empStatus,typeHandler=xxxx}) </insert>
需要注意的是,如果在具体属性配置,请务必保证查询和保存配置的类型转换器一致。