mybatis 自动化处理 mysql 的json类型字段 终极方案
创始人
2024-01-20 09:11:06

文章目录

  • mybatis 自动化处理 mysql 的json类型字段 终极方案
    • mysql 建表 json 字段,添加1条json 数据
    • 对应的java对象 `JsonEntity`
    • mybatis,不使用 通用mapper
      • 手动自定义1个类型处理器,专门处理 JsonNode 和Json 的互相转化
        • 将 自定义的类型处理器 加入到 mybatis 核心配置,不用 xml
        • @Repository中sql查询 jdbc 的 json 字段,自动映射为 java 类型
        • 源码中的关键代码点:mybatis 如何映射json结果到 java对象
    • mybatis,使用 通用mapper
    • 最终效果展示 ,增删改查测试 代码示例 :
      • 查询并显示 json
      • 直接更新json
    • 源代码下载
    • 参考文档

mybatis 自动化处理 mysql 的json类型字段 终极方案

本文基于原生的 mybatis ,而不是 mybatis-plus ,请知悉。
目标1-查询:查询数据库的json字段,转换为java的json对象,并优雅的返回前端
目标2-更新:识别前端的请求参数,转换为 数据库的 Json 字段 ,比如新增/更新
目标3-注解:不使用 xml增加 typeHandler,而是 使用注解方式
目标4-智能:不在sql中的字段上指定 typeHandler, 不要每次都手写,要 自动化识别

  • 官网教程 ✔ 大而全,但是代码例子太少,有的地方1遍看不明白
  • 腾讯教程 ❌可 行,但是不够自动化,不优雅
  • 源码分析 ❓ 越看越懵,而且最后的而解决方案跟腾讯的一样,不优雅

mysql 建表 json 字段,添加1条json 数据

-- 建表 json 字段,添加1条json 数据
create table t_test_json(id int primary key auto_increment,json_field JSON  default null);
insert into t_test_json( json_field) values ('{"hello":"world"}');

对应的java对象 JsonEntity

@Table(name="t_test_json")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class JsonEntity{@Idprivate Integer id;// 为何不是 ArrayNode 或者 ObjectNode ? // 因为 JsonNode 是他们俩的父类,可以自动兼容2种格式的json : [{},{}] 和 {} private JsonNode jsonField;@SneakyThrows@Overridepublic String toString() {return JacksonUtils.writeValueAsString(this);}
}

mybatis,不使用 通用mapper

  • 因为 javaType JsonNode 和 mysql 的json类型字段,都不是 mybatis 默认能互相转化处理的,所以需要 手动创建 类型处理器
    在这里插入图片描述

手动自定义1个类型处理器,专门处理 JsonNode 和Json 的互相转化

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/*** includeNullJdbcType 在 Mybatis 3.4.0 开始 默认为true。* 想让mybatis 自动化处理映射关系,则必须保证 includeNullJdbcType =true,* 因为如果只是设置了  @MappedJdbcTypes(value = JdbcType.VARCHAR ) 则该处理器就无法自动处理 JdbcType是json 的情况。* 实际上,根据官方文档,mybatis 是把所有的返回值都当作 JdbcType = null 来自动 选择类型处理器的* 如果includeNullJdbcType =false,则必须在 sql中返回的字段上 明确标注 typeHandler= xxx.class * * @MappedJdbcTypes的value 设置为 JdbcType.LONGVARCHAR 或者 JdbcType.LONGVARCHAR 都可以。* 建议JdbcType.LONGVARCHAR,据测试,json 类型的返回结果的JdbcType = LONGVARCHAR * * @ColumnType when the reult is ResultMap*/
// @MappedTypes(JsonNode.class) // 因为BaseTypeHandler 泛型中指定了JsonNode 的话,这个注解也可以省略 
@MappedJdbcTypes(value = JdbcType.VARCHAR, includeNullJdbcType = true)
@Component
public class JsonNodeTypeHandler extends BaseTypeHandler implements InitializingBean {static JsonNodeTypeHandler j;@AutowiredObjectMapper objectMapper;/*** 魔法 注入 单例bean objectMapper; * 在 @Controller 中注入ObjectMapper 不需要这么麻烦,直接 @Autowired 即可 。* 非Controller 注入原理:spring 启动过程中 实例化JsonNodeTypeHandler 的 bean 时,会自动把 objectMapper 携带过来;* spring 启动完成后的bean 又会被擦除 。所以,这个要及时赋值一下引用 objectMapper*/@Overridepublic void afterPropertiesSet() {j = this; // 初始化静态实例j.objectMapper = this.objectMapper; //及时拷贝引用}@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, JsonNode jsonNode, JdbcType jdbcType) throws SQLException {ps.setString(i, jsonNode != null ? jsonNode.toString() : null);}@SneakyThrows@Overridepublic JsonNode getNullableResult(ResultSet rs, String colName) {return read(rs.getString(colName));}@SneakyThrows@Overridepublic JsonNode getNullableResult(ResultSet rs, int colIndex) {return read(rs.getString(colIndex));}@SneakyThrows@Overridepublic JsonNode getNullableResult(CallableStatement cs, int i) {return read(cs.getString(i));}@SneakyThrowsprivate JsonNode read(String json) {return json != null ? j.objectMapper.readTree(json) : null;}
}

将 自定义的类型处理器 加入到 mybatis 核心配置,不用 xml

public static SqlSessionFactory getSqlSessionFactory(DataSource dataSource, String javaEntityPath, String xmlMapperLocation) throws Exception {SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();factoryBean.setDataSource(dataSource);factoryBean.setTypeAliasesPackage(javaEntityPath);//mybatis configurationorg.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();// 下划线转驼峰configuration.setMapUnderscoreToCamelCase(true);// 返回Map类型时,数据库为空的字段也要返回  https://www.cnblogs.com/guo-xu/p/12548949.htmlconfiguration.setCallSettersOnNulls(true);// 配置 拦截器 打印 sql : TODO 补充 拦截器实现代码// configuration.addInterceptor(new PrintMybatisSqlInterceptor());factoryBean.setConfiguration(configuration);ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();factoryBean.setMapperLocations(resolver.getResources(xmlMapperLocation));// 自定义的类型处理器: 自动双向解析 JsonNode 类型 和 mysql中的 json; 千万别写xml 了,太low factoryBean.setTypeHandlers(new JsonNodeTypeHandler());return factoryBean.getObject();
}/*** 每个类型的数据库连接,都需要单独创建1个SqlSessionFactory* 比如mysql DataSource 需要 配置1个 @Bean SqlSessionFactory* oracle 或者 hive 或者其他的数据库,都需要单独配置自己的 @Bean SqlSessionFactory * 但是 ,getSqlSessionFactory() 这个静态方法是共用的,只需要修改对应entity和xml文件地址的参数 javaEntityPath 和 xmlMapperLocation 即可 */
@Bean
public SqlSessionFactory yourSqlSessionFactory(DataSource yourDataSource) throws Exception {return getSqlSessionFactory(yourDataSource,"com.server.model.entity.testpath","classpath:mapper/testpath/*.xml");
}

@Repository中sql查询 jdbc 的 json 字段,自动映射为 java 类型

@Select(" SELECT * from t_test_json where JSON_CONTAINS(json_field, #{vo.jsonField}) limit 1 ")
List testQueryJson(@Param("vo") JsonEntity vo);

源码中的关键代码点:mybatis 如何映射json结果到 java对象

