如何在保存之前避免实体中包含元素的列表但在持续时间失败?

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

我需要允许用户编辑订单,包括在这些订单项下添加新的订单项和子项。我相应地为三个实体建模,OrderOrderItems 和 OrderSubItems。每个 OrderItem 必须有一个或多个 OrderSubItem(为简洁起见,省略了额外的实体道具):

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

}

当用户请求更新订单时说要添加新的 OrderItem 和新的 OrderSubItems,调用以下控制器:

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

当用户发送:

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

验证通过控制器。然后,调用服务:

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

就在保存之前,我可以看到每个订单项都有一个订单子项。但是保存失败并显示 ConstraintViolationException 抱怨新添加的订单项的订单子项为空。

为什么验证失败?我想这可能与双向关系和列表的操作有关。

更新 1

我创建了一个自定义的 @NotNull 验证器,这样我就可以检查 OrderItem 的内容,你瞧,它 orderSubItems 在持续验证期间为空。

更新 2

经过更多研究,我发现我添加到 OrderItem 中的任何 @OneToMany 属性 都只会被 Hibernate 似乎创建的一个对象设为 null。没错,看起来 Hibernate 创建了一个 OrderItem。关系是否是双向的并不重要。列表是否被操纵也无关紧要。其他属性填充良好。现在,这是奇怪的部分:

如果我在 Order 级别编写自定义验证器以检查其 OrderItem 是否具有非空 orderSubItems 并删除 NotNull orderSubItems 上的注释,它顺利通过了验证和保存。

也就是说,Hibernate 创建的 OrderItem 似乎没有附加到 Order 对象。这是 Hibernate 为先保存 OrderItem,然后从数据库中获取其 ID 以便保存其 orderSubItems 所做的临时步骤吗?如果是这样,为什么它会调用验证?

有时我更喜欢使用 DTO 来自定义和仅公开我需要的字段,而不是将我的实体公开给 API 的客户。我认为当您在其他对象中有数据时,处理实体会更容易。
例如:

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