2团
Published on 2025-12-15 / 0 Visits
0
0

MyBatis-Plus + MySQL JSON 最佳实践指南

当前项目中,需要将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. 验证与排查

验证字段真实类型

  • 使用MySQLJSON_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


Comment