数据库字段多类型Json值处理

工作中,某些需求可能会出现根据用户所选择的套餐不同 ,需要输入套餐不同选项值,这些选项每个套餐中都不尽相同,这些数据都需要存入库中,举个例子:

不同的商品类别有完全不同的属性。

数据项(用户选择)​ ​:商品类别(如:手机、图书、衣服) ​​不同属性​​:

  • ​手机​:颜色、内存、存储容量、CPU型号
  • ​图书​:作者、出版社、ISBN、页数
  • ​衣服​:尺码、颜色、材质、季节

核心是处理​​动态的、可变的实体属性​ ​。根据数据项(或类型)的不同,实体拥有一组不同的属性。这种模式通常被称为 ​​EAV(实体-属性-值)模型​​ 或其改良方案。

实现方案:一个字段用于判断所属类型,另一个字段存储对应的json数据。

POM依赖

xml 复制代码
<parent>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>3.5.6</version>  
</parent>  
<properties>  
    <maven.compiler.source>17</maven.compiler.source>  
    <maven.compiler.target>17</maven.compiler.target>  
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>  
</properties>  
<dependencies>  
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
    <dependency>  
        <groupId>com.baomidou</groupId>  
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>  
        <version>3.5.14</version>  
    </dependency>  
    <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->  
    <dependency>  
        <groupId>org.postgresql</groupId>  
        <artifactId>postgresql</artifactId>  
        <version>42.7.8</version>  
    </dependency>  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-data-jpa</artifactId>  
    </dependency>  
    <dependency>  
        <groupId>org.projectlombok</groupId>  
        <artifactId>lombok</artifactId>  
        <version>1.18.42</version>  
    </dependency>  
    <dependency>  
        <groupId>com.zaxxer</groupId>  
        <artifactId>HikariCP</artifactId>  
    </dependency>  
</dependencies>

公用类

java 复制代码
package com.polaris.json.dto;  
  
import lombok.Data;  
  
import java.io.Serializable;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */@Data  
public class Book implements Serializable {  
  
    private String author;  
    private String publisher;  
    private String isbn;  
}
java 复制代码
package com.polaris.json.dto;  
  
import lombok.Data;  
  
import java.io.Serializable;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */@Data  
public class Phone implements Serializable {  
  
    private String color;  
    private String memory;  
    private String storage;  
}
java 复制代码
package com.polaris.json.enums;  
  
import com.polaris.json.dto.Book;  
import com.polaris.json.dto.Phone;  
import lombok.Getter;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */@Getter  
public enum ProductType {  
    /**  
     * 产品类型  
     */  
    PHONE(Phone.class),  
  
    BOOK(Book.class);  
  
    private final Class<?> resolveType;  
  
    ProductType(Class<?> resolveType) {  
        this.resolveType = resolveType;  
    }}

Mybatis 实现

通过继承org.apache.ibatis.type.BaseTypeHandler,并重写相关方法。

java 复制代码
package com.polaris.json.mybatis.entity;  
  
import com.baomidou.mybatisplus.core.toolkit.StringUtils;  
import com.fasterxml.jackson.core.JsonProcessingException;  
import com.fasterxml.jackson.databind.ObjectMapper;  
import lombok.extern.slf4j.Slf4j;  
import org.apache.ibatis.type.BaseTypeHandler;  
import org.apache.ibatis.type.JdbcType;  
import org.postgresql.util.PGobject;  
  
import java.sql.CallableStatement;  
import java.sql.PreparedStatement;  
import java.sql.ResultSet;  
import java.sql.SQLException;  
import java.util.Objects;  
  
/**  
 * 多态类型转换  
 *  
 * @author SilverGravel  
 * @since 2025/9/24  
 */@Slf4j  
public abstract class DynamicTypeHandler extends BaseTypeHandler<Object> {  
  
    protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();  
  
  
    @Override  
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {  
        try {  
            if (parameter == null) {  
                ps.setObject(i, null);  
                return;  
            }            // postgresql 处理json  
            PGobject pGobject = new PGobject();  
            pGobject.setType("json");  
            pGobject.setValue(OBJECT_MAPPER.writeValueAsString(parameter));  
            ps.setObject(i, pGobject);  
        } catch (JsonProcessingException e) {  
            throw new RuntimeException(e);  
        }    }  
  
