2

Consider the following JPA entities:

@Entity @Table(name = "product") class Product { ... }

@Entity @Table(name = "stock") class Stock {
  @JoinColumn(name = "product_id", updatable = false)
  @ManyToOne(fetch = FetchType.LAZY)
  private Product product;

  @Column(name = "quantity")
  private Long quantity;
}

@Entity @Table(name = "cart") class Cart {
  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "cart", orphanRemoval = true)
  private List<CartItem> items = new ArrayList<>();

  public void addItem(CartItem item) { items.add(item); }
  public void removeItem(CartItem item) { items.remove(item); }
}

@Entity @Table(name = "cart_item") class CartItem {
  @JoinColumn(name = "cart_id", updatable = false)
  @ManyToOne(fetch = FetchType.LAZY)
  private Cart cart;

  @JoinColumn(name = "product_id", updatable = false)
  @ManyToOne(fetch = FetchType.LAZY)
  private Product product;

  @Column(name = "quantity")
  private Long quantity;

  @JoinColumn(name = "stock_id", updatable = false)
  @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  private Stock stock;

  public void setQuantity(Long quantity) {
    final Long delta = this.quantity - Math.max(0L, quantity);

    this.quantity += delta;
    this.stock.setQuantity(this.stock.getQuantity() - delta);

    if(quantity < 1) { cart.removeItem(this); }
  }
}

Notice the association from CartItem to Stock. This association has been annotated such that changing the cart item quantity affects its available stock in the other direction, that is, if the cart item quantity is increased, the available stock quantity for the product decreases and vice-versa.

This allows me to fire cartRepository.save(cart), saving all cart items and updating their stock at the same time (due to the Cascade.ALL from Cart to CartItem). This works fine as long as a cart item has a non-zero quantity.

However, when cartRepository.save(cart) is invoked after invoking cart.removeItem(item), the cascade attempts to delete the stock for the cart item as well, which is not the intent. The cart item should be deleted but its associated stock should simply be updated. Is there a way to cascade updates from CartItem to Stock but cascade deletes on CartItem as updates on Stock?

manish
  • 19,695
  • 5
  • 67
  • 91

2 Answers2

7

Changing

class CartItem {
  @JoinColumn(name = "stock_id", updatable = false)
  @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  private Stock stock;
}

to

class CartItem {
  @JoinColumn(name = "stock_id", updatable = false)
  @ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST }, fetch = FetchType.LAZY)
  private Stock stock;
}

(CascadeType.ALL to { CascadeType.MERGE, CascadeType.PERSIST }) solved the problem. Previously the following SQL queries were executed when cartItem.setQuantity(0) was invoked:

delete from cart_item where id=?
delete from stock where id=?

With the change the following SQL queries are executed, as required:

update stock set quantity=? where id=?
delete from cart_item where id=?

Sample application is available on Github for checking correctness of the solution.

manish
  • 19,695
  • 5
  • 67
  • 91
1

Your approach is very problematic to say the least. What happens if multiple threads modify the quantity of the same product? JPA does not guard against concurrent access! And even if you won't run into synchronization issues technically, you will have them logically because the (persistent) quantity will either be overwritten with wrong values (or you get an OptimisticLockException).

You should avoid such headache and treat quantity as the dynamic value it is and calculate the current quantity of a product using a select statement.

If you insist on caching quantities you should use a separate service for this task which uses proper synchronization, a concurrent map or the like.

Robin
  • 3,226
  • 2
  • 20
  • 27