简体   繁体   中英

Spring Jackson deserialize object with reference to existing object by Id

I have three entity classes of the following:

Shipments Entity:

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

Shipment Details Entity:

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

Product Entity:

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

I am receiving JSONs through a rest API. The problem is I do not know how to deserialize a new Shipment with shipmentDetails that have relationships to already existing objects just by ID. I know you can simply deserialize with the objectmapper, but that requires all the fields of product to be in each shipmentDetail. How do i instantiate with just the productID?

Sample JSON received

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

Currently my rest endpoint would then receive the JSON, and do this:

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

what i want to do is:

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

I tried this solution to no. avail: Deserialize with Jackson with reference to an existing object

How do I make the product entity search for an existing product instead of trying to create a new product. The hacky extremely inefficient workaround I have been using is to traverse the json array, and for every productId find the product using the productRepository, and then set the shipmentDetail with the product one by one. Im not sure if this is best practice as im self learning spring.

So in pseudocode what im trying to do would be:

  1. Receive JSON
  2. Instantiate Shipment entity
  3. Instantiate an array of shipmentDetail entities For each shipmentDetail: 1. Find product with given productId 2. Instantiate shipmentDetail with product and shipment

Code has been significantly simplified to better showcase the problem,

I think your current approach is not a bad solution, you are dealing with the problem correctly and in few steps.

Any way, one thing you can try is the following.

The idea will be to provide a new field, productId , defined on the same database column that supports the relationship with the Product entity, something like:

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

The product field must be annotated as not insertable and not updatable : on the contrary, Hibernate will complaint about which field should be used to maintain the relationship with the Product entity, in other words, to maintain the actual column value.

Modify the Shipment relationship with ShipmentDetail as well to propagate persistence operations (adjust the code as per your needs):

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

Then, you can safely rely on the Spring+Jackson deserialization and obtain a reference to the received Shipment object:

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

This approach has an obvious drawback, the existence of the Product is not checked beforehand.

Although you can ensure data integrity at database level with the use of foreign keys, perhaps it would be convenient to validate that the information is right before perform the actual insertion:

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

You have a bottleneck in your code in this part:

Product product = productRepository.findById(productId)

Because you are making a query for each productId, and it will perform badly with large number of products. Ignoring that, I will recommend this aproach.

  1. Build your own deserializer (see 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. Add the deserializer to your Shipment class:

     @JsonDeserialize(using = ShipmentDeserializer.class) public class Shipment { // Class code }
  3. Deserialize the string:

     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. At this point, you are only converting the Json into classes, so we need to persist the data.

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

I know you want to reduce the code in the test method, but I DO NOT RECOMMEND to combine the Json deserialize with the persistence layer. But if you want to follow that path, you could move the productRepository.findById(productId) into the ShipmentDeserializer class like 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 = 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;
        }
    }

But if you want to do that, you need to inject the repository into the deserializer (see this ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM