当前项目中,需要将JSON字段存储至MySQL数据库中。项目的技术栈:Spring Boot 3.5.x、MyBatis-Plus 3.5.x、MySQL 8.x。在这个过程中,踩了一些坑,当前将实践经验整理出来。
核心结论:
单条完整更新:用
updateById。批量/条件/无需先查:
对象已转换为JSON字符串:用
update+setSql+CAST。对象未转换为JSON字符串:用
update+setSql+typeHandler参数。
禁止:对象已转换为JSON字符串时,使用
setSql + typeHandler参数。
1. 实体映射规范
JSON列必须:
MySQL列类型为
JSON;实体上
@TableName(autoResultMap = true);JSON字段标注
@TableField(typeHandler = JacksonTypeHandler.class)。
示例实体代码:
@Data
@TableName(value = "json_test", autoResultMap = true)
public class JsonTest {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, Object> mapData;
@TableField(typeHandler = JacksonTypeHandler.class)
private JsonNode jsonNodeData;
@TableField(typeHandler = JacksonTypeHandler.class)
private TestStructuredDataDTO structuredData;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
数据输入DTO示例:
@Data
@NoArgsConstructor
public class JsonTestDTO {
private String name;
private Map<String, Object> mapData;
private JsonNode jsonNodeData;
private TestStructuredDataDTO structuredData;
}
2. 三种更新策略与代码示例
服务实现关键方法如下:
1) updateById(简单直观,先查后改)
适用:单条更新,完整实体落库。
行为:
JacksonTypeHandler正确处理写/读,MySQL JSON列入库为JSON OBJECT。
@Transactional(rollbackFor = Exception.class)
public boolean updateJsonData(Long id, Map<String, Object> mapData, JsonNode jsonNodeData, TestStructuredDataDTO structuredData) {
JsonTest jsonTest = jsonTestMapper.selectById(id);
if (jsonTest == null) return false;
jsonTest.setMapData(mapData);
jsonTest.setJsonNodeData(jsonNodeData);
jsonTest.setStructuredData(structuredData);
int rows = jsonTestMapper.updateById(jsonTest);
return rows > 0;
}
2) LambdaUpdateWrapper + setSql + CAST(推荐的局部/条件/批量更新)
适用:无需先查、批量、部分字段更新。
关键:先
ObjectMapper序列化为字符串,再在SQL用CAST(? AS JSON)转为JSON类型。
@Transactional(rollbackFor = Exception.class)
public boolean updateJsonDataWithApply(Long id, Map<String, Object> mapData, JsonNode jsonNodeData, TestStructuredDataDTO structuredData) {
String mapDataJson = objectMapper.writeValueAsString(mapData);
String jsonNodeDataJson = objectMapper.writeValueAsString(jsonNodeData);
String structuredDataJson = objectMapper.writeValueAsString(structuredData);
LambdaUpdateWrapper<JsonTest> wrapper = Wrappers.<JsonTest>lambdaUpdate()
.eq(JsonTest::getId, id)
.setSql("map_data = CAST({0} AS JSON)", mapDataJson)
.setSql("json_node_data = CAST({0} AS JSON)", jsonNodeDataJson)
.setSql("structured_data = CAST({0} AS JSON)", structuredDataJson);
return jsonTestMapper.update(null, wrapper) > 0;
}
3) LambdaUpdateWrapper + setSql + typeHandler(推荐)
适用:无需先查、批量、部分字段更新。
优点:
JacksonTypeHandler正确处理写/读,MySQL JSON列入库为JSON OBJECT。
public boolean updateWithTypeHandler(Long id, Map<String, Object> mapData, JsonNode jsonNodeData,
TestStructuredDataDTO testStructuredDataDTO) {
// 方式:使用 setSql + typeHandler 参数(不使用 CAST)
LambdaUpdateWrapper<JsonTest> wrapper = Wrappers.<JsonTest>lambdaUpdate()
.eq(JsonTest::getId, id)
.setSql("map_data = {0,typeHandler=" + JacksonTypeHandler.class.getName() + "}", mapData)
.setSql("json_node_data = {0,typeHandler=" + JacksonTypeHandler.class.getName() + "}", jsonNodeData)
.setSql("structured_data = {0,typeHandler=" + JacksonTypeHandler.class.getName() + "}", testStructuredDataDTO);
return jsonTestMapper.update(null, wrapper) > 0;
}
```JSON
### 4) 反例:setSql + 对象JSON + typeHandler参数(禁止)
- 禁止用法:将字段预先转换为JSON,后续执行`setSql + typeHandler`写入数据库
- 写法:`setSql("structured_data = {0,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}", jsonString)`
```java
@Transactional(rollbackFor = Exception.class)
public boolean updateStructuredDataWithTypeHandler(Long id, TestStructuredDataDTO structuredData) {
String jsonString = objectMapper.writeValueAsString(structuredData);
LambdaUpdateWrapper<JsonTest> wrapper = Wrappers.<JsonTest>lambdaUpdate()
.eq(JsonTest::getId, id)
.setSql("structured_data = {0,typeHandler=" + JacksonTypeHandler.class.getName() + "}", jsonString);
return jsonTestMapper.update(null, wrapper) > 0; // 但会存成JSON STRING!
}
存在问题:
实际:如果将对象预先转换为JSON字符串,再执行
setSql + typeHandler写入数据库,写入的JSON字符串会带\转义符,影响后续的查询和业务处理;直接证据(MyBatis日志):
JDBC Connection [HikariProxyConnection@848004347 wrapping com.mysql.cj.jdbc.ConnectionImpl@7aa651ab] will be managed by Spring
==> Preparing: UPDATE json_test SET structured_data = ? WHERE (id = ?)
==> Parameters: "{\"title\":\"TypeHandler测试\",\"description\":\"使用TypeHandler参数方式进行更新\",\"score\":96,\"enabled\":true,\"tags\":[\"test\",\"json\",\"typehandler\"],\"metadata\":{\"author\":\"tester\",\"version\":\"1.0\",\"timestamp\":1700000000}}"(String), 1(Long)
<== Updates: 1
结果:数据库里对应记录的
structured_data值显示大量\(转义)。查询时期望对象(OBJECT),却拿到字符串(STRING),无法反序列化为目标DTO,导致应用层报错。
3. 验证与排查
验证字段真实类型
使用MySQL
JSON_TYPE区分存储是否为OBJECT/ARRAY/STRING/...:
SELECT id, JSON_TYPE(structured_data) AS type, structured_data
FROM json_test
WHERE id IN (1, 2);
期望:
CAST写入 →type = OBJECT,无反斜杠;typeHandler参数写入 →type = STRING,值形如"{\"key\":\"value\"}"。
常见问题与解决
问题:查询失败/值中出现大量反斜杠
\根因:使用了
setSql + typeHandler参数,JSON被当作字符串存入(JSON STRING)。修复:改为
setSql("col = CAST({0} AS JSON)", jsonString)或改回updateById。
问题:TypeHandler未生效
检查是否缺少
@TableName(autoResultMap = true);字段是否正确标注
@TableField(typeHandler = JacksonTypeHandler.class);数据库列类型必须为
JSON。
4. 项目建议(TL;DR)
映射:
autoResultMap = true+@TableField(JacksonTypeHandler.class)+ 列类型JSON。更新策略:
单条完整更新:用
updateById。批量/条件/无需先查:
对象已转换为JSON字符串:用
update+setSql+CAST。对象未转换为JSON字符串:用
update+setSql+typeHandler参数。
禁止:对象已转换为JSON字符串时,使用
setSql + typeHandler参数。
验证:
SELECT JSON_TYPE(col)确保为OBJECT/ARRAY,不是STRING。