Spring Boot中序列化和反序列化多态实体属性的理想方式是什么?

问题描述 投票:0回答:1

我有一个 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 ")?

spring-boot jpa java-8 jackson-databind
1个回答
0
投票

由于

.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 列,无需使用转换器

抽象记录.java

@NoArgsConstructor
@JsonTypeInfo(use = Id.DEDUCTION)
@JsonSubTypes({
    @Type(RecordType1.class),
    @Type(RecordType2.class)
})
@MappedSuperclass
public abstract class AbstractRecord implements Serializable {}

RecordType1.class、RecordType2.class 等

@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 根本不支持这一点。

BaseEntity.java

@Entity
@Data
public class BaseEntity implements Serializable {
  @Column
  @JdbcTypeCode(SqlTypes.JSON)
  private AbstractRecord record;

  @Column
  private String entity;
}
© www.soinside.com 2019 - 2024. All rights reserved.