Java / MyBatis 类型处理器

MyBatis 类型处理器

1. 前言

MyBatis 提供了诸多类型处理器,但是相较于丰富的数据库类型仍然略显不足,比如 MyBatis 只能将 JSON 数据类型当成普通的字符串处理。因此 MyBatis 提供了类型处理器接口,让开发者可以根据具体的业务需求来自定义适合的类型处理器。

本小节,我们将以 JSON 类型处理器作为落脚点,来介绍类型处理器,并自定义 JSON 类型处理器。

2. JSON 数据类型

首先,我们需要为 MyBatis 内置类型处理器增加一个它无法处理的数据类型,这里我们选择 MySQL5.7 中新增的 JSON 数据类型,这也是大家普遍使用的一个数据类型。在可用的数据库环境中,我们运行如下脚本:

DROP TABLE IF EXISTS blog;
CREATE TABLE blog
(
  id   int(11) unsigned primary key auto_increment,
  info json,
  tags json
);
INSERT INTO blog(info, tags)
VALUES ('{"title": "世界更大", "content": "世界更大的内容", "rank": 1}', '["世界观"]'),
       ('{"title": "人生更短", "content": "人生更短的内容", "rank": 2}', '["人文"]');

在这个脚本中,我们新建了一个 blog 数据表,blog 数据表除 id 外有 info 和 tags 两个字段,这两个字段都是 JSON 类型,并通过 insert 语句添加了两条记录。

3. 类型处理器

MyBatis 默认是无法很好处理 info 和 tags 这两个字段的,只能将它们当成字符串类型来处理,但显然这不是我们想要的效果。我们希望新增 json 类型处理器来处理好这两个字段。

MyBatis 提供了 TypeHandler 接口,自定义类型处理器需要实现了这个接口才能工作。考虑到很多开发者不够熟练,MyBatis 还提供了一个 BaseTypeHandler 抽象类来帮助我们做自定义类型处理器,只需继承这个基类,然后实现它的方法即可。

3.1 JsonObject 处理器

JSON 可分为 object 和 array 两大类,分别对应 info 和 tags 字段,这两类需要分别实现类型处理器。由于需要对 JSON 进行处理,我们在 pom.xml 文件中添加上对应的依赖。

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.60</version>
</dependency>

这里,我们使用阿里巴巴开源的 fastjson库。

在 com.imooc.mybatis 包下新建 handler 包,并向 handler 包中添加上 json object 的类型处理器 JsonObjectTypeHandler。如下:

package com.imooc.mybatis.handler;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.ibatis.type.*;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@MappedJdbcTypes(JdbcType.VARCHAR) // 对应jdbc 类型
@MappedTypes({JSONObject.class}) // 对应处理后类型
public class JsonObjectTypeHandler extends BaseTypeHandler<JSONObject> {
  // 当为 PreparedStatement 参数时,如何处理对象
  @Override
  public void setNonNullParameter(PreparedStatement preparedStatement, int i, JSONObject o, JdbcType jdbcType) throws SQLException {
    preparedStatement.setString(i, JSON.toJSONString(o));
  }

  // 当通过名称从结果中取json字段时如何处理
  @Override
  public JSONObject getNullableResult(ResultSet resultSet, String s) throws SQLException {
    String t = resultSet.getString(s);
    return JSON.parseObject(t);
  }

  // 当通过序列号从结果中取json字段时如何处理
  @Override
  public JSONObject getNullableResult(ResultSet resultSet, int i) throws SQLException {
    String t = resultSet.getString(i);
    return JSON.parseObject(t);
  }

  // 当通过序列号从 CallableStatement 中取json字段时如何处理
  @Override
  public JSONObject getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
    String t = callableStatement.getString(i);
    return JSON.parseObject(t);
  }
}

有了 BaseTypeHandler 作为基础后,实现一个类型处理器就比较简单了,我们只需要为其中 4 个方法添加上对应的实现即可。

类型处理器有两个作用,第一处理 Java 对象到 JdbcType 类型的转换,对应 setNonNullParameter 方法;第二处理 JdbcType 类型到 Java 类型的转换,对应 getNullableResult 方法,getNullableResult 有 3 个重载方法。下面我们依次来说明这四个方法的作用:

  • setNonNullParameter:处理 PreparedStatement 中的 JSONObject 参数,当调用 PreparedStatement 执行 SQL 语句时,调用该处理 JSONObject 类型的参数,这里我们通过 fastjson 的JSON.toJSONString(o)函数将 JSONObject 转化为字符串类型即可。
  • getNullableResult:从结果集中获取字段,这里 CallableStatement 和 ResultSet 分别对应不同的执行方式,对于 JDBC 而言 JSON 类型也会当做字符串来处理,因此这里我们需要将字符串类型转化为 JSONObject 类型,对应 JSON.parseObject(t)代码。

3.2 JsonArray 处理器

与 JsonObjectTypeHandler 一样,在 handler 包下新建 JsonArrayTypeHandler 类,继承 BaseTypeHandler 类,并将具体方法的实现从 JSON.parseObject 改变为 JSON.parseArray,如下:

@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({JSONArray.class})
public class JsonArrayTypeHandler extends BaseTypeHandler<JSONArray> {
  @Override
  public void setNonNullParameter(PreparedStatement preparedStatement, int i, JSONArray o, JdbcType jdbcType) throws SQLException {
    preparedStatement.setString(i, JSON.toJSONString(o));
  }

