Spring Jackson 通过 Id 引用现有对象反序列化对象

Spring Jackson deserialize object with reference to existing object by Id

我有以下三个实体 类:

发货实体:

@Entity
@Table(name = "SHIPMENT")
public class Shipment implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "SHIPMENT_ID", nullable = false)
    private int shipmentId;

    @Column(name = "DESTINATION", nullable = false)
    private String destination;

    @OneToMany(mappedBy = "shipment")
    private List<ShipmentDetail> shipmentDetailList;
    
//bunch of other variables omitted

    public Shipment(String destination) {
        this.destination = destination;
        shipmentDetailList = new ArrayList<>();
    }

货件详细信息实体:

@Entity
@Table(name = "SHIPMENT_DETAIL")
public class ShipmentDetail implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "SHIPMENT_DETAIL_ID", nullable = false)
    private int shipmentDetailId;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID", nullable = false)
    private Product product;

    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "SHIPMENT_ID", nullable = false)
    private Shipment shipment;

//bunch of other variables omitted 


    public ShipmentDetail() {
    }

    public ShipmentDetail(Shipment shipment, Product product) {
        this.product = product;
        this.shipment = shipment;
    }

产品实体:

@Entity
@Table(name = "Product")
public class Product implements Serializable {

    @Id
    @Column(name = "PRODUCT_ID", nullable = false)
    private String productId;

    @Column(name = "PRODUCT_NAME", nullable = false)
    private String productName;

//bunch of other variables omitted 

    public Product() {
    }

    public Product(String productId, String productName) {
        this.productId = productId;
        this.productName = productName;
    }

我通过休息 API 接收 JSONs。问题是我不知道如何反序列化带有 shipmentDetails 的新 Shipment,这些 ShipmentDetails 仅通过 ID 与现有对象有关系。我知道您可以简单地使用 objectmapper 反序列化,但这需要产品的所有字段都在每个 shipmentDetail 中。我如何仅使用 productID 进行实例化?

样本JSON收到

{
    "destination": "sample Dest",
    "shipmentDetails": [
        {
            "productId": "F111111111111111"
        },
        {
            "productId": "F222222222222222"
        }
    ]
}

目前我的休息端点将收到 JSON,然后执行此操作:

public ResponseEntity<String> test(@RequestBody String jsonString) throws JsonProcessingException {
        JsonNode node = objectMapper.readTree(jsonString);
        String destination = node.get("destination").asText();
        Shipment newShipment = new Shipment(destination);
        shipmentRepository.save(newShipment);

        JsonNode shipmentDetailsArray = node.get("shipmentDetails");
        int shipmentDetailsArrayLength = shipmentDetailsArray.size();
        for (int c = 0; c < shipmentDetailsArrayLength; c++) {
            String productId = node.get("productId").asText();
            Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
            ShipmentDetail shipmentDetail = new ShipmentDetail(newShipment, product, quantity);
            shipmentDetailRepository.save(shipmentDetail);
        }
    }

我想做的是:

public ResponseEntity<String> test2(@RequestBody String jsonString) throws JsonProcessingException {
    
    JsonNode wholeJson = objectMapper.readTree(jsonString);
    Shipment newShipment = objectMapper.treeToValue(wholeJson, Shipment.class);
    
    return new ResponseEntity<>("Transfer Shipment successfully created", HttpStatus.OK);
}

我试过这个解决方案没有。效用:

如何让产品实体搜索现有产品而不是尝试创建新产品。我一直使用的 hacky 极其低效的解决方法是遍历 json 数组,并为每个 productId 使用 productRepository 查找产品,然后将 shipmentDetail 与产品一一设置。我不确定这是否是我自学的最佳实践 spring。

所以在伪代码中我试图做的是:

  1. 收到JSON
  2. 实例化 Shipment 实体
  3. 实例化一组 shipmentDetail 实体 对于每批货物详细信息: 1. 查找具有给定 productId 的产品 2. 用产品和装运实例化 shipmentDetail

代码已显着简化以更好地展示问题,

我认为你目前的方法不是一个糟糕的解决方案,你正在正确地处理问题并且只需要几个步骤。

无论如何,您可以尝试以下方法。

我们的想法是提供一个新字段 productId,定义在支持与 Product 实体关系的同一数据库列上,例如:

@Entity
@Table(name = "SHIPMENT_DETAIL")
public class ShipmentDetail implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "SHIPMENT_DETAIL_ID", nullable = false)
    private int shipmentDetailId;

    @Column(name = "PRODUCT_ID", nullable = false)
    private String productId;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID", insertable = false, updatable = false)
    private Product product;

    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "SHIPMENT_ID", nullable = false)
    private Shipment shipment;

//bunch of other variables omitted 


    public ShipmentDetail() {
    }

    public ShipmentDetail(Shipment shipment, Product product) {
        this.product = product;
        this.shipment = shipment;
    }
}

product字段必须注解为not insertable而不是updatable:相反,Hibernate会抱怨应该使用哪个字段来维护与Product实体,换句话说,保持实际的列值。

修改 ShipmentShipmentDetail 的关系以传播持久性操作(根据您的需要调整代码):

