文章目录
- 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,不要每次都手写,要 自动化识别
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@Builderpublic class JsonEntity{ @Id private Integer id; // 为何不是 ArrayNode 或者 ObjectNode ? // 因为 JsonNode 是他们俩的父类,可以自动兼容2种格式的json : [{},{}] 和 {} private JsonNode jsonField; @SneakyThrows @Override public 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;// @MappedTypes(JsonNode.class) // 因为BaseTypeHandler 泛型中指定了JsonNode 的话,这个注解也可以省略 @MappedJdbcTypes(value = JdbcType.VARCHAR, includeNullJdbcType = true)@Componentpublic class JsonNodeTypeHandler extends BaseTypeHandler<JsonNode> implements InitializingBean { static JsonNodeTypeHandler j; @Autowired ObjectMapper objectMapper; @Override public void afterPropertiesSet() { j = this; // 初始化静态实例 j.objectMapper = this.objectMapper; //及时拷贝引用 } @Override public void setNonNullParameter(PreparedStatement ps, int i, JsonNode jsonNode, JdbcType jdbcType) throws SQLException { ps.setString(i, jsonNode != null ? jsonNode.toString() : null); } @SneakyThrows @Override public JsonNode getNullableResult(ResultSet rs, String colName) { return read(rs.getString(colName)); } @SneakyThrows @Override public JsonNode getNullableResult(ResultSet rs, int colIndex) { return read(rs.getString(colIndex)); } @SneakyThrows @Override public JsonNode getNullableResult(CallableStatement cs, int i) { return read(cs.getString(i)); } @SneakyThrows private 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 configuration org.apache.ibatis.session.Configuration configuration = new org.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 = new PathMatchingResourcePatternResolver(); factoryBean.setMapperLocations(resolver.getResources(xmlMapperLocation)); // 自定义的类型处理器: 自动双向解析 JsonNode 类型 和 mysql中的 json; 千万别写xml 了,太low factoryBean.setTypeHandlers(new JsonNodeTypeHandler()); return factoryBean.getObject();}@Beanpublic SqlSessionFactory yourSqlSessionFactory(DataSource yourDataSource) throws Exception { return getSqlSessionFactory(yourDataSource,"com.server.model.entity.testpath","classpath:mapper/testpathprivate List<UnMappedColumnAutoMapping> 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
<dependency> <groupId>tk.mybatisgroupId> <artifactId>mapperartifactId> <version>${mapper.version}version>dependency>
- 与手写sql 处理 json 字段的最大的不同点 :需要注解
@ColumnType
明确标注出来 json 字段 - 并且 JsonNodeTypeHandler 中不能有未知类型的泛型
(比如T)
,必须是 确定的已知的java类型 (比如JsonNode
)
import tk.mybatis.mapper.annotation.ColumnType;@Table(name="t_test_json")@Data@AllArgsConstructor@NoArgsConstructor@Builderpublic class JsonEntity{ @Id private Integer id; // 多了这个ColumnType,通用mapper生成sql必须的;如果没有该注解,则最终生成sql时 该JsonNode类型的字段将会被忽略 // 如果你没有使用 通用mapper,而是完全手写sql,那么完全没必要加该注解,mybatis的自动发现足咦!! @ColumnType(typeHandler = JsonNodeTypeHandler.class) private JsonNode jsonField; @SneakyThrows @Override public String toString() { return JacksonUtils.writeValueAsString(this); }}
最终效果展示 ,增删改查测试 代码示例 :
查询并显示 json
- controller 层
@AutoWiredIDao 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 "{ 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: *@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: *
-
更新后结果
源代码下载
参考文档
来源地址:https://blog.csdn.net/w1047667241/article/details/127697481