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"
},
之前的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;
}
自定义转换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.需要在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;
}
2.继承BaseTypeHandler实现对JSON类型的转换
- 第一步,定义一个abstract class,继承于org.apache.ibatis.type.BaseTypeHandler,作为Object类型的转换基类,所有想varchar与Object的互转,只需要继承此基类即可,无需重复写第一个方法那些自定义转换的步骤。
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());
}
}
- 第二步,定义具体的实现类,继承步骤一中的AbstractObjectTypeHandler,要转什么类型的Java对象,转什么对象。
public class CssTypeHandler extends AbstractObjectTypeHandler<Css> {
}
- 第三步,修改原有Element类的数据结构,去除String类型,如图:
@Data
public class Element {
private String id;
private Object css;
private Datetime createTime;
}
- 第四步,配置类型处理器包扫描路径,在application.properties里面配置:
#mybatis typeHandler扫描
mybatis.typeHandlersPackage=com.eqxiu.chart.handler.impl
- 第五步,修改对应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>
- 第六步,这里有个小坑,在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>
最后,业务代码就变得不再冗杂了,也便于扩展。
@Override
public Element getById(String id) {
return elementDao.selectByPrimaryKey(id);
}
@Override
public boolean save(Element element) {
return elementDao.insert(element) > 0;
}
三丶反思,继续扩张认知边界
这篇文章只是从业务实现层面去介绍了如何用TypeHandler的思路,优雅的解决Mybatis+Mysql实现对json数据类型字段的存取。但是对于,MyBatis中TypeHandler如何具体执行的以及设计思路未作探讨,下一篇文章会继续写Mybatis中TypeHandler的原理。