3.4 typeHandler 类型处理器
MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,或者从结果集(ResultSet)中取出一个值时,都会用注册了的 typeHandler 进行处理。
由于数据库可能来自于不同的厂商,不同的厂商设置的参数可能有所不同,同时数据库也可以自定义数据类型,typeHandler 允许根据项目的需要自定义设置 Java 传递到数据库的参数中,或者从数据库读出数据,我们也需要进行特殊的处理,这些都可以在自定义的 typeHandler 中处理,尤其是在使用枚举的时候我们常常需要使用 typeHandler 进行转换。
typeHandler 和别名一样,分为 MyBatis 系统定义和用户自定义两种。一般来说,使用 MyBatis 系统定义就可以实现大部分的功能,如果使用用户自定义的 typeHandler,我们在处理的时候务必小心谨慎,以避免出现不必要的错误。
typeHandler 常用的配置为 Java 类型(javaType)、JDBC 类型(jdbcType)。typeHandler 的作用就是将参数从 javaType 转化为 jdbcType,或者从数据库取出结果时把 jdbcType 转化为 javaType。
3.4.1 系统定义的 typeHandler
MyBatis 系统内部定义了一系列的 typeHandler,如代码清单 3-11 所示。我们可以在源码查看它们,让我们看看 org.apache.ibatis.type.TypeHandlerRegistry。
代码清单 3-11:MyBatis 系统定义的 typeHandler
public TypeHandlerRegistry() { register(Boolean.class, new BooleanTypeHandler()); register(boolean.class, new BooleanTypeHandler()); register(JdbcType.BOOLEAN, new BooleanTypeHandler()); register(JdbcType.BIT, new BooleanTypeHandler()); ...... // issue #273 register(Character.class, new CharacterTypeHandler()); register(char.class, new CharacterTypeHandler()); }
这便是系统为我们注册的 typeHandler。目前 MyBatis 为我们注册了多个 typeHander,让我们看看表 3-3,从而了解 typeHandler 对应的 Java 类型和 JDBC 类型。
表 3-3 系统注册的 typeHandler 简介
我们需要注意下面几点。
数值类型的精度,数据库 int、double、decimal 这些类型和 java 的精度、长度都是不一样的。
时间精度,取数据到日用 DateOnlyTypeHandler 即可,用到精度为秒的用 SqlTimestamp TypeHandler 等。
让我们选取一个 MyBatis 系统自定义的 typeHandler,并了解它的具体内容。我们可以看到 MyBatis 源码包 org.apache.ibatis.type 下面定义的 StringTypeHandler。StringTypeHandler 是一个最常用的 typeHandler,负责处理 String 类型,如代码清单 3-12 所示。
代码清单 3-12:StringTypeHandler.java
public class StringTypeHandler 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); } }
说明一下上面的代码。
StringTypeHandler 继承了 BaseTypeHandler。而 BaseTypeHandler 实现了接口 typeHandler,并且自己定义了 4 个抽象的方法。所以继承它的时候,正如本例一样需要实现其定义的 4 个抽象方法,这些方法已经在 StringTypeHandler 中用 @Override 注解注明了。
setParameter 是 PreparedStatement 对象设置参数,它允许我们自己填写变换规则。
getResult 则分为 ResultSet 用列名(columnName)或者使用列下标(columnIndex)来获取结果数据。其中还包括了用 CallableStatement(存储过程)获取结果及数据的方法。
3.4.2 自定义 typeHandler
一般而言,MyBatis 系统定义的 typeHandler 已经能够应付大部分的场景了,但是我们不能排除不够用的情况。首先需要明确两个问题:我们自定义的 typeHandler 需要处理什么类型?现有的 typeHandler 适合我们使用吗?我们需要特殊的处理 Java 的那些类型(JavaType)和对应处理数据库的那些类型(JdbcType),比如说字典项的枚举。
这里让我们重复覆盖一个字符串参数的 typeHandler,我们首先配置 XML 文件,确定我们需要处理什么类型的参数和结果,如代码清单 3-13 所示。
代码清单 3-13:注册自定义 typeHandler
<typeHandlers> <typeHandler jdbcType="VARCHAR" javaType="string" handler="com.learn.chapter3.typeHandler.MyStringTypeHandler"/> </typeHandlers>
上面定义的数据库类型为 VARCHAR 型。当 Java 的参数为 string 型的时候,我们将使用 MyStringTypeHandler 进行处理。但是只有这个配置 MyBatis 不会自动帮助你去使用这个 typeHandler 去转化,你需要更多的配置。
对于 MyStringTypeHandler 的要求是必须实现接口:org.apache.ibatis.type.TypeHandler,在 MyBatis 中,我们也可以继承 org.apache.ibatis.type.BaseTypeHandler 来实现,因为 BaseTypeHandler 已经实现了 typeHandler 接口。在自定义的 typeHandler 中我们看到了如何继承 BaseTypeHandler 来实现。现在我们用 typeHandler 接口来实现,如代码清单 3-14 所示。
代码清单 3-14:MyStringTypeHandler.java
@MappedTypes({String.class}) @MappedJdbcTypes(JdbcType.VARCHAR) public class MyStringTypeHandler implements TypeHandler<String> { private Logger log = Logger.getLogger(MyStringTypeHandler.class); @Override public void setParameter(PreparedStatement ps, int index, String value, JdbcType jt) throws SQLException { log.info("使用我的 TypeHandler"); ps.setString(index, value); } @Override public String getResult(ResultSet rs, String colName) throws SQLException { log.info("使用我的 TypeHandler,ResultSet 列名获取字符串"); return rs.getString(colName); } @Override public String getResult(ResultSet rs, int index) throws SQLException { log.info("使用我的 TypeHandler,ResultSet 下标获取字符串"); return rs.getString(index); } @Override public String getResult(CallableStatement cs, int index) throws SQLException { log.info("使用我的 TypeHandler,CallableStatement 下标获取字符串"); return cs.getString(index); } }
代码里涉及了使用预编译(PreparedStatement)设置参数,获取结果集的时候使用的方法,并且给出日志。
自定义 typeHandler 里用注解配置 JdbcType 和 JavaType。这两个注解是:
@MappedTypes 定义的是 JavaType 类型,可以指定哪些 Java 类型被拦截。
@MappedJdbcTypes 定义的是 JdbcType 类型,它需要满足枚举类 org.apache.ibatis.type.JdbcType 所列的枚举类型。
到了这里我们还不能测试,因为还需要去标识哪些参数或者结果类型去用 typeHandler 进行转换,在没有任何标识的情况下,MyBatis 是不会启用你定义的 typeHandler 进行转换结果的,所以还要给予对应的标识,比如配置 jdbcType 和 javaType,或者直接用 typeHandler 属性指定,因此我们需要修改映射器的 XML 配置。我们进行如下修改,如代码清单 3-15 所示。
代码清单 3-15:修改映射文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.learn.chapter3.mapper.RoleMapper"> <resultMap type="role" id="roleMap"> <!--定义结果类型转化器标识,才能使用类型转换器 --> <id column="id" property="id" javaType="long" jdbcType="BIGINT" /> <result column="role_name" property="roleName" javaType="string" jdbcType="VARCHAR" /> <result column="note" property="note" typeHandler="com.learn.chapter3.typeHandler.MyStringTypeHandler"/> </resultMap> <select id="getRole" parameterType="long" resultMap="roleMap"> select id, role_name, note from t_role where id = #{id} </select> <select id="findRole" parameterType="string" resultMap="roleMap"> select id, role_name, note from t_role where role_name like concat('%', #{roleName javaType=string, jdbcType=VARCHAR, typeHandler=com. learn.chapter3.typeHandler.MyStringTypeHandler}, '%') </select> <insert id="insertRole" parameterType="role"> insert into t_role(role_name, note) values (#{roleName}, #{note}) </insert> <delete id="deleteRole" parameterType="long"> delete from t_role where id = #{id} </delete> </mapper>
我们这里引入了 resultMap,它提供了映射规则,这里给了 3 种 typeHandler 的使用方法。
在配置文件里面配置,在结果集的 roleName 定义 jdbcType 和 javaType。只有定义的 jdbcType、javaType 和我们定义在配置里面的 typeHandler 是一致的,MyBatis 才能够知道用我们自定义的类型转化器进行转换。
映射集里面直接定义具体的 typeHandler,这样就不需要再在配置里面定义了。
在参数中制定 typeHandler,这样 MyBatis 就会用对应的 typeHandler 进行转换。这样也不需要在配置里面定义了。
做了以上的修改,我们运行一下 findRole 方法看看结果。
DEBUG 2016-02-01 17:52:18,862 org.apache.ibatis.datasource.pooled. Pooled DataSource: Created connection 1558712965. DEBUG 2016-02-01 17:52:18,863 org.apache.ibatis.transaction.jdbc. Jdbc Transaction: Setting autocommit to false on JDBC Connection [com.mysql.jdbc. JDBC4Connection@5ce81285] DEBUG 2016-02-01 17:52:18,867 org.apache.ibatis.logging.jdbc. BaseJdbc Logger: ==> Preparing: select id, role_name, note from t_role where role_name like concat('%', ?, '%') DEBUG 2016-02-01 17:52:18,906 org.apache.ibatis.logging.jdbc. BaseJdbc Logger: ==> Parameters: test(String) com.learn.chapter3.typeHandler.MyStringTypeHandler: 使用我的 TypeHandler,ResultSet 列名获取字符串 INFO 2016-02-01 17:52:18,938 com.learn.chapter3.typeHandler. MyString TypeHandler: 使用我的 TypeHandler,ResultSet 列名获取字符串 DEBUG 2016-02-01 17:52:18,938 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: <== Total: 1 1 DEBUG 2016-02-01 17:52:18,939 org.apache.ibatis.transaction.jdbc. Jdbc Transaction: Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5ce81285]
从结果看,程序已经正确运行了,我们定义的 typeHandler 已经启动。如果在配置 typeHandler 的时候也可以进行包配置,那么 MyBatis 就会扫描包里面的 typeHandler,以减少配置的工作,让我们看看如何配置,如代码清单 3-16 所示。
代码清单 3-16:通过扫描注册 typeHandler
<typeHandlers> <package name="com.learn.chapter3.typeHandler"/> </typeHandlers>
3.4.3 枚举类型 typeHandler
3.4.2 节给出了自定义 Java 类型的 typeHandler,但是在 MyBatis 中枚举类型的 typeHandler 则有自己特殊的规则,MyBatis 内部提供了两个转化枚举类型的 typeHandler 给我们使用。
org.apache.ibatis.type.EnumTypeHandler
org.apache.ibatis.type.EnumOrdinalTypeHandler
其中,EnumTypeHandler 是使用枚举字符串名称作为参数传递的,EnumOrdinalTypeHandler 是使用整数下标作为参数传递的。如果枚举和数据库字典项保持一致,我们使用它们就可以了。然而这两个枚举类型应用却不是那么广泛,更多的时候我们希望使用自定义的 typeHandler 处理它们。所以在这里我们也会谈及自定义的 typeHanlder 实现枚举映射。
下面以性别为例,讲述如何实现枚举类。现在我们有一个性别枚举,它定义了字典:男(male),女(female)。那么我们可以轻易得到一个枚举类,如代码清单 3-17 所示。
代码清单 3-17:Sex.java 性别枚举
package com.learn.chapter3.enums; public enum Sex { MALE(1, "男"), FEMALE(2, "女"); private int id; private String name; private Sex(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public static Sex getSex(int id) { if (id == 1) { return MALE; } else if (id == 2) { return FEMALE; } return null; } }
3.4.3.1 EnumOrdinalTypeHandler
在没有配置的时候,EnumOrdinalTypeHandler 是 MyBatis 默认的枚举类型的处理器。为了让 EnumOrdinalTypeHandler 能够处理它,我们在 MyBatis 做如下配置,如代码清单 3-18 所示。
代码清单 3-18:配置性别枚举
<typeHandlers> ...... <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.learn.chapter3.enums.Sex" /> ...... </typeHandlers>
这样当 MyBatis 遇到这个枚举就可以识别这个枚举了,然后我们给出 userMapper.xml,请注意代码清单 3-19 中加粗的代码。
代码清单 3-19:userMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.learn.chapter3.mapper.UserMapper"> <resultMap type="com.learn.chapter3.po.User" id="userMap"> <id column="id" property="id" javaType="long" jdbcType="BIGINT" /> <result column="user_name" property="userName"/> <result column="cnname" property="cnname"/> <result column="birthday" property="birthday" /> <result column="sex" property="sex" typeHandler="org. apache.ibatis.type.EnumOrdinalTypeHandler"/> <result column="email" property="email" /> <result column="mobile" property="mobile" /> <result column="note" property="note" /> </resultMap> <select id="getUser" parameterType="long" resultMap="userMap"> select id, user_name, cnname, birthday, sex, email, mobile, note from t_user where id = #{id} </select> <insert parameterType="com.learn.chapter3.po.User" id="insertUser"> insert into t_user(user_name, cnname, birthday, sex, email, mobile, note) values(#{userName}, #{cnname}, #{birthday}, #{sex, typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler}, #{email}, #{mobile}, #{note}) </insert> </mapper>
为了测试,我们需要一个接口,如代码清单 3-20 所示。
代码清单 3-20:UserMapper.java
package com.learn.chapter3.mapper; import com.learn.chapter3.po.User; public interface UserMapper { public User getUser(Long id); public int insertUser(User user); }
我们要确保引入这个映射器到 MyBatis 上下文中,请参考 roleMapper.xml 的引入方法,然后就可以进行测试了,如代码清单 3-21 所示。
代码清单 3-21:测试 EnumOrdinalTypeHandler
public static void testEnumOrdinalTypeHandler() { SqlSession sqlSession = null; try { sqlSession = SqlSessionFactoryUtil.openSqlSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = new User(); user.setUserName("zhangsan"); user.setCnname("张三"); user.setMobile("18888888888"); user.setSex(Sex.MALE); user.setEmail("zhangsan@163.com"); user.setNote("test EnumOrdinalTypeHandler!!"); user.setBirthday(new Date()); userMapper.insertUser(user); User user2 = userMapper.getUser(1L); System.out.println(user2.getSex()); sqlSession.commit(); } catch(Exception ex) { System.err.println(ex.getMessage()); sqlSession.rollback(); } finally { if (sqlSession != null) { sqlSession.close(); } } }
让我们看看结果。
...... DEBUG 2016-02-03 11:24:20,347 org.apache.ibatis.datasource.pooled. PooledDataSource: Created connection 1815546035. DEBUG 2016-02-03 11:24:20,347 org.apache.ibatis.transaction.jdbc. JdbcTransaction: Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6c3708b3] DEBUG 2016-02-03 11:24:20,347 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==> Preparing: insert into t_user(user_name, cnname, birthday, sex, email, mobile, note) values(?, ?, ?, ?, ?, ?, ?) DEBUG 2016-02-03 11:24:20,407 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==> Parameters: zhangsan(String), 张三(String), 2016-02-03 11:24:20.087 (Timestamp),0(Integer),zhangsan@163.com (String), 18888888888(String), test EnumOrdinalTypeHandler!!(String) DEBUG 2016-02-03 11:24:20,407 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Updates: 1 DEBUG 2016-02-03 11:24:20,407 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: select id, user_name, cnname, birthday, sex, email, mobile, note from t_user where id = ? DEBUG 2016-02-03 11:24:20,407 org.apache.ibatis.logging.jdbc.BaseJdbc Logger: ==> Parameters: 1(Long) DEBUG 2016-02-03 11:24:20,427 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Total: 1 MALE DEBUG 2016-02-03 11:24:20,427 org.apache.ibatis.transaction.jdbc.JdbcTransaction: Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@6c3708b3] DEBUG 2016-02-03 11:24:20,447 org.apache.ibatis.transaction.jdbc.JdbcTransaction: Resetting autocommit to true on JDBC Connection [com.mysql.jdbc. JDBC4Connection@6c3708b3] DEBUG 2016-02-03 11:24:20,447 org.apache.ibatis.transaction.jdbc.JdbcTransaction: Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@6c3708b3] ......
运行成功了,我们看看数据库中的枚举插入结果,如图 3-1 所示。
图 3-1 枚举插入结果
我们发现它插入的是枚举定义的下标,而取出也是根据下标进行转化的。
3.4.3.2 EnumTypeHandler
EnumTypeHandler 是使用枚举名称去处理 Java 枚举类型。EnumTypeHandler 对应的是一个字符串,让我们来看看它的用法。
首先定义一个字符串,VARCHAR 型的字典项,例如将 3.4.3.1 节的性别(sex)修改为 VARCHAR 型,然后修改映射 XML 文件。这时我们在映射文件里面做了全部的限定描述(javaType、jdbcType、typeHandler 全配置),这样就不需要在 MyBatis 配置文件里再进行配置了,如代码清单 3-22 所示。
代码清单 3-22:使用 EnumTypeHandler 做转换枚举
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.learn.chapter3.mapper.UserMapper"> <resultMap type="com.learn.chapter3.po.User" id="userMap"> <id column="id" property="id" javaType="long" jdbcType="BIGINT" /> <result column="user_name" property="userName"/> <result column="cnname" property="cnname"/> <result column="birthday" property="birthday" /> <result column="sex" property="sex" typeHandler="org.apache.ibatis.type. EnumTypeHandler"/> <result column="email" property="email" /> <result column="mobile" property="mobile" /> <result column="note" property="note" /> </resultMap> <select id="getUser" parameterType="long" resultMap="userMap"> select id, user_name, cnname, birthday, sex, email, mobile, note from t_user where id = #{id} </select> <insert parameterType="com.learn.chapter3.po.User" id="insertUser"> insert into t_user(user_name, cnname, birthday, sex, email, mobile, note) values(#{userName}, #{cnname}, #{birthday}, #{sex, typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler}, #{email}, #{mobile}, #{note}) </insert> </mapper>
然后把 POJO 的属性 sex 从整数型修改为 String 型就可以了。EnumTypeHandler 是通过 Enum.name 方法将其转化为字符串,通过 Enum.valueOf 方法将字符串转化为枚举的。
当我们做了这样的修改后,插入的结果如图 3-2 所示。
图 3-2 EnumTypeHandler 转化枚举
3.4.3.3 自定义枚举类的 typeHandler
在大部分的情况下我们都不想使用系统的枚举 typeHandler 而是采用自定义。如果下标和名称往往都不是我们想要的结果,那么我们就可以参考自定义的 typeHandler 来定义其映射关系的规则。首先,我们增加配置,如代码清单 3-23 所示。
代码清单 3-23:增加自定义 typeHandler 的定义
<typeHandler handler="com.learn.chapter3.typeHandler.SexEnumTypeHandler" javaType="com.learn.chapter3.enums.Sex" />
然后,给出 SexEnumTypeHandler 的定义,如代码清单 3-24 所示。
代码清单 3-24:SexEnumTypeHandler.java
package com.learn.chapter3.typeHandler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; import com.learn.chapter3.enums.Sex; public class SexEnumTypeHandler implements TypeHandler<Sex> { @Override public Sex getResult(ResultSet rs, String name) throws SQLException { int id = rs.getInt(name); return Sex.getSex(id); } @Override public Sex getResult(ResultSet rs, int index) throws SQLException { int id = rs.getInt(index); return Sex.getSex(id); } @Override public Sex getResult(CallableStatement cs, int index) throws SQLException { int id = cs.getInt(index); return Sex.getSex(id); } @Override public void setParameter(PreparedStatement ps, int index, Sex sex, JdbcType jdbcType) throws SQLException { ps.setInt(index, sex.getId()); } }
最后,把代码清单 3-22 修改为我们定义的 SexEnumTypeHandler 即可运行程序了,这样自定义枚举就可以和数据库的字典项目对应起来了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论