微服务mybatis typehandler使用详解(就这一篇够了)
作者:Crystalqy
Mybatis TypeHandler类型转换器是负责Java类和jdbc
类型之间的转换
主要涉及到下面这几个类:
TypeHandler
类型转换器的顶层接口BaseTypeHandler
抽象类继承自TypeHandler
,Mybatis
中所有的类型转换器实现均继承他。TypeHandlerRegistry
类型转换器注册器,负责存储类型转换器。TypeAliasRegistry
类型别名转换器,用来存储类型与别名的对应关系。
1 TypeHandler
TypeHandler
是类型转换器的顶层接口,其定义了类型转换器应该具有的功能,
TypeHandler主要解决了两个问题:
- 可以指定我们在Java实体类所包含的自定义类型存入数据库后的类型是什么
- 从数据库中取出该数据后自动转换为我们自定义的Java类型
其源码如下:
public interface TypeHandler<T> { /** * 用于定义在Mybatis设置参数时该如何把Java类型的参数转换为对应的数据库类型 * @param ps 当前的PreparedStatement对象 * @param i 当前参数的位置 * @param parameter 当前参数的Java对象 * @param jdbcType 当前参数的数据库类型 * @throws SQLException */ void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; /** * 用于在Mybatis获取数据结果集时如何把数据库类型转换为对应的Java类型 * @param rs 当前的结果集 * @param columnName 当前的字段名称 * @return 转换后的Java对象 * @throws SQLException */ T getResult(ResultSet rs, String columnName) throws SQLException; /** * 用于在Mybatis通过字段位置获取字段数据时把数据库类型转换为对应的Java类型 * @param rs 当前的结果集 * @param columnIndex 当前字段的位置 * @return 转换后的Java对象 * @throws SQLException */ T getResult(ResultSet rs, int columnIndex) throws SQLException; /** * 用于Mybatis在调用存储过程后把数据库类型的数据转换为对应的Java类型 * @param cs 当前的CallableStatement执行后的CallableStatement * @param columnIndex 当前输出参数的位置 * @return * @throws SQLException */ T getResult(CallableStatement cs, int columnIndex) throws SQLException; }
2 BaseTypeHandler
BaseTypeHandler
是一个抽象类,改类实现了TypeHandler
中的方法并实现了异常捕获。继承改类我们可以很容易的实现一个自定义类型转换器,其源码如下:
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> { protected Configuration configuration; public void setConfiguration(Configuration c) { this.configuration = c; } public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { if (parameter == null) { if (jdbcType == null) { throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters."); } try { ps.setNull(i, jdbcType.TYPE_CODE); } catch (SQLException e) { throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " + "Cause: " + e, e); } } else { setNonNullParameter(ps, i, parameter, jdbcType); } } public T getResult(ResultSet rs, String columnName) throws SQLException { T result = getNullableResult(rs, columnName); if (rs.wasNull()) { return null; } else { return result; } } public T getResult(ResultSet rs, int columnIndex) throws SQLException { T result = getNullableResult(rs, columnIndex); if (rs.wasNull()) { return null; } else { return result; } } public T getResult(CallableStatement cs, int columnIndex) throws SQLException { T result = getNullableResult(cs, columnIndex); if (cs.wasNull()) { return null; } else { return result; } } public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException; public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException; public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException; }
我们可以看到BaseTypeHandler对TypeHandler接口的四个方法做了一个简单的选择,把null值的情况都做了一个过滤,核心的取值和设值的方法还是抽象出来了供子类来实现。使用BaseTypeHandler还有一个好处是它继承了另外一个叫做TypeReference的抽象类,通过TypeReference的getRawType()方法可以获取到当前TypeHandler所使用泛型的原始类型。这对Mybatis在注册TypeHandler的时候是非常有好处的。在没有指定javaType的情况下,Mybatis在注册TypeHandler时可以通过它来获取当前TypeHandler所使用泛型的原始类型作为要注册的TypeHandler的javaType类型,这个在讲到Mybatis注册TypeHandler的方式时将讲到。
3 注册TypeHandler
为什么Java自带的类型在存取的时候不会出错,我们自定义的类型就会出错?那是因为mybatis已经将这些类型的TypeHandler提前写好了,并且注册好了
具体注册了哪些,我们可以看TypeHandlerRegistry这个类:
public TypeHandlerRegistry(Configuration configuration) { this.unknownTypeHandler = new UnknownTypeHandler(configuration); register(Boolean.class, new BooleanTypeHandler()); register(boolean.class, new BooleanTypeHandler()); register(JdbcType.BOOLEAN, new BooleanTypeHandler()); register(JdbcType.BIT, new BooleanTypeHandler()); register(Byte.class, new ByteTypeHandler()); register(byte.class, new ByteTypeHandler()); register(JdbcType.TINYINT, new ByteTypeHandler()); register(Short.class, new ShortTypeHandler()); register(short.class, new ShortTypeHandler()); register(JdbcType.SMALLINT, new ShortTypeHandler()); register(Integer.class, new IntegerTypeHandler()); register(int.class, new IntegerTypeHandler()); register(JdbcType.INTEGER, new IntegerTypeHandler()); register(Long.class, new LongTypeHandler()); register(long.class, new LongTypeHandler()); register(Float.class, new FloatTypeHandler()); register(float.class, new FloatTypeHandler()); register(JdbcType.FLOAT, new FloatTypeHandler()); register(Double.class, new DoubleTypeHandler()); register(double.class, new DoubleTypeHandler()); register(JdbcType.DOUBLE, new DoubleTypeHandler()); register(Reader.class, new ClobReaderTypeHandler()); register(String.class, new StringTypeHandler()); register(String.class, JdbcType.CHAR, new StringTypeHandler()); register(String.class, JdbcType.CLOB, new ClobTypeHandler()); register(String.class, JdbcType.VARCHAR, new StringTypeHandler()); register(String.class, JdbcType.LONGVARCHAR, new StringTypeHandler()); register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler()); register(String.class, JdbcType.NCHAR, new NStringTypeHandler()); register(String.class, JdbcType.NCLOB, new NClobTypeHandler()); register(JdbcType.CHAR, new StringTypeHandler()); register(JdbcType.VARCHAR, new StringTypeHandler()); register(JdbcType.CLOB, new ClobTypeHandler()); register(JdbcType.LONGVARCHAR, new StringTypeHandler()); register(JdbcType.NVARCHAR, new NStringTypeHandler()); register(JdbcType.NCHAR, new NStringTypeHandler()); register(JdbcType.NCLOB, new NClobTypeHandler()); register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler()); register(JdbcType.ARRAY, new ArrayTypeHandler()); register(BigInteger.class, new BigIntegerTypeHandler()); register(JdbcType.BIGINT, new LongTypeHandler()); register(BigDecimal.class, new BigDecimalTypeHandler()); register(JdbcType.REAL, new BigDecimalTypeHandler()); register(JdbcType.DECIMAL, new BigDecimalTypeHandler()); register(JdbcType.NUMERIC, new BigDecimalTypeHandler()); register(InputStream.class, new BlobInputStreamTypeHandler()); register(Byte[].class, new ByteObjectArrayTypeHandler()); register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler()); register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler()); register(byte[].class, new ByteArrayTypeHandler()); register(byte[].class, JdbcType.BLOB, new BlobTypeHandler()); register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler()); register(JdbcType.LONGVARBINARY, new BlobTypeHandler()); register(JdbcType.BLOB, new BlobTypeHandler()); register(Object.class, unknownTypeHandler); register(Object.class, JdbcType.OTHER, unknownTypeHandler); register(JdbcType.OTHER, unknownTypeHandler); register(Date.class, new DateTypeHandler()); register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler()); register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler()); register(JdbcType.TIMESTAMP, new DateTypeHandler()); register(JdbcType.DATE, new DateOnlyTypeHandler()); register(JdbcType.TIME, new TimeOnlyTypeHandler()); register(java.sql.Date.class, new SqlDateTypeHandler()); register(java.sql.Time.class, new SqlTimeTypeHandler()); register(java.sql.Timestamp.class, new SqlTimestampTypeHandler()); register(String.class, JdbcType.SQLXML, new SqlxmlTypeHandler()); register(Instant.class, new InstantTypeHandler()); register(LocalDateTime.class, new LocalDateTimeTypeHandler()); register(LocalDate.class, new LocalDateTypeHandler()); register(LocalTime.class, new LocalTimeTypeHandler()); register(OffsetDateTime.class, new OffsetDateTimeTypeHandler()); register(OffsetTime.class, new OffsetTimeTypeHandler()); register(ZonedDateTime.class, new ZonedDateTimeTypeHandler()); register(Month.class, new MonthTypeHandler()); register(Year.class, new YearTypeHandler()); register(YearMonth.class, new YearMonthTypeHandler()); register(JapaneseDate.class, new JapaneseDateTypeHandler()); // issue #273 register(Character.class, new CharacterTypeHandler()); register(char.class, new CharacterTypeHandler()); }
4. 实现自定义的TypeHandler
我们可以直接继承BaseTypeHandler来实现我们自己的类型转换器
我们要实现把json字符串转换成为我们的对象,可以这么实现:
public class JsonTypeHandler<T> extends BaseTypeHandler<T> { private final Class<T> type; public JsonTypeHandler(Class<T> type) { if (type == null) { throw new IllegalArgumentException("输入参数不能为空"); } this.type = type; } @Override public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { try { String json = JsonUtil.toJson(parameter); ps.setString(i, json); } catch (Exception e) { throw new RuntimeException("Json对象转换成String失败", e); } } @Override public T getNullableResult(ResultSet rs, String columnName) throws SQLException { String json = rs.getString(columnName); return convertJsonToObject(json); } @Override public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String json = rs.getString(columnIndex); return convertJsonToObject(json); } @Override public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String json = cs.getString(columnIndex); return convertJsonToObject(json); } private T convertJsonToObject(String json) { try { return JsonUtil.fromJson(json, type); } catch (Exception e) { throw new RuntimeException("json转换成Java对象失败", e); } } }
因为我们使用的是Spring boot工程,只需要把JsonTypeHandler放到Spring boot可以扫描的目录下即可。
在XML中使用:
<resultMap id="BaseResultMap" type="com.db.model.SettlementBill"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="receipts_code" jdbcType="VARCHAR" property="receiptsCode" /> <result column="finance_info" jdbcType="VARCHAR" property="financeDetailVO1" javaType="com.db.model.SettlementBill" typeHandler="com.mybatis.handler.JsonArrayTypeHandler" /> </resultMap> <select id="test" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select id, receipts_code, finance_info from settlement_bill where id = #{id,jdbcType=INTEGER} </select>
Mybatis Plus中的使用方式:
@TableName(value = "settlement_bill", autoResultMap = true) @Data public class SettlementBill extends CxmBaseModel implements Serializable { @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField(value = "finance_info",typeHandler = JsonTypeHandler.class) private BatchFinanceDetailVO1 financeDetailVO1; }
5. Mybatis自动获取TypeHandle
在介绍了Mybatis是如何注册TypeHandler之后就介绍一下Mybatis是如何获取对应的TypeHandler进行类型转换的。
如果我们在Mapper.xml文件中配置某一个属性或变量的映射关系时指定了该属性对应的javaType和jdbcType,则Mybatis会从注册好的TypeHandler中寻找对应的javaType和jdbcType组合的TypeHandler进行处理,这也是Mybatis最基本的获取TypeHandler进行类型转换的方式。
1.javaType和jdbcType都指定
假设Mybatis配置文件中有这么一段TypeHandler的注册信息:
<typeHandlers> <typeHandler handler="com.tiantian.mybatis.handler.StringArrayTypeHandler" javaType="[Ljava.lang.String;" jdbcType="VARCHAR"/> </typeHandlers>
看这样一个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.tiantian.mybatis.mapper.UserMapper"> <resultMap id="UserResult" type="User"> <id column="id" property="id"/> <result column="interests" property="interests" javaType="[Ljava.lang.String;" jdbcType="VARCHAR"/> </resultMap> <insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyColumn="id"> insert into t_user(name, age, interests) values(#{name}, #{age}, #{interests, javaType=[Ljava.lang.String;, jdbcType=VARCHAR}) </insert> <update id="updateUser" parameterType="User"> update t_user set name=#{name}, age=#{age}, interests=#{interests} where id=#{id} </update> <select id="findById" parameterType="int" resultMap="UserResult"> select * from t_user where id=#{id} </select> <delete id="deleteUser" parameterType="int"> delete from t_user where id=#{id} </delete> </mapper>
我们可以看到在id为UserResult的resultMap中,我们定义了一个对应字段interests的映射关系,并且定义了其javaType为“[Ljava.lang.String;”,jdbcType为VARCHAR,这个时候Mybatis就会到已经注册了的TypeHandler中寻找到能处理javaType和jdbcType对应的类型转换的TypeHandler来进行处理。在这里就会找到我们注册的StringArrayTypeHandler。在上面id为insertUser的insert语句中,我们也为变量interests指定了它的javaType和jdbcType属性,这时候Mybatis也会寻找javaType和jdbcType对应的TypeHandler。上面这样定义是Mybatis最基本也是最完整地获取到对应的TypeHandler的方法。
2. 只指定变量对应的javaType类型。
这个时候Mybatis会拿着这个javaType和jdbcType为null的组合到注册的TypeHandler中寻找对应的TypeHandler。
(1)不动StringArrayTypeHandler的注册信息,把我们的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.tiantian.mybatis.mapper.UserMapper"> <resultMap id="UserResult" type="User"> <id column="id" property="id"/> <result column="interests" property="interests" javaType="[Ljava.lang.String;"/> </resultMap> <select id="findById" parameterType="int" resultMap="UserResult"> select * from t_user where id=#{id} </select> </mapper>
此时我们发现最终执行的结果interests为null,这说明Mybatis没有使用我们定义的StringArrayTypeHandler来转换interests。
(2)UserMapper.xml还像上面那样定义,但是也只指定javaType属性来注册我们的StringArrayTypeHandler,代码如下:
<typeHandlers> <typeHandler handler="com.tiantian.mybatis.handler.StringArrayTypeHandler" javaType="[Ljava.lang.String;"/> </typeHandlers>
这个时候再运行上面的测试代码,输出结果发现interests转换为User类字符串数组类型的interests属性。
这是因为我们是以javaType和null注册的StringArrayTypeHandler,然后在需要转换interests时又是以相同的javaType和null来寻找的,所以就会找到我们注册的StringArrayTypeHandler来进行类型转换。
3. 只指定变量对应的jdbcType类型
这个时候Mybatis会利用我们指定的返回类型和对应的属性取该属性在返回类型中对应的javaType,之后再拿着该javaType和我们指定的jdbcType到注册的TypeHandler中获取对应的TypeHandler。
保持之前指定javaType和jdbcType的方式注册StringArrayTypeHandler,然后在定义interests变量的时候不指定javaType,只指定jdbcType,这个时候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.tiantian.mybatis.mapper.UserMapper"> <resultMap id="UserResult" type="User"> <id column="id" property="id"/> <result column="interests" property="interests" jdbcType="VARCHAR"/> </resultMap> <select id="findById" parameterType="int" resultMap="UserResult"> select * from t_user where id=#{id} </select> </mapper>
这个时候Mybatis是这样获取TypeHandler的:首先它发现我们的interests没有指定javaType,这个时候它就会通过我们指定的类型User和属性interests获取User类的interests属性对应的java类型,即String数组,再拿着获取到的javaType和我们指定的jdbcType即VARCHAR去寻找对应的TypeHandler,这个时候就找到了我们之前以String数组和VARCHAR注册好的StringArrayTypeHandler来处理interests的类型转换。
4. javaType类型和jdbcType类型都不指定
这个时候Mybatis会以方式2中的方式获取到对应的javaType类型,然后再以方式1获取到对应的TypeHandler。
首先,注册一个javaType为String数组,jdbcType不指定即为null的TypeHandler—StringArrayTypeHandler,代码如下:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="config/jdbc.properties"></properties> <typeAliases> <package name="com.tiantian.mybatis.model"/> </typeAliases> <typeHandlers> <typeHandler handler="com.tiantian.mybatis.handler.StringArrayTypeHandler" javaType="[Ljava.lang.String;"/> </typeHandlers> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> <mappers> <mapper resource="com/tiantian/mybatis/mapper/UserMapper.xml"/> </mappers> </configuration>
然后,定义我们的interests字段的映射关系时既不指定javaType,又不指定jdbcType,代码如下:
<?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.tiantian.mybatis.mapper.UserMapper"> <resultMap id="UserResult" type="User"> <id column="id" property="id"/> <result column="interests" property="interests"/> </resultMap> <select id="findById" parameterType="int" resultMap="UserResult"> select * from t_user where id=#{id} </select> </mapper>
此时我们以javaType为String数组和jdbcType为null注册了一个StringArrayTypeHandler,然后在定义interests字段的映射关系时我们没有指明其对应的javaType和jdbcType,这个时候Mybatis会利用我们指定的User类型和interests属性获取到User类的interests属性对应的java类型,即String数组,然后结合jdbcType为null去寻找注册的TypeHandler,这样就找到了StringArrayTypeHandler。经StringArrayTypeHandler的处理就把jdbcType为VARCHAR的数据转换为javaType为String数组的数据。
5. 直接通过变量的typeHandler属性指定其对应的TypeHandler
还有一种形式是我们直接通过变量的typeHandler属性指定其对应的TypeHandler,这个时候Mybatis就会使用该用户自己指定的TypeHandler来进行类型转换,而不再以javaType和jdbcType组合的方式获取对应的TypeHandler。
首先在Mybatis的配置文件中以javaType和jdbcType配套的方式注册一个StringArrayTypeHandler,代码如下:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="config/jdbc.properties"></properties> <typeAliases> <package name="com.tiantian.mybatis.model"/> </typeAliases> <typeHandlers> <typeHandler handler="com.tiantian.mybatis.handler.StringArrayTypeHandler" javaType="[Ljava.lang.String;" jdbcType="VARCHAR"/> </typeHandlers> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> <mappers> <mapper resource="com/tiantian/mybatis/mapper/UserMapper.xml"/> </mappers> </configuration>
按照前面说的Mybatis按照变量的javaType和jdbcType来取对应的TypeHandler的话,这里注册的StringArrayTypeHandler只有在指定变量的javaType为字符串数组而jdbcType为VARCHAR的情况下才能被获取到。
然后我们在UserMapper.xml文件中不指定interests字段对应的javaType和jdbcType,但是通过typeHandler属性指定将以StringArrayTypeHandler来进行类型转换,代码如下:
<?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.tiantian.mybatis.mapper.UserMapper"> <resultMap id="UserResult" type="User"> <id column="id" property="id"/> <result column="interests" property="interests" typeHandler="com.tiantian.mybatis.handler.StringArrayTypeHandler"/> </resultMap> <select id="findById" parameterType="int" resultMap="UserResult"> select * from t_user where id=#{id} </select> </mapper>
这是因为我们指定了进行interests字段的映射关系时使用StringArrayTypeHandler来进行类型转换。当指定了某一个字段或变量进行映射关系时所使用的TypeHandler时,Mybatis在需要进行类型转换时就使用给定的TypeHandler进行类型转换,而不会再通过javaType和jdbcType的组合去注册好的TypeHandler中寻找对应的TypeHandler。