    @Override  
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {  
        final String json = rs.getString(columnName);  
        String type = rs.getString(getFlagTypeName());  
        return getObject(json, type, rs.wasNull());  
    }  
  
    @Override  
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {  
        final String json = rs.getString(columnIndex);  
        final String type = rs.getString(getFlagTypeName());  
        return getObject(json, type, rs.wasNull());  
    }  
    @Override  
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {  
        final String json = cs.getString(columnIndex);  
        final String type = cs.getString(getFlagTypeName());  
        boolean wasNull = cs.wasNull();  
        return getObject(json, type, wasNull);  
  
    }  
    private Object getObject(String json, String type, boolean wasNull) {  
        if (StringUtils.isBlank(json) && wasNull) {  
            return null;  
        }        if (Objects.isNull(type)) {  
            log.warn(getFlagTypeName() + " 字段值为空,不做转换");  
            return null;  
        }        return parse(json, type);  
    }  
    /**  
     * 解析数据累心  
     *  
     * @param json json值  
     * @param type 枚举值  
     * @return 返回解析的类型  
     */  
    protected abstract Object parse(String json, String type);  
  
    protected String getFlagTypeName() {  
        return "type";  
    }  
}
java 复制代码
package com.polaris.json.mybatis.entity;  
  
import com.fasterxml.jackson.core.JsonProcessingException;  
import com.polaris.json.enums.ProductType;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */public class ProductTypeHandler extends DynamicTypeHandler {  
  
  
    @Override  
    protected Object parse(String json, String type) {  
        if (type == null) {  
            return null;  
        }        ProductType typeEnum = ProductType.valueOf(type);  
        try {  
            return OBJECT_MAPPER.readValue(json, typeEnum.getResolveType());  
        } catch (JsonProcessingException e) {  
            throw new RuntimeException(e);  
        }    }}
java 复制代码
package com.polaris.json.mybatis.entity;  
  
import com.baomidou.mybatisplus.annotation.TableField;  
import com.baomidou.mybatisplus.annotation.TableId;  
import com.baomidou.mybatisplus.annotation.TableName;  
import com.polaris.json.enums.ProductType;  
import lombok.Data;  
  
import java.io.Serializable;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */@Data  
@TableName(value = "products",autoResultMap = true)  
public class Product implements Serializable {  
  
    @TableId  
    private String id;  
  
    private ProductType type;  
  
    private String name;  
  
    @TableField(value = "data",typeHandler = ProductTypeHandler.class)  
    private Object data;  
  
    @SuppressWarnings("unchecked")  
    public <T> T getData() {  
        return (T) data;  
    }}
java 复制代码
package com.polaris.json.mybatis.mapper;  
  
import com.baomidou.mybatisplus.core.mapper.BaseMapper;  
import com.polaris.json.mybatis.entity.Product;  
import org.apache.ibatis.annotations.Mapper;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */
@Mapper  
public interface ProductMapper extends BaseMapper<Product> {  
}

JPA 实现

java 复制代码
package com.polaris.json.jpa.entity;  
  
import com.baomidou.mybatisplus.core.toolkit.StringUtils;  
import com.fasterxml.jackson.core.JsonProcessingException;  
import com.fasterxml.jackson.databind.ObjectMapper;  
import lombok.extern.slf4j.Slf4j;  
import org.hibernate.engine.spi.SharedSessionContractImplementor;  
import org.hibernate.type.SqlTypes;  
import org.hibernate.usertype.ParameterizedType;  
import org.hibernate.usertype.UserType;  
import org.postgresql.util.PGobject;  
import org.springframework.util.ObjectUtils;  
  
import java.io.Serializable;  
import java.sql.PreparedStatement;  
import java.sql.ResultSet;  
import java.sql.SQLException;  
import java.util.Objects;  
import java.util.Properties;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */@Slf4j  
public abstract class DynamicTypeConverter implements UserType<Object>, ParameterizedType {  
  
    protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();  
  
    public static final String FLAG_FIELD = "flagField";  
  
    private String field;  
  
    @Override  
    public int getSqlType() {  
        return SqlTypes.JSON;  
    }  
    @Override  
    public Class<Object> returnedClass() {  
        return Object.class;  
    }  
    @Override  
    public Object nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException {  
        final String json = rs.getString(position);  
        final String type = rs.getString(getFlagTypeName());  
        if (StringUtils.isBlank(json) && rs.wasNull()) {  
            return null;  
        }        if (Objects.isNull(type)) {  
            log.warn(getFlagTypeName() + " 字段值为空,不做转换");  
            return null;  
        }        return parse(json, type);  
    }  
    /**  
     * 解析数据累心  
     *  
     * @param json json值  
     * @param type 枚举值  
     * @return 返回解析的类型  
     */  
    protected abstract Object parse(String json, String type);  
  
