MySQL JSON字段存储 + Java对应 + MyBatis Plus 增删改查全教程

本文完整覆盖:MySQL JSON字段创建、Java实体对应类型、MyBatis Plus 自动序列化/反序列化、增删改查实战样例(含JSON字段条件查询),代码可直接复制使用。

一、MySQL 如何存储 JSON 字段

1. 前提要求

MySQL 5.7及以上版本 原生支持 JSON 数据类型,相比字符串存储优势:

  • 自动校验JSON格式合法性
  • 支持高效JSON字段查询、索引
  • 存储空间更优

2. 建表语句(JSON字段定义)

直接用 JSON 关键字声明字段即可,示例创建用户表,info 为JSON字段:

CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `info` json DEFAULT NULL COMMENT '用户详情JSON',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

二、MySQL JSON字段 对应 Java 什么类型?

方案1:String 类型(简单但不推荐)

直接用 String 接收JSON字符串,需要手动序列化/反序列化,操作繁琐。

private String info; // 对应MySQL JSON字段

方案2:自定义对象 + MyBatis Plus 类型处理器(✅ 推荐)

MyBatis Plus 提供自动序列化/反序列化能力,将JSON字段直接映射为Java对象,面向对象操作,无需手动处理JSON。

  • 对应Java类型:自定义实体类
  • 依赖MP自带的 JacksonTypeHandler(基于Jackson自动转换)

三、环境准备(Maven依赖)

SpringBoot + MyBatis Plus 核心依赖:

<!-- SpringBoot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- MyBatis Plus Boot Starter -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

<!-- MySQL驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- Lombok 简化代码 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

四、MyBatis Plus 实战代码

1. 定义JSON对应的Java嵌套对象

对应MySQL info JSON字段的结构:

import lombok.Data;

/**
 * JSON字段对应的Java实体
 * 对应MySQL info字段: {"age":20,"address":"北京","phone":"13800138000"}
 */
@Data
public class UserInfo {
    private Integer age;
    private String address;
    private String phone;
}

2. 主实体类(核心:JSON字段映射)

关键注解:

  • @TableName(autoResultMap = true):必须开启,否则类型处理器不生效
  • @TableField(typeHandler = JacksonTypeHandler.class):指定JSON字段的类型处理器
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import java.util.Date;

@Data
// 开启自动结果映射(必须)
@TableName(value = "user", autoResultMap = true)
public class User {
    // 主键自增
    @TableId(type = IdType.AUTO)
    private Long id;

    private String username;

    // JSON字段:绑定类型处理器,自动转换Java对象 <-> MySQL JSON
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;

    private Date createTime;
}

3. Mapper & Service 层(MP原生接口)

Mapper接口
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}
Service接口
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserService extends IService<User> {
}
Service实现类
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

五、MyBatis Plus 增删改查 完整样例

直接在测试类/Controller中调用,JSON字段自动处理,无需手动转JSON字符串。

1. 新增(插入带JSON字段的数据)

@SpringBootTest
class MybatisPlusJsonTest {
    @Resource
    private UserService userService;

    @Test
    void addUser() {
        // 1. 组装JSON对象
        UserInfo info = new UserInfo();
        info.setAge(20);
        info.setAddress("北京市朝阳区");
        info.setPhone("13800138000");

        // 2. 组装主实体
        User user = new User();
        user.setUsername("张三");
        user.setInfo(info); // 直接赋值Java对象
        user.setCreateTime(new Date());

        // 3. 新增(MP自动将info转为JSON存入MySQL)
        boolean save = userService.save(user);
        System.out.println("新增结果:" + save);
    }
}

2. 修改(更新JSON字段)

@Test
void updateUser() {
    // 1. 新的JSON数据
    UserInfo newInfo = new UserInfo();
    newInfo.setAge(21);
    newInfo.setAddress("上海市浦东新区");
    newInfo.setPhone("13900139000");

    // 2. 更新实体
    User user = new User();
    user.setId(1L);
    user.setInfo(newInfo); // 直接更新JSON对象

    // 3. 修改(MP自动更新MySQL JSON字段)
    boolean update = userService.updateById(user);
    System.out.println("修改结果:" + update);
}

3. 查询(普通查询 + JSON字段条件查询)

