【问题标题】:Spring Jackson deserialize object with reference to existing object by IdSpring Jackson 通过 Id 引用现有对象反序列化对象
【发布时间】:2021-06-03 14:30:56
【问题描述】:

我有以下三个实体类:

发货实体:

@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;
    }

我正在通过 REST API 接收 JSON。问题是我不知道如何使用仅通过 ID 与现有对象有关系的 shippingDetails 反序列化新 Shipment。我知道您可以简单地使用 objectmapper 进行反序列化,但这需要 product 的所有字段都在每个 shippingDetail 中。如何仅使用 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);
}

我试过这个解决方案没有。利用: Deserialize with Jackson with reference to an existing object

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

所以在伪代码中我想要做的是:

  1. 接收 JSON
  2. 实例化发货实体
  3. 实例化一系列 shippingDetail 实体 每批货物详情: 1. 查找具有给定 productId 的产品 2. 用 product 和 shipping 实例化 shippingDetail

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

【问题讨论】:

    标签: java json spring jackson deserialization


    【解决方案1】:

    您在这部分的代码中遇到了瓶颈:

    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 类中:

       @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 转换为类,因此我们需要对数据进行持久化。

      
       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 类中,如下所示:

    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)。

    【讨论】:

    • 感谢您的回答,但考虑到瓶颈,那么避免这种情况的最佳做法是什么?如何在不每次都查询数据库的情况下验证和错误检查收到的 productId?
    • 我认为最好的方法是避免对每个产品进行查询。只做 1 次查询。为此,我会将每个产品 ID 存储在一个数组中,然后调用一个新方法 productRepository.findInList(productsIds),相当于 SELECT * FROM PRODUCTS WHERE PRODUCT_ID IN (productsIds)。然后,您将拥有所有数据库现有产品,并且您可以检查您最初存储的每个 productId 是否都在此产品数组中。这会使代码更复杂一些,但您将有一个查询来检查现有产品
    • 嗨,再次感谢您的进一步解释。因此,如果我做对了,执行上述所有操作的理想且有效的方法是: 1. 接收 JSON 2. 使用自定义反序列化器序列化货件和 shippingDetails 3. 通过以下方式序列化 shippingDetails 数组: 1. 将每个 productId 提取到大批。 2. 使用方法取回产品列表并检查产品是否存在 3. 创建 shippingDetails 列表 4. 在自定义反序列化器之外的一个数据库调用中持久化 shipping 和 shippingDetails。
    • 对,你明白了。当您在一个 json 中收到数十个产品时,单个查询将很重要,但如果您只收到几个产品,那么您可能不会注意到太大的差异。重要的是: 1. 尝试从反序列化层解耦数据库,将函数分配给不同的类。 2. 尽量不要执行几十个查询,因为这样很没有效率
    • 好的,我尝试制作自定义反序列化器,问题是我无法将 productRepository 注入自定义反序列化器,因为自定义反序列化器不是 bean。知道怎么做吗?
    【解决方案2】:

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

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

    我们的想法是提供一个新字段 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字段必须注释为notinsertable而不是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);
    }
    

    【讨论】:

      猜你喜欢
      • 2014-03-11
      • 2017-04-26
      • 2020-08-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-12
      • 1970-01-01
      • 2021-02-18
      相关资源
      最近更新 更多