  @Override
  public JSONArray getNullableResult(ResultSet resultSet, String s) throws SQLException {
    String t = resultSet.getString(s);
    // // 变成了 parseArray
    return JSON.parseArray(t);
  }

  @Override
  public JSONArray getNullableResult(ResultSet resultSet, int i) throws SQLException {
    String t = resultSet.getString(i);
    // // 变成了 parseArray
    return JSON.parseArray(t);
  }

  @Override
  public JSONArray getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
    String t = callableStatement.getString(i);
    // 变成了 parseArray
    return JSON.parseArray(t);
  }
}

4. 注册类型处理器

自定义类型处理器无法直接被 MyBatis 加载,我们需要增加相关的配置告诉 MyBatis 加载类型处理器。

4.1 全局注册

在全局配置配置文件中可通过 typeHandlers 属性来注册类型处理器。如下:

<typeHandlers>
  <package name="com.imooc.mybatis.handler"/>
</typeHandlers>

通过 package 项来指定类型处理器所在的包路径,这样 handler 包中的所有类型处理器都会注册到全局。

当然如果你的类型处理器分散在其它地方,也可以通过如下方式来注册。

<typeHandlers>
  <typeHandler handler="com.imooc.mybatis.handler.JsonArrayTypeHandler"/>
</typeHandlers>

全局注册的类型处理器会自动被 MyBatis 用来处理所有符合类型的参数。如 JsonArrayTypeHandler 通过 MappedJdbcTypes 注解表明了自己将会处理 JdbcType.VARCHAR 类型,MyBatis 会自动将字符串类型的参数交给 JsonArrayTypeHandler 来进行处理。

但是,这样显然有问题,因为 JsonObjectTypeHandler 注册的类型也是 JdbcType.VARCHAR 类型,所以全局注册是不推荐的,除非你需要对所有参数都做类型转换。

4.2 局部注册

由于全局注册会对其它类型产生歧义和污染,因此我们选择更加精准的局部注册。在 BlogMapper 中,我们来注册和使用类型处理器。

在 BlogMapper.xml 文件中,我们添加上如下配置。

<resultMap id="blogMap" type="com.imooc.mybatis.model.Blog">
  <result column="id" property="id"/>
  <result column="info" property="info" typeHandler="com.imooc.mybatis.handler.JsonObjectTypeHandler"/>
  <result column="tags" property="tags" typeHandler="com.imooc.mybatis.handler.JsonArrayTypeHandler"/>
</resultMap>

<select id="selectById" resultMap="blogMap">
  SELECT * FROM blog WHERE id = #{id}
</select>

我们定义了 名为 blogMap 的 resultMap 和名为 selectById 的查询。在 result 映射中,我们注册了相关的类型处理器,info 字段对应

JsonObjectTypeHandler 类型处理器,tags 字段对应 JsonArrayTypeHandler 类型处理器。

这样自定义的类型处理器不会污染到其它数据,blogMap 的类型 com.imooc.mybatis.model.Blog 定义如下:

package com.imooc.mybatis.model;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

public class Blog {
  private Long id;
  private JSONObject info;
  private JSONArray tags;
  // 省略了 getter 和 setter 方法
}

4.3 处理 JDBC 类型

在对应的 BlogMapper.java 接口上添加上对应的 selectById 方法:

package com.imooc.mybatis.mapper;

import com.imooc.mybatis.model.Blog;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface BlogMapper {
  Blog selectById(Integer id);
}

我们测试一下 selectById 方法:

BlogMapper blogMapper = session.getMapper(BlogMapper.class);
Blog blog = blogMapper.selectById(1);
System.out.println(blog.toString());
String title = blog.getInfo().getString("title");
System.out.println(title);
String tag = blog.getTags().getString(0);
System.out.println(tag);

输出结果如下:

Blog{id=1, info={"rank":1,"title":"世界更大","content":".......****............"}, tags=["世界观"]}
世界更大
世界观

从结果中可以看出,类型处理器成功的处理了查询的数据,info 和 tags 字段都能够通过 fastjson 的 API 来获取里面的内容。

4.4 处理 JSON 类型

在查询可以工作的情况下,那么如何通过 insert 插入 JSON 对象了。

我们在 BlogMapper 中新增一个 insertBlog 方法,如下:

<insert id="insertBlog">
  INSERT INTO blog(info,tags)
  VALUES(#{info,typeHandler=com.imooc.mybatis.handler.JsonObjectTypeHandler},
  #{tags,typeHandler=com.imooc.mybatis.handler.JsonArrayTypeHandler})
</insert>
public interface BlogMapper {
  int insertBlog(@Param("info") JSONObject info, @Param("tags") JSONArray tags);
}

这样 MyBatis 就可以处理 JSON 类型的参数了,我们再次测试一下:

JSONObject info = new JSONObject().fluentPut("title", "测试案例").fluentPut("rank", 1);
JSONArray tags = new JSONArray().fluentAdd("测试");
int rows = blogMapper.insertBlog(info, tags);
System.out.println(rows);

输出结果:

1

可以看到类型处理器成为了 Java JSON 类型和 JDBC 类型转换桥梁,在查询的时候主动将数据库类型转化为了可用的 JSON 类型,而在插入的时候将 JSON 类型又转化为了数据库可识别的字符串类型。

5. 小结

  • 自定义类型处理器并不难,MyBatis 已经帮我们做好了大多数工作,我们只需在适当的位置适当的配置就可以了。
  • 数据库 JSON 类型的用处会越来越广泛,在 MyBatis 官方未内置处理器之前,我们也可以通过本小节的方式来提早的使用它。