实现java类型和JDBC类型的相互转换 - 使用BaseTypeHandler实现数据库某一个字段的嵌套json的解析(不定数量的key-value键值对)
1 背景
我们都知道数据库的一个varchar类型的字段存储字符串数据,一些特定结构的数据结构,比如说Set集合,List,map等被转成字符串后也可以存在这样的字段中(这种场景是经常会出现的),所以在取出这些数据的时候往往还需要在这种特殊的字段上进行特殊的对象映射,上面提到的映射都不难,而其中最复杂的场景是当某一个字段存储的是嵌套的json字符串的时候,比如说下面这种
{
"getField":"",
"tableName":"",
"conditions":"",
"handler":"",
"toolId": "_BUILD_XXXX_TEST_IPV6_H3C_LF",
"toolParams": {
"param1":"value1",
"param2":"value2",
"fields":"deviceGenerateVO,deviceLinkVO,projectMain",
"systemParams":{
"deviceGenerateVO":{},
"deviceLinkVO":{},
"projectMain":{}
}
}
}
2 前置知识
2.1 mybatis的TypeHandler
可以参考以下链接来弄懂这个类型处理器的用法Mybatis之TypeHandler使用教程
2.2 Gson解析字符串
Gson解析JSON中动态未知字段key的方法
这篇文章使用了Gson解析JSON中的未知的动态字段Key的方法,但是发现其最底层的json对象中包含的kv键值对的数量还是不变的。从下面这个json数据可以看出来,future对象下有不定数量的预测天数及其对应的天气,但是每一天使用到的天气模型是固定的(这是最深层的json对象)。如果说我们future对象每一天的天气详情所用到的字段都是不固定的,那么这里提到的Gson解析会失效
{ "resultcode":"200","reason":"successed!",
"result":{
"sk":{
"temp":"24","wind_direction":"东北风","wind_strength":"2级","humidity":"28%","time":"17:38"
},
"today":{
"temperature":"15℃~26℃","weather":"多云转晴","wind":"东北风微风","week":"星期日","city":"桂林","date_y":"2015年10月11日","dressing_index":"舒适","dressing_advice":"建议着长袖T恤、衬衫加单裤等服装。年老体弱者宜着针织长袖衬衫、马甲和长裤。","uv_index":"弱","comfort_index":"","wash_index":"较适宜","travel_index":"较适宜","exercise_index":"较适宜","drying_index":""
},
"future":{
"day_20151011":{"temperature":"15℃~26℃","weather":"多云转晴","wind":"东北风微风","week":"星期日","date":"20151011"},
"day_20151012":{"temperature":"16℃~27℃","weather":"晴转多云","wind":"微风","week":"星期一","date":"20151012"},
"day_20151013":{"temperature":"16℃~26℃","weather":"多云转晴",,"wind":"微风","week":"星期二","date":"20151013"},
"day_20151014":{"temperature":"17℃~27℃","weather":"晴","wind":"北风微风","week":"星期三","date":"20151014"},
"day_20151015":{"temperature":"17℃~28℃","weather":"晴","wind":"北风微风","week":"星期四","date":"20151015"},
"day_20151016":{"temperature":"17℃~30℃","weather":"晴","wind":"北风微风","week":"星期五","date":"20151016"},
"day_20151017":{"temperature":"17℃~30℃","weather":"晴","wind":"北风微风","week":"星期六","date":"20151017"}
}
},
"error_code":0
}
3 如何jdbc包含复杂的json数据的字段映射到java类中
这里我们在写映射数据表的java类时需要在对应的复杂字段上的@TableName注解声明typeHandler是哪个,这个处理器可以用mybatis提供的,也可以是由我们自定义的,但是mybatis提供的类型处理器不足以处理复杂的jdbc的类型,需要我们自定义一个。
1 假设有以下数据表:
-- authorityTemplateVariable definition
CREATE TABLE `authorityTemplateVariable` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`variableType` varchar(20) DEFAULT 'string' COMMENT '变量类型(string,array)',
`remarks` varchar(255) DEFAULT '' COMMENT '备注',
`variableKey` varchar(100) DEFAULT '' COMMENT '配置生成系统的参数',
`variableKeyConfig` varchar(512) DEFAULT '' COMMENT '变量配置 {"key":"变量Key数据来源的拼装,封号隔开 table.name","position":"条件list跟key一一映射,conditions","handler":"钩子函数,用于复杂数据获取", toolid是工具函数的id,toolParams是其调用时的参数}\nS\n',
`updateTime` bigint(20) DEFAULT NULL COMMENT '更新时间',
`updateUser` varchar(100) DEFAULT '' COMMENT '更新人',
`isCustomHandler` bigint(20) unsigned DEFAULT NULL COMMENT '是否调用自定义的handler,1表示自定义,此时调用外部的工具函数,0表示非自定义,依据原先的模式调用variableKeyConfig字段规定的handler的方法',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_variableKey` (`variableKey`,`variableType`) USING BTREE COMMENT '变量索引'
) ENGINE=InnoDB AUTO_INCREMENT=155 DEFAULT CHARSET=utf8 COMMENT='权威模版变量';
2
假如说数据库该张表的variableKeyConfig字段存储了以下格式的信息, fields字段的value和systemParams的key一一对应,如果fields为空,则systemParams的value是空json串。
// 格式1
{
"getField":"",
"tableName":"",
"conditions":"",
"handler":"",
"toolId": "_BUILD_XXXX_TEST_IPV6_H3C_LF",
"toolParams": {
"param1":"value1",
"param2":"value2",
"fields":"",
"systemParams":{}
}
}
// 格式2
{
"getField":"",
"tableName":"",
"conditions":"",
"handler":"",
"toolId": "_BUILD_XXXX_TEST_IPV6_H3C_LF",
"toolParams": {
"param1":"value1",
"param2":"value2",
"fields":"deviceGenerateVO_deviceLinkVO_projectMain",
"systemParams":{
"deviceGenerateVO":{},
"deviceLinkVO":{},
"projectMain":{}
}
}
}
所以为了和数据库authorityTemplateVariable表的字段对应,我们创建的entity对象如下:
@Data
@EqualsAndHashCode(callSuper = false)
@TableName(value = "authorityTemplateVariable", autoResultMap = true)
@ApiModel(description = "authorityTemplateVariable")
public class AuthorityTemplateVariable extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
@ApiModelProperty("主键")
@FieldInterface(title = "ID")
public Long id;
@ApiModelProperty("变量类型")
@TableField("variableType")
private String variableType;
@ApiModelProperty("备注")
@TableField("remarks")
private String remarks;
@ApiModelProperty("配置生成系统的参数")
@TableField("variableKey")
private String variableKey;
@ApiModelProperty("变量配置 {'key':'变量Key数据来源的拼装,封号隔开 table.name','position':'条件list跟key一一映射, conditions',"
+ "'handler':'钩子函数,用于复杂数据获取'}")
@TableField(value = "variableKeyConfig", typeHandler = VariableKeyConfigTypeHandler.class)
private VariableKeyConfig variableKeyConfig;
@ApiModelProperty("更新时间")
@FieldInterface(title = "更新时间", isDate = true)
@TableField(value = "updateTime", fill = FieldFill.INSERT_UPDATE)
private Long updateTime;
@ApiModelProperty("更新人")
@TableField(value = "updateUser", fill = FieldFill.INSERT_UPDATE)
private String updateUser;
}
// 映射authorityTemplateVariable表的variableKeyConfig字段的json数据格式(仅仅映射第一层)
@Data
public class VariableKeyConfig {
// 内部handler函数
//变量Key数据来源的拼装,封号隔开 table.name
private String getField = "";
//条件list跟key一一映射,conditions
private String tableName = "";
private String conditions = "";
private String handler = "";
// 调用外部工具函数使用到的配置值
private String toolId = "";
// 这个字段也是json对象
private String toolParams;
}
3 自定义的typeHandler
通过下面重写的方法可以知道,我们之所以能自动实现映射,是通过将PreparedStatement预处理对象和ResultSet结果集传递给处理方法实现的,ResultSet用于查询时将查询结果映射到具体的VariableKeyConfig类中,PreparedStatement方法则将具体的呆插入的值转化为json对象存放到该字段中。
//数据库存储的类型
@MappedJdbcTypes(JdbcType.VARCHAR)
//需要转换的对象
@MappedTypes(VariableKeyConfig.class)
public class VariableKeyConfigTypeHandler<T> extends BaseTypeHandler<T> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, (new Gson()).toJson(parameter));
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
String data = rs.getString(columnName);
VariableKeyConfig variableKeyConfig = jsonMap2VariableKeyConfig(data);
return StringUtils.isBlank(data) ? null : (T) variableKeyConfig;
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String data = rs.getString(columnIndex);
VariableKeyConfig variableKeyConfig = jsonMap2VariableKeyConfig(data);
return StringUtils.isBlank(data) ? null : (T) variableKeyConfig;
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String data = cs.getString(columnIndex);
VariableKeyConfig variableKeyConfig = jsonMap2VariableKeyConfig(data);
return StringUtils.isBlank(data) ? null : (T) variableKeyConfig;
}
/**
* @Description: 将json中的第一层级的键值对映射到VariableKeyConfig对象的各个字段中, 自定义的处理机制。
* @Author: yuxiangang
* @Date: 2022/12/3
*/
public VariableKeyConfig jsonMap2VariableKeyConfig(String data) {
VariableKeyConfig variableKeyConfig = new VariableKeyConfig();
JsonObject jsonObject = (new Gson()).fromJson(data, JsonObject.class);
if (jsonObject.get("handler") != null) {
variableKeyConfig.setHandler(StringUtils.remove(
String.valueOf(jsonObject.get("handler")), "\""));
}
if (jsonObject.get("conditions") != null) {
variableKeyConfig.setConditions(StringUtils.remove(
String.valueOf(jsonObject.get("conditions")), "\""));
}
if (jsonObject.get("getField") != null) {
variableKeyConfig.setGetField(StringUtils.remove(
String.valueOf(jsonObject.get("getField")), "\""));
}
if (jsonObject.get("tableName") != null) {
variableKeyConfig.setTableName(StringUtils.remove(
String.valueOf(jsonObject.get("tableName")), "\""));
}
if (jsonObject.get("toolId") != null) {
variableKeyConfig.setToolId(StringUtils.remove(
String.valueOf(jsonObject.get("toolId")), "\""));
}
if (jsonObject.get("toolParams") != null) {
variableKeyConfig.setToolParams(StringUtils.remove(
String.valueOf(jsonObject.get("toolParams")), "\""));
}
return variableKeyConfig;
}
}
4 流程:
可以想象一下整个查询流程,从数据表authorityTemplateVariable查询获得结果集 -> 准备映射到AuthorityTemplateVariable中 -> 在处理variableKeyConfig字段时,发现映射对象的@TableField注解, 指定了一个字段映射处理器,typeHandler=VariableKeyConfigTypeHandler.class, 进入VariableKeyConfigTypeHandler类,并且将ResultSet传递给该类的字段处理方法上 -> 调用我们自定义的多层嵌套的json解析过程,由此实现了复杂的json格式到java类型的映射。