我有一个 Entity 类,其列属性的类型是 抽象类。我想序列化(对象到 JSON 字符串),同时将其保存在数据库列中,并在从数据库检索它时将其反序列化为抽象类(反过来将字符串转换为适当的具体类)。
这是我完成它的方法:
ProductEntity.java
@Entity
@Table(name="PRODUCT")
@Data
public class ProductEntity{
@Id
@Column(name = "ID", insertable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private BigInteger id;
@Column(name = "DESCRIPTION")
private String description;
@Column(name = "NAME")
private String name;
@Column(name = "PRODUCT_TYPE")
private String productType;
@Column(name = "PRODUCT_SPECS")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property =
"productType") // -------------------> Map to concrete class based on productType value
@Convert(converter = ObjectConverter.class) // ------------> custom converter
private ProductSpecification productSpec;
}
注意:“Product_SPECS”数据库列是 JSON 类型。
产品规格.java
@NoArgsConstructor
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS,
include = JsonTypeInfo.As.WRAPPER_OBJECT,
@JsonSubTypes({
@JsonSubTypes.Type(value = ComputerSpecification.class, name = "computer"),
@JsonSubTypes.Type(value = SpeakerSpecification.class, name = "speaker")
})
public abstract class ProductSpecification{ }
计算机规范.java
@Getter
@Setter
@NoArgsConstructor
@JsonTypeName("computer")
public class ComputerSpecification extends ProductSpecification {
String memory;
String displaySize;
String processor;
@JsonCreator
public ComputerSpecification (@JsonProperty("memory") String memory,
@JsonProperty("displaysize") String displaySize,
@JsonProperty("processor") String processor){
super();
this.memory = memory;
this.displaySize = displaySize;
this.processor = processor;
}
}
SpeakerSpecification.java
@Getter
@Setter
@NoArgsConstructor
@JsonTypeName("computer")
public class SpeakerSpecification extends ProductSpecification {
String dimension;
String sensitivity;
String bassPrinciple;
String amplifierPower;
@JsonCreator
public SpeakerSpecification (@JsonProperty("sensitivity") String sensitivity,
@JsonProperty("dimension") String dimension,
@JsonProperty("bassPrinciple") String bassPrinciple,
@JsonProperty("amplifierPower") String amplifierPower){
super();
this.sensitivity = sensitivity;
this.dimension = dimension;
this.bassPrinciple = bassPrinciple;
this.amplifierPower = amplifierPower;
}
}
ObjectConverter.java
注意:我使用 Jackson ObjectMapper 进行序列化和反序列化。
public class ObjectConverter implements AttributeConverter<Object, String>{
private final static Logger LOGGER = LoggerFactory.getLogger(ObjectConverter.class);
private static final ObjectMapper mapper;
static {
mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_NULL);
}
@Override
public String convertToDatabaseColumn(Object attributeObject) {
if (attributeObject == null) {
return "";
}
try {
return mapper.writeValueAsString(attributeObject);
} catch (JsonProcessingException e) {
LOGGER.error("Could not convert to database column", e);
return null;
}
}
@Override
public Object convertToEntityAttribute(String dbColumnValue) {
try {
if (StringUtils.isBlank(dbColumnValue)) {
return null;
}
return mapper.readValue(dbColumnValue, ProductSpecification.class); // ----> mapped to
abstract class
} catch (Exception e) {
LOGGER.error("Could not convert to entity attribute", e);
return null;
}
}
}
请求正文1:
{
"name" : "Bose Bass Module 700 - Black- Wireless, Compact Subwoofer",
"description" : "This wireless, compact subwoofer is designed to be paired with the Bose sound
bar 700 to bring music, movies, and TV to life with Deep, dramatic bass. ",
"productSpec" : {
"sensitivity" : "90 dB",
"bassPrinciple" : "reflex",
"amplifierPower" : "700 watts",
"dimension" : "14-5/16inW x 42-13/16inH x 16-5/16inD"
}
}
此请求将保存在数据库列“Product_SPECS”中:
{".SpeakerSpecification ":{"sensitivity" : "90 dB","bassPrinciple" : "reflex", "amplifierPower" :"700
watts", "dimension" : "14-5/16inW x 42-13/16inH x 16-5/16inD" }}
现在这个解决方案工作得很好。 “SpeakerSpecification”键既不会出现在 GET API 调用的响应中,也不会出现在 swagger 文档中。但是必须将类型信息存储在数据库中确实让我很困扰。
是否有更好的方法来解决此问题,可以避免在列值中包含 typeinfo (".SpeakerSpecification ")?
由于
.SpeakerSpecification:
中的此注释,您可能会获得 ProductSpecification
前缀:@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, include = JsonTypeInfo.As.WRAPPER_OBJECT)
特别是,我认为
JsonTypeInfo.As.WRAPPER_OBJECT
正在推动它。 您可以尝试使用 @JsonTypeInfo(use = Id.DEDUCTION)
。 只要 @JsonSubTypes
中的每个 JSON 类型都有不同的字段,Jackson 就应该能够仅根据字段找出要反序列化为的对象。
我能够使用与您非常相似的设置来实现此功能 - 我们只是将 JSONB 存储在名为
record
的列中。 我们确实将类型标签包含在名为 entity
的另一列中(就像您一样),但这甚至不需要 Jackson 来发挥其魔力。 请注意,我使用的是 Hibernate 6.5,它支持 JSONB 列,无需使用转换器。
@NoArgsConstructor
@JsonTypeInfo(use = Id.DEDUCTION)
@JsonSubTypes({
@Type(RecordType1.class),
@Type(RecordType2.class)
})
@MappedSuperclass
public abstract class AbstractRecord implements Serializable {}
@Data
@Embeddable
public class RecordType1 extends AbstractRecord {
// you only need these two annotations if you want your JSON field names to differ from their Java counterparts
@JsonProperty(name = "field_name1")
@Column(name = "field_name1")
private String fieldName1;
}
现在,如何定义基本实体(您将其称为
ProductEntity
,我将简单地称为 BaseEntity
)取决于您。 如果您需要能够使用 JPQL 查询 JSONB 字段本身,您将需要设置一个继承结构,其中多个实体类扩展一个抽象实体类(对于所有其他字段),然后映射到同一个表,每个表都带有record
字段定义为该列中的特定类型。 您还可以在这些类上使用 @SQLRestriction
来过滤您的 productType
字段,这简化了这些类的存储库/服务逻辑。
如果您不需要这个,您应该能够从子类型类中删除
@Embeddable
注释,并从基本抽象记录类中删除 @MappedSuperclass
注释。
您还可以在此处使用泛型
T
来扩展 AbstractRecord
来代替 AbstractRecord
,但请记住,您不能够针对来自泛型的字段名称编写 JPQL 查询。 Hibernate 根本不支持这一点。
@Entity
@Data
public class BaseEntity implements Serializable {
@Column
@JdbcTypeCode(SqlTypes.JSON)
private AbstractRecord record;
@Column
private String entity;
}