  • 查询到结果ResultSet ,去交给DefaultResultSetHandler类的createAutomaticMappings方法去处理映射关系

    /*** 查询到结果ResultSet ,去交给DefaultResultSetHandler类的createAutomaticMappings方法去处理映射关系 * @Param metaObject MetaObject#findProperty() 能把 rs 结果中columnName 的下划线去除,对应到java对象 JsonEntity 的属性名 * @Param rsw final TypeHandler typeHandler = rsw.getTypeHandler(propertyType, columnName);*/
    private List createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix)
    

    在这里插入图片描述

    • 这个 createAutomaticMappings 方法,内部主要干了几件事

      • 找出resultset结果集中那些无法用 mybatis内置的类型处理器映射的字段名,比如json类型的 json_field

      • json_field改为驼峰格式jsonField,反射查找到Java对象中该属性为 private JsonNode jsonField;

        public String findProperty(String name, boolean useCamelCaseMapping) {if (useCamelCaseMapping) {name = name.replace("_", "");}return findProperty(name);
        }
        
      • 再根据 JsonNode 去查找已注册的类型处理器,就定位到 我们手动 自定义的类型处理器 JsonNodeTypeHandler
        在这里插入图片描述

        • 从源码能看出,json 字段对应的jdbcType 其实是 jdbcType.LONGVARCHAR ,
          假如我们设置 @MappedJdbcTypes(value = JdbcType.VARCHAR, includeNullJdbcType = true),
          则经过一些简单的 null 判断,最后依然可以定位到手写的这个 JsonNodeTypehandler 。
          最关键的就是:一定要保证 includeNullJdbcType = true,防止 JdbcType 手动设置错误导致 定位失败
    • 接下来,就是 执行 自定义类型处理器的 方法 typeHandler.getResult(ResultSet rs, String colName) 获取到值了

mybatis,使用 通用mapper

使用 通用mapper,可以少写很多单表操作的sql ,增删改查,单表操作非常方便

  • pom
    tk.mybatismapper${mapper.version}
    
    
  • 与手写sql 处理 json 字段的最大的不同点 :需要注解@ColumnType 明确标注出来 json 字段
  • 并且 JsonNodeTypeHandler 中不能有未知类型的泛型 (比如T),必须是 确定的已知的java类型 (比如 JsonNode)
import tk.mybatis.mapper.annotation.ColumnType;@Table(name="t_test_json")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class JsonEntity{@Idprivate Integer id;// 多了这个ColumnType,通用mapper生成sql必须的;如果没有该注解,则最终生成sql时 该JsonNode类型的字段将会被忽略// 如果你没有使用 通用mapper,而是完全手写sql,那么完全没必要加该注解,mybatis的自动发现足咦!!@ColumnType(typeHandler = JsonNodeTypeHandler.class) private JsonNode jsonField;@SneakyThrows@Overridepublic String toString() {return JacksonUtils.writeValueAsString(this);}
}

