Search code examples
hibernatespring-data-jpajpa-2.1hibernate-cascade

Prevent delete from cascading to an entity instance


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?


Solution

  • 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.