工作中,某些需求可能会出现根据用户所选择的套餐不同 ,需要输入套餐不同选项值,这些选项每个套餐中都不尽相同,这些数据都需要存入库中,举个例子:
不同的商品类别有完全不同的属性。
数据项(用户选择) :商品类别(如:手机、图书、衣服) 不同属性:
- 手机:颜色、内存、存储容量、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
对应数据库表的字段名。