@OneToMany(mappedBy = "shipment", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ShipmentDetail> shipmentDetailList;

然后,您可以安全地依赖 Spring+Jackson 反序列化并获得对接收到的 Shipment 对象的引用:

public ResponseEntity<String> processShipment(@RequestBody Shipment shipment) {
  // At this point shipment should contain the different details,
  // each with the corresponding productId information

  // Perform the validations required, log information, if necessary

  // Save the object: it should persist the whole object tree in the database
  shipmentRepository.save(shipment);
}

这种方法有一个明显的缺点,Product 的存在没有事先检查。

虽然您可以使用外键确保数据库级别的数据完整性,但在执行实际插入之前验证信息是否正确可能会很方便:

public ResponseEntity<String> processShipment(@RequestBody Shipment shipment) {
  // At this point shipment should contain the different details,
  // each with the corresponding productId information

  // Perform the validations required, log information, if necessary
  List<ShipmentDetail> shipmentDetails = shipment.getShipmentDetails();
  if (shipmentDetails == null || shipmentDetails.isEmpty()) {
    // handle error as appropriate
    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No shipment details provided");
  }

  shipmentDetails.forEach(shipmentDetail -> {
    String productId = shipmentDetail.getProductId();
    Product product = productRepository.findById(productId).orElseThrow(
      () -> new ResponseStatusException(HttpStatus.NOT_FOUND, 
            "No product with ID of: " + productId + " exists!")
    )
  });

  // Everything looks fine, save the object now
  shipmentRepository.save(shipment);
}

你这部分的代码有瓶颈:

Product product = productRepository.findById(productId)

因为您正在为每个 productId 进行查询,并且它会在大量产品时表现不佳。忽略这一点,我会推荐这种方法。

  1. 构建您自己的解串器(参见this):

    public class ShipmentDeserializer extends JsonDeserializer {
         @Override
         public Shipment deserialize(JsonParser jp, DeserializationContext ctxt)
                 throws IOException, JsonProcessingException {
             JsonNode node = jp.getCodec().readTree(jp);
             String destination = node.get("destination").asText();
             Shipment shipment = new Shipment(destination);
             JsonNode shipmentDetailsNode = node.get("shipmentDetails");
             List shipmentDetailList = new ArrayList();
             for (int c = 0; c < shipmentDetailsNode.size(); c++) {
                 JsonNode productNode = shipmentDetailsNode.get(c);
                 String productId = productNode.get("productId").asText();
                 Product product = new Product(productId);
                 ShipmentDetail shipmentDetail = new ShipmentDetail(product);
                 shipmentDetailList.add(shipmentDetail);
             }
             shipment.setShipmentDetailList(shipmentDetailList);
             return shipment;
         }
     }
    
  2. 将解串器添加到您的 Shipment class:

     @JsonDeserialize(using = ShipmentDeserializer .class)
     public class Shipment {
         // Class code
     }
    
  3. 反序列化字符串:

    
     public ResponseEntity test2(@RequestBody String jsonString) throws JsonProcessingException {
         Shipment newShipment = objectMapper.readValue(jsonString, Shipment.class);
         /* More code */
         return new ResponseEntity("Transfer Shipment successfully created", HttpStatus.OK);
     }
     
    
  4. 此时,您只是将Json转换为class,所以我们需要持久化数据。

    
     public ResponseEntity test2(@RequestBody String jsonString) throws JsonProcessingException {
         Shipment newShipment = objectMapper.readValue(jsonString, Shipment.class);
         shipmentRepository.save(newShipment);
         List<ShipmentDetail> shipmentDetails = newShipment.getShipmentDetailList();
         for (int i = 0; i < shipmentDetails.size(); c++) {
             ShipmentDetail shipmentDetail = shipmentDetails.get(i);
             shipmentDetail.setShipment(newShipment);
             Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
             shipmentDetail.setProduct(product);
             shipmentDetailRepository.save(shipmentDetail);
         }
         return new ResponseEntity("Transfer Shipment successfully created", HttpStatus.OK);
     }
     
    

我知道您想减少测试方法中的代码,但是我不建议将Json反序列化与持久层结合起来。但是,如果您想遵循该路径,则可以将 productRepository.findById(productId) 移动到 ShipmentDeserializer class 中,如下所示:

public class ShipmentDeserializer extends JsonDeserializer {
        @Override
        public Shipment deserialize(JsonParser jp, DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            JsonNode node = jp.getCodec().readTree(jp);
            String destination = node.get("destination").asText();
            Shipment shipment = new Shipment(destination);
            JsonNode shipmentDetailsNode = node.get("shipmentDetails");
            List shipmentDetailList = new ArrayList();
            for (int c = 0; c < shipmentDetailsNode.size(); c++) {
                JsonNode productNode = shipmentDetailsNode.get(c);
                String productId = productNode.get("productId").asText();
                Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
                ShipmentDetail shipmentDetail = new ShipmentDetail(product);
                shipmentDetailList.add(shipmentDetail);
            }
            shipment.setShipmentDetailList(shipmentDetailList);
            return shipment;
        }
    }

但是如果你想这样做,你需要将存储库注入反序列化器(参见 this)。