    protected String getFlagTypeName() {  
        if (field == null) {  
            return "type";  
        }        return field;  
    }  
  
    @Override  
    public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws SQLException {  
  
        try {  
            if (value == null) {  
                st.setNull(index, SqlTypes.JSON);  
                return;  
            }            // postgresql 处理json  
            PGobject pGobject = new PGobject();  
            pGobject.setType("json");  
            pGobject.setValue(OBJECT_MAPPER.writeValueAsString(value));  
            st.setObject(index, pGobject);  
        } catch (JsonProcessingException e) {  
            throw new RuntimeException(e);  
        }    }  
    @Override  
    public boolean equals(Object x, Object y) {  
        return Objects.equals(x, y);  
    }  
    @Override  
    public int hashCode(Object x) {  
        return Objects.hashCode(x);  
    }  
    @Override  
    public Object deepCopy(Object value) {  
        return value;  
    }  
    @Override  
    public boolean isMutable() {  
        return true;  
    }  
    @Override  
    public Serializable disassemble(Object value) {  
        return (Serializable) deepCopy(value);  
    }  
    @Override  
    public Object assemble(Serializable cached, Object owner) {  
        return deepCopy( cached);  
    }  
  
    @Override  
    public void setParameterValues(Properties parameters) {  
        if (ObjectUtils.isEmpty(parameters)) {  
            return;  
        }        if (parameters.containsKey(FLAG_FIELD)) {  
            this.field = parameters.getProperty(FLAG_FIELD);  
        }    }}
java 复制代码
package com.polaris.json.jpa.entity;  
  
import com.fasterxml.jackson.core.JsonProcessingException;  
import com.polaris.json.enums.ProductType;  
import org.hibernate.type.SqlTypes;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */public class ProductTypeConverter extends DynamicTypeConverter{  
  
  
    @Override  
    protected Object parse(String json, String type) {  
        if (type == null) {  
            return null;  
        }        ProductType typeEnum = ProductType.valueOf(type);  
        try {  
            return OBJECT_MAPPER.readValue(json, typeEnum.getResolveType());  
        } catch (JsonProcessingException e) {  
            throw new RuntimeException(e);  
        }    }  
  
}
java 复制代码
package com.polaris.json.jpa.entity;  
  
import com.polaris.json.enums.ProductType;  
import jakarta.persistence.*;  
import lombok.Data;  
import org.hibernate.annotations.Type;  
  
import java.io.Serializable;  
  
/**  
 * @author  
 */  
@Entity  
@Table(name = "products")  
@Data  
public class Product implements Serializable {  
  
    @Id  
    private String id;  
  
    @Enumerated(EnumType.STRING)  
    @Column(name = "type")  
    private ProductType type;  
  
    @Column(name = "name")  
    private String name;  
  
    @Type(value = ProductTypeConverter.class,parameters = {@org.hibernate.annotations.Parameter(name = DynamicTypeConverter.FLAG_FIELD,value = "type")})  
    private Object data;  
  
    @SuppressWarnings("unchecked")  
    public <T> T getData() {  
        return (T) data;  
    }  
}
java 复制代码
package com.polaris.json.jpa.repository;  
  
import com.polaris.json.jpa.entity.Product;  
import org.springframework.data.jpa.repository.JpaRepository;  
import org.springframework.stereotype.Repository;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */@Repository  
public interface ProductRepository extends JpaRepository<Product, String> {  
}

启动类

java 复制代码
package com.polaris.json;  
  
import com.baomidou.mybatisplus.core.toolkit.Wrappers;  
import com.polaris.json.dto.Book;  
import com.polaris.json.dto.Phone;  
import com.polaris.json.enums.ProductType;  
import com.polaris.json.jpa.repository.ProductRepository;  
import com.polaris.json.mybatis.entity.Product;  
import com.polaris.json.mybatis.mapper.ProductMapper;  
import org.springframework.boot.*;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.boot.builder.SpringApplicationBuilder;  
import org.springframework.context.ConfigurableApplicationContext;  
  
import java.util.Arrays;  
import java.util.List;  
  
/**  
 * @author SilverGravel  
 * @since 2025/10/4  
 */@SpringBootApplication  
public class JsonDataApplication  {  
    public static void main(String[] args) {  
        ConfigurableApplicationContext context = new SpringApplicationBuilder()  
                .web(WebApplicationType.NONE)  
                .sources(JsonDataApplication.class)  
                .run(args);  
        ProductMapper productMapper = context.getBean(ProductMapper.class);  
        productMapper.delete(Wrappers.emptyWrapper());  
        productMapper.insert(mybatis());  
        System.out.println(productMapper.selectList(Wrappers.emptyWrapper()));  
  
        ProductRepository repository = context.getBean(ProductRepository.class);  
        repository.deleteAll();  
        repository.saveAllAndFlush(jpa());  
        System.out.println(repository.findAll());  
  
  
    }  
    private static List<Product> mybatis() {  
        Product product = new Product();  
        product.setData(phone());  
        product.setId("1");  
        product.setType(ProductType.PHONE);  
        product.setName("手机");  
  
        Product book = new Product();  
        book.setId("2");  
        book.setType(ProductType.BOOK);  
        book.setName("图书");  
        book.setData(book());  
  
        return Arrays.asList(product, book);  
    }  
  
    private static List<com.polaris.json.jpa.entity.Product> jpa() {  
        com.polaris.json.jpa.entity.Product product = new com.polaris.json.jpa.entity.Product();  
        product.setData(phone());  
        product.setId("1");  
        product.setType(ProductType.PHONE);  
        product.setName("手机");  
  
        com.polaris.json.jpa.entity.Product book = new com.polaris.json.jpa.entity.Product();  
        book.setId("2");  
        book.setType(ProductType.BOOK);  
        book.setName("图书");  
        book.setData(book());  
  
        return Arrays.asList(product, book);  
    }  
    private static Phone phone() {  
        Phone phone = new Phone();  
        phone.setColor("丁香紫");  
        phone.setMemory("16G");  
        phone.setStorage("512G");  
        return phone;  
    }  
    private static Book book() {  
        Book book = new Book();  
        book.setAuthor("Silver");  
        book.setPublisher("Publisher");  
        book.setIsbn("383838");  
        return book;  
    }}

总结

两者的核心实现都需要有 java.sql.ResultSet,通过该接口获取行集数据 ,通过 rs.getString等方法获取type字段的数据。JPA的 Type注解可以使用parameters参数注入相关参数的属性值。

java 复制代码
@Type(value = ProductTypeConverter.class,parameters = {@
org.hibernate.annotations.Parameter(name = DynamicTypeConverter.FLAG_FIELD,value = "type")})
java 复制代码
@Slf4j  
public abstract class DynamicTypeConverter implements UserType<Object>, ParameterizedType {
java 复制代码
@Override  
public void setParameterValues(Properties parameters) {  
    if (ObjectUtils.isEmpty(parameters)) {  
        return;  
    }    if (parameters.containsKey(FLAG_FIELD)) {  
        this.field = parameters.getProperty(FLAG_FIELD);  
    }}

可以实现 org.hibernate.usertype.ParameterizedType接口来获取 不同实体type对应数据库表的字段名。

相关推荐
李贺梖梖2 小时前
Tomcat的CATALINA_BASE
java·tomcat
0wioiw02 小时前
Java基础(①Tomcat + Servlet + JSP)
java·servlet·tomcat
用户4099322502122 小时前
PostgreSQL处理SQL居然像做蛋糕?解析到执行的4步里藏着多少查询优化的小心机?
后端·ai编程·trae
李贺梖梖2 小时前
Tomcat&Http协议
java·http·tomcat
每天一个java小知识3 小时前
Spring-AI 接入(本地大模型 deepseek + 阿里云百炼 + 硅基流动)
java·人工智能·spring
代码匠心3 小时前
从零开始学Flink:数据输出的终极指南
java·大数据·后端·flink
掘根3 小时前
【Qt】多线程
java·开发语言·qt
IT_陈寒3 小时前
SpringBoot性能调优实战:5个让接口响应速度提升300%的关键配置
前端·人工智能·后端
xcLeigh4 小时前
Python操作国产金仓数据库(KingbaseES)全流程:搭建连接数据库的API接口
后端