最终效果展示 ,增删改查测试 代码示例 :

查询并显示 json

  • controller 层
    @AutoWired
    IDao dao;
    @ApiOperation(value = "查询 自动转换 JsonNode 和 json 类型,自动发现 typeHandler ")
    @PostMapping("test/json")
    public ResultBean testJson(@RequestBody(required = false) JsonEntity vo) {return ResultUtils.oK(dao.select(vo));
    }
    
  • swagger 或者 postman 发起请求 json 格式
    • 请求 1: 查询 全量 json 结果
      curl -X POST "http://localhost:8080/api/test/json" -H "accept: */*" -H "Content-Type: application/json" -d "{}"

    • 请求 2: 根据 id 查询 json 结果,-d “{}” 里加参数即可
      curl -X POST "http://localhost:8080/api/test/json" -H "accept: */*" -H "Content-Type: application/json" -d "{ id:1}"

      {id:1}
      

      以上 2种情况,dao 层都使用 通用mapper 生成sql即可。
      都能正常被 @RequestBody(required = false) JsonEntity vo 识别 并且 生成JsonEntity 对象,return 正常的JsonEntity 结果
      在这里插入图片描述

    • 请求 3: 根据 JsonNode 字段 查询 JsonEntity 结果
      curl -X POST "http://localhost:8034/api/test/json" -H "accept: */*" -H "Content-Type: application/json" -d "{ jsonField:{\"hello\":\"world\"}}"

      • 通用mapper 自动生成sql 的where 条件为 where json_field = {"hello":"world"} ,查询结果为空 ❌。
      • 放弃通用mapper,手动 自定义sql ,使用 函数 JSON_CONTAINS 去匹配json ,ok ✔
        /*** 直接 查询 json 字段* 无法使用 通用mapper 生成的sql, 必须 手写 sql判断 json 是否存在的 JSON_CONTAINS 语法:  * where JSON_CONTAINS(json, #{vo.jsonField})* 最终生成的sql 是 * select * from  t_test_json where JSON_CONTAINS(json_field,'{"hello": "world"}')* 如此,才能让 mysql 正常查询json 字段和返回结果 * 参考 mysql 正确比较 2个json 字段 的写法 https://blog.csdn.net/weixin_39926042/article/details/118812599*/
      @Select(" select * from  t_test_json where JSON_CONTAINS(json_field, #{vo.jsonField}) ")
      List selectByJson(@Param("vo") JsonEntity vo);
      

      -

  • 直接查询json 字段,没问题,接下来,看下 如何直接 更新 json 字段

直接更新json

  • swagger 或者 postaman 请求 json 格式参数
    curl -X POST "http://localhost:8034/api/test/json/update" -H "accept: */*" -H "Content-Type: application/json" -d "{\"id\": 1, \"jsonField\": {\"hello\":\"world again!\"}}"

    {"id": 1,"jsonField": {"hello":"world again!"}
    }
    
  • controlller 层

    @ApiOperation(value = "更新 json 字段,自动生成sql或者手写sql 均可,都无需指定 typehandler 属性 ")
    @PostMapping("/json/update")
    public ResultBean testUpdateJson(@RequestBody JsonEntity vo) {final IJsonDao dao = SpringContextUtils.getBean(IJsonDao.class);// 通用mapper生成 sql UPDATE T_TEST_JSON SET id = id,json_field = {"hello":"world again!"} WHERE id = 1 ;final int i = dao.updateByPrimaryKeySelective(vo);// 手动写 sql UPDATE t_test_json SET json_field = {"hello":"world again!"} where id = 1 ;return DFResultUtils.oK(dao.testUpdateJson((vo)));
    }
    

    -

  • 通用mapper Dao 的写法

    @Repository("jsonDao")
    public interface IJsonDao extends MyMapper {@Transactional(value = JK.transactionManager, rollbackFor = Exception.class)@Select(" UPDATE t_test_json SET json_field = #{vo.jsonField} where id  = #{vo.id}")List testUpdateJson(@Param("vo") JsonEntity vo);
    }
    /** MyMapper 通用mapper类 写法 
    public interface MyMapperextendsBaseMapper,ExampleMapper,ConditionMapper,MySqlMapper {
    }*/
    
  • 更新后结果
    -

源代码下载

  • 下载地址 https://download.csdn.net/download/w1047667241/86927373在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

参考文档

  1. mysql 正确比较 2个json 字段 的写法: JSON_CONTAINS(json_field, #{vo.jsonField})

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
阳澄湖在哪里哪个省的 阳澄湖是... 点击题目下方苏州生活指南有用 有趣 有态度近期的阳澄湖度很高,3月24日,央视二套《第一时间》直播连...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
金属硬度排行 各种金属硬度 世界上最硬的东西排名分别为硫化碳块,石墨烯,金刚石,金属玻璃和金属锇。这五种物体几乎是整个世界上最坚...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...