3.1 普通查询(自动返回Java对象)
@Test
void selectUser() {
    // 根据ID查询
    User user = userService.getById(1L);
    // 直接获取JSON对应的Java对象,无需反序列化
    UserInfo info = user.getInfo();
    System.out.println("用户信息:" + user);
    System.out.println("JSON字段解析:年龄=" + info.getAge() + ",地址=" + info.getAddress());
}
3.2 条件查询(查询JSON字段内部值)

使用MySQL JSON函数查询:

  • 字段->'$.属性':推荐写法
  • JSON_EXTRACT(字段, '$.属性'):原生写法

示例:查询 info 中 age=21 的用户

@Test
void selectByJsonCondition() {
    // 构造查询条件:info.age = 21
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    // 重点:MySQL JSON字段条件查询语法
    wrapper.apply("info->'$.age' = {0}", 21);

    // 执行查询
    List<User> userList = userService.list(wrapper);
    System.out.println("JSON条件查询结果:" + userList);
}

4. 删除

@Test
void deleteUser() {
    // 根据ID删除
    boolean remove = userService.removeById(1L);
    System.out.println("删除结果:" + remove);
}

六、关键注意事项

  1. 必须开启 autoResultMap = true
    实体类的 @TableName 注解必须加这个参数,否则类型处理器无法生效,会导致JSON字段映射失败。

  2. 类型处理器选择

    • JacksonTypeHandler:MP默认,依赖Jackson(无需额外配置)
    • FastjsonTypeHandler:若用Fastjson,需替换依赖+注解
  3. JSON字段空值处理
    直接赋值 null 即可,MP会自动存储为 NULL。

  4. JSON字段索引
    若需要频繁查询JSON内部字段,可给MySQL JSON字段创建函数索引,提升查询效率。


总结

  1. MySQL用**JSON类型**存储JSON数据;
  2. Java对应自定义对象,配合MP的JacksonTypeHandler自动转换;
  3. MyBatis Plus 增删改查直接操作Java对象,无需手动处理JSON字符串;
  4. JSON字段条件查询使用 字段->'$.属性' 语法。
package com.example.demo.utils;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.reflection.property.PropertyNamer;

import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.util.Collection;

/**
 * 纯手写 JSON Lambda 工具类
 * 【完全独立】不依赖 MyBatis Plus 内部 API,仅保留 MP 注解作为可选兼容
 * 解决 3.5.x 版本 API 重构导致的报错
 */
public class JsonLambdaUtils {

    /**
     * 核心方法:双Lambda生成MySQL JSON查询语句
     * @param entityLambda  主实体Lambda(如 User::getInfo)
     * @param jsonLambda    JSON对象Lambda(如 UserInfo::getAge)
     * @return 示例:info->'$.age'
     */
    public static <T, J> String json(SFunction<T, J> entityLambda, SFunction<J, ?> jsonLambda) {
        // 1. 解析主实体 Lambda -> 数据库列名
        String entityColumnName = getEntityColumnName(entityLambda);

        // 2. 解析 JSON 对象 Lambda -> 属性名
        String jsonFieldName = getLambdaFieldName(jsonLambda);

        // 3. 拼接 MySQL JSON 语法
        return String.format("%s->'$.%s'", entityColumnName, jsonFieldName);
    }

    /**
     * 【核心独立实现】通过 SerializedLambda 解析实体字段名
     */
    private static <T> String getEntityColumnName(SFunction<T, ?> func) {
        try {
            // 1. 强制序列化获取 SerializedLambda (核心黑魔法)
            SerializedLambda lambda = getSerializedLambda(func);

            // 2. 获取 Lambda 对应的方法名 (例如: getInfo)
            String methodName = lambda.getImplMethodName();

            // 3. 方法名转属性名 (getInfo -> info)
            String fieldName = PropertyNamer.methodToProperty(methodName);

            // 4. 获取实体类类型 (例如: com.example.demo.entity.User)
            // 注意:这里直接用 getInstantiatedMethodType() 获取类描述符,或者强制转换
            // 为了兼容不同版本,我们直接通过反射获取泛型类型
            Class<?> entityClass = getFunctionalInterfaceClass(lambda);

            // 5. 从 MP 表信息中获取最终的数据库列名 (保留 MP 注解适配,如需纯手写可替换逻辑)
            TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
            if (tableInfo == null) {
                throw new RuntimeException("未找到实体类 [" + entityClass.getName() + "] 的表信息,请检查注解");
            }

            // 6. 根据属性名匹配数据库列名 (支持 @TableField 注解的列名映射)
            Collection<TableFieldInfo> fieldList = tableInfo.getFieldList();
            for (TableFieldInfo fieldInfo : fieldList) {
                if (fieldInfo.getProperty().equals(fieldName)) {
                    return fieldInfo.getColumn();
                }
            }

            // 如果找不到,直接返回属性名(默认情况)
            return fieldName;

        } catch (Exception e) {
            throw new RuntimeException("解析实体 Lambda 失败", e);
        }
    }

