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
, OrderItem
s and OrderSubItem
s. Each OrderItem
must have one or more OrderSubItem
s (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 OrderSubItem
s, 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 OrderItem
s 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?
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);
}
}