WHCSRL 技术网

MyBatis+MySQL8.0存取Json字段之TypeHandler

一丶背景

在业务开发过程中,为了实现一个在线编辑器功能,存取了一些CSS样式在MySQL里面,就像这样:

"css": {
                "id": "3",
                "width": 11,
                "height": 12,
                "left": 13,
                "top": 14,
                "createtTime": "2021-10-14 10:40:11",
                "updateTime": "2021-10-14 10:40:11",
                "creatorId": "1111"
            },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

之前的MySQL存JSON字段都是用BLOB类型,在MySQL 5.7后添加了JSON类型作为对JSON字段的存储。但是MyBatis并未支持,因此有了这篇文章要解决的问题。

二丶解决方案

1.自定义转换

也就是查询的时候,将用fastjson这类工具包,JSON串转为对象;在存储时,手动将对象转为json串,然后存入类型为VARCHAR的字段里。就像这样:

@Data
public class Element {
    private String id;
    private String css;
    private Objetc cssObj;
    private Datetime createTime;
}

@Data
public class Css{
    private String id;
    private int width;
    private int height;
    private int left;
    private int top;
    private Date createTime;
    private Date updateTime;
    private String creatorId;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

自定义转换ServiceImpl:

@Override
public Element getById(String id) {
    Element element = elementDao.selectByPrimaryKey(id);
    String css= element .getCss();
    Css cccObj = JsonUtils.fromJson(css, Css.class);
    element .setCssObj(cccObj );
    return element ;
}

@Override
public boolean save(Element element) {
    Css cssObj = element.getCssObj();
    element.setCss(JsonUtil.toJson(cssObj ));
    return elementDao.insert(element) > 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这样的解决方案存在两个问题:
1.需要在model类上面增加冗余字段:cssObj。
2.每一个业务逻辑里面都需要增加上述转换方法,代码冗余了些。
3.不够优雅
这时候,阅读框架源码的作用体现出来了,Mybatis本身作为一个ORM框架,自己是实现了类型转换的,可不可以参考Mybatis的实现,来自己设计一个转换器呢?Mybatis预定义的基础类型转换是通过实现TypeHandler接口或者继承抽象类BaseTypeHandler来实现,其默认的转换类型如图:在这里插入图片描述
本文采用的方式是继承BaseTypeHandler的方式,来实现对JSON数据类型的转换。Mybatis的BaseTypeHandler具体代码如图:
BaseTypeHandler:

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;  
   
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

2.继承BaseTypeHandler实现对JSON类型的转换

  1. 第一步,定义一个abstract class,继承于org.apache.ibatis.type.BaseTypeHandler,作为Object类型的转换基类,所有想varcharObject的互转,只需要继承此基类即可,无需重复写第一个方法那些自定义转换的步骤。
package com.eqxiu.chart.handler;

import com.eqxiu.chart.util.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;

import org.apache.ibatis.type.JdbcType;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * ClassName: AbsractObjectTypeHandler
 * Description: Model对象json转换抽象类 解决mybatis插入json数据报错问题
 * Author: lizhiyu
 * Date: 2021/10/14 11:05
 * Version: mvp
 **/
public abstract class AbstractObjectTypeHandler<T> extends BaseTypeHandler<T> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter,
                                    JdbcType jdbcType) throws SQLException {
        ps.setString(i, JsonUtils.toJson(parameter));
    }

    @Override
    public T getNullableResult(ResultSet rs, String columnName)
            throws SQLException {
        String data = rs.getString(columnName);
        return StringUtils.isBlank(data) ? null : JsonUtils.fromJson(data, (Class<T>) getRawType());
    }

    @Override
    public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String data = rs.getString(columnIndex);
        return StringUtils.isBlank(data) ? null : JsonUtils.fromJson(data, (Class<T>) getRawType());
    }

    @Override
    public T getNullableResult(CallableStatement cs, int columnIndex)
            throws SQLException {
        String data = cs.getString(columnIndex);
        return StringUtils.isBlank(data) ? null : JsonUtils.fromJson(data, (Class<T>) getRawType());
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  1. 第二步,定义具体的实现类,继承步骤一中的AbstractObjectTypeHandler,要转什么类型的Java对象,转什么对象。
public class CssTypeHandler extends AbstractObjectTypeHandler<Css> {
}
  • 1
  • 2
  1. 第三步,修改原有Element类的数据结构,去除String类型,如图:
@Data
public class Element {
    private String id;
    private Object css;
    private Datetime createTime;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 第四步,配置类型处理器包扫描路径,在application.properties里面配置:
#mybatis typeHandler扫描
mybatis.typeHandlersPackage=com.eqxiu.chart.handler.impl
  • 1
  • 2
  1. 第五步,修改对应XML文件,将对应属性使用自定义的转换器:
<resultMap id="BaseResultMap" type="com.eqxiu.chart.model.Element">
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="css" jdbcType="OTHER" property="css" typeHandler="com.eqxiu.chart.handler.Impl.CssTypeHandler" />
    <result column="compType" jdbcType="VARCHAR" property="comptype" />
    <result column="contentIds" jdbcType="VARCHAR" property="contentIds" typeHandler="com.eqxiu.chart.handler.Impl.ListToVarcharTypeHandler" />
    <result column="chartData" jdbcType="VARCHAR" property="chartData" />
    <result column="createTime" jdbcType="TIMESTAMP" property="createTime" />
    <result column="updateTime" jdbcType="TIMESTAMP" property="updateTime" />
    <result column="creatorId" jdbcType="VARCHAR" property="creatorId" />
  </resultMap>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  1. 第六步,这里有个小坑,在MySQL8.0后,插入JSON对象还得用uff8mb8编码才行,不然会类型错误。所以在insert或者update时,需要加上CONVERT函数,作编码转换。
<!-- foreach批量插入 -->
  <insert id="insertBatch">
    INSERT element (id, css, compType, contentIds, chartData, createTime, updateTime, creatorId)
    VALUES
    <foreach collection ="elementList" item="element" separator =",">
      (#{element.id,jdbcType=VARCHAR}
      , CONVERT(#{element.css,jdbcType=OTHER,typeHandler=com.eqxiu.chart.handler.Impl.CssTypeHandler} using utf8mb4)
      , #{element.comptype,jdbcType=VARCHAR}
      , #{element.contentIds,jdbcType=VARCHAR,typeHandler=com.eqxiu.chart.handler.Impl.ListToVarcharTypeHandler}
      , #{element.chartData,jdbcType=VARCHAR},#{element.createTime,jdbcType=TIMESTAMP},#{element.updateTime,jdbcType=TIMESTAMP}
      , #{element.creatorId,jdbcType=VARCHAR})
    </foreach >
  </insert>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

最后,业务代码就变得不再冗杂了,也便于扩展。

@Override
public Element getById(String id) {
    return elementDao.selectByPrimaryKey(id);
}

@Override
public boolean save(Element element) {
    return elementDao.insert(element) > 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

三丶反思,继续扩张认知边界

这篇文章只是从业务实现层面去介绍了如何用TypeHandler的思路,优雅的解决Mybatis+Mysql实现对json数据类型字段的存取。但是对于,MyBatis中TypeHandler如何具体执行的以及设计思路未作探讨,下一篇文章会继续写Mybatis中TypeHandler的原理。

推荐阅读