Search code examples
springhibernatejpabean-validationbidirectional

How to avoid a List in entity containing element before save but failing at persist time?


I need to allow users to edit orders which include adding new order items and sub-items under those order items. I have modeled three entities accordingly, Order, OrderItems and OrderSubItems. Each OrderItem must have one or more OrderSubItems (omitting additional entity props for brevity):

@Entity
@Table(name="[Order]")
@Data
public class Order {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)
    @JoinColumn(name="orderId", nullable=false)
    @Valid
    @NotNull(message="order.orderItems.notNullorEmpty")
    @Size(min=1, message="order.orderItems.notNullorEmpty")
    private List<OrderItem> orderItems;
}

@Entity
@Data
public class OrderItem {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)
    @JoinColumn(name="orderId", nullable=false)
    @Valid
    @NotNull(message="order.orderSubItems.notNullorEmpty")
    @Size(min=1, message="order.orderSubItems.notNullorEmpty")
    private List<OrderSubItem> ordersSubItems;

    @ToString.Exclude
    @JsonIgnore
    @ManyToOne
    @JoinColumn(name="orderId", insertable=false, updatable=false)
    private Order order;

}

@Entity
@Data
public class OrderSubItem {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @ToString.Exclude
    @JsonIgnore
    @ManyToOne
    @JoinColumn(name="orderId", insertable=false, updatable=false)
    private OrderItem orderItem;

}

When a user request update of the order say to add a new OrderItem with new OrderSubItems, the following controller is called:

@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
@Validated
@Slf4j
public class OrderController {

    @NonNull
    private final OrderService orderService;

    @PutMapping
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void update(@Valid @RequestBody Order order) {
        orderService.update(order);
    }
}

When the user sends:

Order(id=1, orderItems=[
    OrderItem(id=null, orderSubItems=[OrderSubItem(id=null, value=1)]), 
    OrderItem(id=1, orderSubItems=[OrderSubItem(id=1, value=2)]) 

validation passes here at the controller. Then, the service is called:

@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {

    @Transactional
    public void update(Order order) {

        List<OrderItem> orderItems = order.getOrderItems();
        for (OrderItem orderItem : orderItems) {
            List<OrderSubItem> orderSubItems = orderItem.getOrderSubItems();
            List<OrderSubItem> newOrderSubItems = new ArrayList<OrderSubItem>();
            Collections.reverse(orderSubItems);
            for (OrderSubItem orderSubItem : orderSubItems) {

                if (orderSubItem.getValue() == null) {

                    //skip it                   
                } else {

                    orderSubItem.setIndex(newOrderSubItems.size());
                    newOrderSubItems.add(orderSubItem);

                }
            }
            orderItem.setOrderSubItems(newOrderSubItems);
            //also tried:
            // orderItem.getOrderSubItems().clear();
            // orderItem.getOrderSubItems().addAll(newOrderSubItems);
        }
        log.debug(order);   
        orderRepo.save(order);
}

Just before the save, I can see that every order item has a order sub item. But the save fails with a ConstraintViolationException complaining that the newly added order item has null for its order sub items.

Why is validating failing? I think it may have something to do with the bi-directional relationship and the manipulation of the list.

Update 1

I created a custom @NotNull validator so I could examine the contents of OrderItem and lo and behold it orderSubItems is null during validation at persist time.

Update 2

After more research, I found that any @OneToMany property that I add to OrderItem will be null by only one an object that Hibernate appears to create. That's right, it appears that Hibernate creates an OrderItem. It doesn't matter if the relationship is bi-directional or not. It also doesn't matter if the list is manipulated or not. Other properties are populated fine. Now, here's the weird part:

If I write a custom validator at the Order level to check that its OrderItems have non-null orderSubItems and remove the NotNull annotation on orderSubItems, it passes validation and its saves without issue.

That is, the OrderItem that Hibernate creates doesn't appear to be attached to the Order object. Is this an interim step that Hibernate does to save the OrderItem first, then get its id from the database so that it can save its orderSubItems? If so, why does it call validation?


Solution

  • Sometimes I prefer to use DTOs to customize and expose only fields that I need instead of exposing my entities to my API's clients. I think it's easier to handle entities when you have data in other objects.
    For instance:

    @Data
    class OrderDTO {
        private Long orderId;
        private List<ItemDTO> items;
    }
    
    public void update(@Valid @RequestBody OrderDTO orderDTO) {
        Order order = repository.load(orderDTO.getId());
        for (OrderItem orderItem : order.getItems()) {
             repository.delete(orderItem);
        }
        order.getItems().clear();
    
        for (ItemDTO newItem : orderDTO.getItems()) {
            OrderItem orderItem = new OrderItem(newItem.getFieldA(), newItem.getFieldB());
            orderItem.setOrder(order);
            repository.save(orderItem);
            order.getItems().add(orderItem);
        }
    }