文章目录
mybatis 自动化处理 mysql 的json类型字段 终极方案
why json
为何使用json格式存储
1 存储内容经常改变,需要新增或者删减字段,但是字段的删除可能会出错,字段的新增个数不确定(field命名没规律)
2 不想多存储字段的 父类字段 parent_id ,因为sql语法会很复杂
3 不想用其他数据库,比如mogoDB ,多引入框架,会增加复杂度
4 mysql 支持json,但是语法复杂; 借助 mybatis 即可实现 jsonString <==> java jsonObject 的双向操作
简介
本文基于原生的 mybatis ,而不是 mybatis-plus ,请知悉。
目标1-查询:查询
数据库的json字段,转换为java的json对象,并优雅的返回前端
目标2-更新:识别前端的请求参数,转换为 数据库的 Json 字段 ,比如新增/更新
目标3-注解:不使用 xml增加 typeHandler,而是
使用注解方式
目标4-智能:
不在
sql中的字段上指定 typeHandler,
不要每次都手写,要 自动化识别
- 官网教程 ✔ 大而全,但是代码例子太少,有的地方1遍看不明白
- 腾讯教程 ❌可 行,但是不够自动化,不优雅
- 源码分析 ❓ 越看越懵,而且最后的而解决方案跟腾讯的一样,不优雅
mysql 建表 json 字段,添加1条json 数据
-- 建表 json 字段,添加1条json 数据createtable t_test_json(id intprimarykeyauto_increment,json_field JSON defaultnull);insertinto t_test_json( json_field)values('{"hello":"world"}');
对应的java对象
JsonEntity
@Table(name="t_test_json")@Data@AllArgsConstructor@NoArgsConstructor@BuilderpublicclassJsonEntity{@IdprivateInteger id;// 为何不是 ArrayNode 或者 ObjectNode ? // 因为 JsonNode 是他们俩的父类,可以自动兼容2种格式的json : [{},{}] 和 {} privateJsonNode jsonField;@SneakyThrows@OverridepublicStringtoString(){returnJacksonUtils.writeValueAsString(this);}}
mybatis,不使用 通用mapper
- 因为 javaType JsonNode 和 mysql 的json类型字段,都不是 mybatis 默认能互相转化处理的,所以需要 手动创建 类型处理器
手动自定义1个类型处理器,专门处理 JsonNode 和Json 的互相转化
importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importlombok.SneakyThrows;importorg.apache.ibatis.type.BaseTypeHandler;importorg.apache.ibatis.type.JdbcType;importorg.apache.ibatis.type.MappedJdbcTypes;importorg.apache.ibatis.type.MappedTypes;importorg.springframework.beans.factory.InitializingBean;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importjava.sql.CallableStatement;importjava.sql.PreparedStatement;importjava.sql.ResultSet;importjava.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> 泛型中指定了JsonNode 的话,这个注解也可以省略 @MappedJdbcTypes(value =JdbcType.VARCHAR, includeNullJdbcType =true)@ComponentpublicclassJsonNodeTypeHandlerextendsBaseTypeHandler<JsonNode>implementsInitializingBean{staticJsonNodeTypeHandler j;@AutowiredObjectMapper objectMapper;/**
* 魔法 注入 单例bean objectMapper;
* 在 @Controller 中注入ObjectMapper 不需要这么麻烦,直接 @Autowired 即可 。
* 非Controller 注入原理:spring 启动过程中 实例化JsonNodeTypeHandler 的 bean 时,会自动把 objectMapper 携带过来;
* spring 启动完成后的bean 又会被擦除 。所以,这个要及时赋值一下引用 objectMapper
*/@OverridepublicvoidafterPropertiesSet(){
j =this;// 初始化静态实例
j.objectMapper =this.objectMapper;//及时拷贝引用}@OverridepublicvoidsetNonNullParameter(PreparedStatement ps,int i,JsonNode jsonNode,JdbcType jdbcType)throwsSQLException{
ps.setString(i, jsonNode !=null? jsonNode.toString():null);}@SneakyThrows@OverridepublicJsonNodegetNullableResult(ResultSet rs,String colName){returnread(rs.getString(colName));}@SneakyThrows@OverridepublicJsonNodegetNullableResult(ResultSet rs,int colIndex){returnread(rs.getString(colIndex));}@SneakyThrows@OverridepublicJsonNodegetNullableResult(CallableStatement cs,int i){returnread(cs.getString(i));}@SneakyThrowsprivateJsonNoderead(String json){return json !=null? j.objectMapper.readTree(json):null;}}
将 自定义的类型处理器 加入到 mybatis 核心配置,不用 xml
publicstaticSqlSessionFactorygetSqlSessionFactory(DataSource dataSource,String javaEntityPath,String xmlMapperLocation)throwsException{SqlSessionFactoryBean factoryBean =newSqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeAliasesPackage(javaEntityPath);//mybatis configurationorg.apache.ibatis.session.Configuration configuration =neworg.apache.ibatis.session.Configuration();// 下划线转驼峰
configuration.setMapUnderscoreToCamelCase(true);// 返回Map类型时,数据库为空的字段也要返回 https://www.cnblogs.com/guo-xu/p/12548949.html
configuration.setCallSettersOnNulls(true);// 配置 拦截器 打印 sql : TODO 补充 拦截器实现代码// configuration.addInterceptor(new PrintMybatisSqlInterceptor());
factoryBean.setConfiguration(configuration);ResourcePatternResolver resolver =newPathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources(xmlMapperLocation));// 自定义的类型处理器: 自动双向解析 JsonNode 类型 和 mysql中的 json; 千万别写xml 了,太low
factoryBean.setTypeHandlers(newJsonNodeTypeHandler());return factoryBean.getObject();}/**
* 每个类型的数据库连接,都需要单独创建1个SqlSessionFactory
* 比如mysql DataSource 需要 配置1个 @Bean SqlSessionFactory
* oracle 或者 hive 或者其他的数据库,都需要单独配置自己的 @Bean SqlSessionFactory
* 但是 ,getSqlSessionFactory() 这个静态方法是共用的,只需要修改对应entity和xml文件地址的参数 javaEntityPath 和 xmlMapperLocation 即可
*/@BeanpublicSqlSessionFactoryyourSqlSessionFactory(DataSource yourDataSource)throwsException{returngetSqlSessionFactory(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<JsonEntity> 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); */privateList<UnMappedColumnAutoMapping>createAutomaticMappings(ResultSetWrapper rsw,ResultMap resultMap,MetaObject metaObject,String columnPrefix)
- 这个createAutomaticMappings
方法,内部主要干了几件事- 找出resultset结果集中那些无法用 mybatis内置的类型处理器映射的字段名,比如json类型的 json_field- 将json_field
改为驼峰格式jsonField
,反射查找到Java对象中该属性为private JsonNode jsonField;``````publicStringfindProperty(String name,boolean useCamelCaseMapping){if(useCamelCaseMapping){ name = name.replace("_","");}returnfindProperty(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
<dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>${mapper.version}</version><!-- 4.0.4 --></dependency>
- 与手写sql 处理 json 字段的最大的不同点 :需要注解
@ColumnType
明确标注出来 json 字段 - 并且 JsonNodeTypeHandler 中不能有未知类型的泛型
(比如T)
,必须是 确定的已知的java类型 (比如JsonNode
)
importtk.mybatis.mapper.annotation.ColumnType;@Table(name="t_test_json")@Data@AllArgsConstructor@NoArgsConstructor@BuilderpublicclassJsonEntity{@IdprivateInteger id;// 多了这个ColumnType,通用mapper生成sql必须的;如果没有该注解,则最终生成sql时 该JsonNode类型的字段将会被忽略// 如果你没有使用 通用mapper,而是完全手写sql,那么完全没必要加该注解,mybatis的自动发现足咦!!@ColumnType(typeHandler =JsonNodeTypeHandler.class)privateJsonNode jsonField;@SneakyThrows@OverridepublicStringtoString(){returnJacksonUtils.writeValueAsString(this);}}
最终效果展示 ,增删改查测试 代码示例 :
查询并显示 json
- controller 层
@AutoWiredIDao dao;@ApiOperation(value ="查询 自动转换 JsonNode 和 json 类型,自动发现 typeHandler ")@PostMapping("test/json")publicResultBeantestJson(@RequestBody(required =false)JsonEntity vo){returnResultUtils.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<JsonEntity>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")publicResultBeantestUpdateJson(@RequestBodyJsonEntity vo){finalIJsonDao dao =SpringContextUtils.getBean(IJsonDao.class);// 通用mapper生成 sql UPDATE T_TEST_JSON SET id = id,json_field = {"hello":"world again!"} WHERE id = 1 ;finalint i = dao.updateByPrimaryKeySelective(vo);// 手动写 sql UPDATE t_test_json SET json_field = {"hello":"world again!"} where id = 1 ;returnDFResultUtils.oK(dao.testUpdateJson((vo)));}
- 通用mapper Dao 的写法
@Repository("jsonDao")publicinterfaceIJsonDaoextendsMyMapper<JsonEntity>{@Transactional(value =JK.transactionManager, rollbackFor =Exception.class)@Select(" UPDATE t_test_json SET json_field = #{vo.jsonField} where id = #{vo.id}")List<Integer>testUpdateJson(@Param("vo")JsonEntity vo);}/** MyMapper 通用mapper类 写法 public interface MyMapper<T> extends BaseMapper<T>, ExampleMapper<T>, ConditionMapper<T>, MySqlMapper<T> {}*/
- 更新后结果
源代码下载
参考文档
- mysql 正确比较 2个json 字段 的写法: JSON_CONTAINS(json_field, #{vo.jsonField})
版权归原作者 w1047667241 所有, 如有侵权,请联系我们删除。