工作中,某些需求可能会出现根据用户所选择的套餐不同,需要输入套餐不同选项值,这些选项每个套餐中都不尽相同,这些数据都需要存入库中,举个例子:
不同的商品类别有完全不同的属性。
数据项(用户选择):商品类别(如:手机、图书、衣服) 不同属性:
- 手机:颜色、内存、存储容量、CPU型号
- 图书:作者、出版社、ISBN、页数
- 衣服:尺码、颜色、材质、季节
核心是处理动态的、可变的实体属性。根据数据项(或类型)的不同,实体拥有一组不同的属性。这种模式通常被称为 EAV(实体-属性-值)模型 或其改良方案。
实现方案:一个字段用于判断所属类型,另一个字段存储对应的json数据。
POM依赖
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>3.5.6version>
parent>
<properties>
<maven.compiler.source>17maven.compiler.source>
<maven.compiler.target>17maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-spring-boot3-starterartifactId>
<version>3.5.14version>
dependency>
<dependency>
<groupId>org.postgresqlgroupId>
<artifactId>postgresqlartifactId>
<version>42.7.8version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.42version>
dependency>
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
dependency>
dependencies>
公用类
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;
}
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;
}
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,并重写相关方法。
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
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);
} }}
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 getData() {
return (T) data;
}}
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 {
}
JPA 实现
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
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);
} }
}
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 getData() {
return (T) data;
}
}
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 {
}
启动类
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 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 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参数注入相关参数的属性值。
@Type(value = ProductTypeConverter.class,parameters = {@
org.hibernate.annotations.Parameter(name = DynamicTypeConverter.FLAG_FIELD,value = "type")})
@Slf4j
public abstract class DynamicTypeConverter implements UserType, ParameterizedType {
@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对应数据库表的字段名。