    /**
     * 解析 JSON 对象的字段名
     */
    private static <J> String getLambdaFieldName(SFunction<J, ?> func) {
        try {
            SerializedLambda lambda = getSerializedLambda(func);
            String methodName = lambda.getImplMethodName();
            return PropertyNamer.methodToProperty(methodName);
        } catch (Exception e) {
            throw new RuntimeException("解析 JSON Lambda 失败", e);
        }
    }

    /**
     * 【核心独立工具】获取 SerializedLambda
     * 这是 Java 标准 API,不依赖任何第三方框架
     */
    private static SerializedLambda getSerializedLambda(Serializable func) {
        try {
            // 通过反射调用 writeReplace 方法,获取序列化代理对象
            java.lang.reflect.Method method = func.getClass().getDeclaredMethod("writeReplace");
            method.setAccessible(true);
            return (SerializedLambda) method.invoke(func);
        } catch (Exception e) {
            throw new RuntimeException("获取 SerializedLambda 失败,请确保 SFunction 是可序列化的", e);
        }
    }

    /**
     * 【核心独立工具】通过 Lambda 获取实体类类型
     */
    private static Class<?> getFunctionalInterfaceClass(SerializedLambda lambda) {
        // 方案1: 尝试从 implMethodName 或其他字段推断 (较复杂)
        // 方案2: 更简单可靠的方式 - 强制转换并通过泛型提取 (推荐)
        // 这里我们直接通过类加载器获取实例化的类
        String implClass = lambda.getImplClass().replace("/", ".");
        try {
            return Class.forName(implClass);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("加载实体类失败: " + implClass, e);
        }
    }

    // ====================== 新增:JSON解引用表达式(用于字符串匹配) ======================
    public static <T, J> String jsonUnquote(SFunction<T, J> entityLambda, SFunction<J, ?> jsonLambda) {
        String column = getEntityColumnName(entityLambda);
        String field = getLambdaFieldName(jsonLambda);
        // ->> :MySQL解引用,去掉JSON字符串的引号,必须用于like/字符串eq
        return String.format("%s->>'$.%s'", column, field);
    }

    // ====================== 新增:封装eq/like/in通用方法 ======================
    /**
     * JSON 等于查询
     * @param wrapper 查询包装器
     * @param entityLambda 实体JSON字段(如 User::getInfo)
     * @param jsonLambda JSON内部字段(如 UserInfo::getAge)
     * @param value 匹配值
     */
    public static <T, J> void jsonEq(LambdaQueryWrapper<T> wrapper,
                                     SFunction<T, J> entityLambda,
                                     SFunction<J, ?> jsonLambda,
                                     Object value) {
        String expr = json(entityLambda, jsonLambda);
        wrapper.apply(expr + " = ?", value);
    }

    /**
     * JSON 模糊查询(字符串)
     */
    public static <T, J> void jsonLike(LambdaQueryWrapper<T> wrapper,
                                       SFunction<T, J> entityLambda,
                                       SFunction<J, ?> jsonLambda,
                                       String value) {
        String expr = jsonUnquote(entityLambda, jsonLambda);
        wrapper.apply(expr + " LIKE ?", "%" + value + "%");
    }

    /**
     * JSON IN 查询
     */
    public static <T, J> void jsonIn(LambdaQueryWrapper<T> wrapper,
                                     SFunction<T, J> entityLambda,
                                     SFunction<J, ?> jsonLambda,
                                     Collection<?> values) {
        String expr = json(entityLambda, jsonLambda);
        wrapper.apply(expr + " IN ({0})", values);
    }

    /**
     * 通用函数式接口,用于替代 MP 的 SFunction (如果不想引入 MP 依赖)
     * 请将此接口放在独立的包下,例如 com.example.demo.function
     */
    @FunctionalInterface
    public interface SFunction<T, R> extends Serializable {
        R apply(T t);
    }
}
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