So I am trying to implement a short e-commerce kind of project using SpringBoot and everything. As per my model, every user
will have a cart
with a list of items (CartItems
in my case), and these items will consist of a particular product
.
I am adding only the relevant fields in these classes
CartItem.java
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CartItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cart_item_id")
private Long id;
@ManyToOne
@JoinColumn(name = "cart_id")
private Cart cart;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
}
Cart.java
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Cart {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cart_id")
private Long id;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "cart", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, orphanRemoval = true)
private List<CartItem> cartItems = new ArrayList<>();
private Double totalPrice = 0.0;
}
Product.java
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id;
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
@OneToMany(mappedBy = "product", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, fetch = FetchType.EAGER)
private List<CartItem> products = new ArrayList<>();
}
Everything is working fine until I try to delete a product
from the cart
. With the endpoint, what I want should happen is that the cart
's total price should change, and the corresponding cartItem
that was holding that product
in that specific cart
should get deleted.
Below is the controller code -
@DeleteMapping("/carts/{cartId}/product/{productId}")
public ResponseEntity<String> deleteProductFromCart(@PathVariable Long cartId,
@PathVariable Long productId) {
String status = cartService.deleteProductFromCart(cartId, productId);
return new ResponseEntity<>(status, HttpStatus.OK);
}
Below is the service implementation -
@Transactional
@Override
public String deleteProductFromCart(Long cartId, Long productId) {
Cart cart = cartRepository.findById(cartId)
.orElseThrow(() -> new ResourceNotFoundException("Cart", "id", cartId));
CartItem cartItem = cartItemRepository.findCartItemByProductIdAndCartId(cartId, productId);
if (cartItem == null) {
throw new ResourceNotFoundException("Product", "id", productId);
}
cart.setTotalPrice(cart.getTotalPrice() -
(cartItem.getPrice() * cartItem.getQuantity()));
// Code #1
// cart.getCartItems().remove(cartItem);
// cartRepository.save(cart);
// Code #2
// cartItemRepository.delete(cartItem);
// Code #3
// cartItemRepository.deleteCartItemByProductIdAndCartId(cartId, productId);
return "Product " + cartItem.getProduct().getName() + " removed from the cart !!!";
}
The custom repository method -
@Modifying
@Query("DELETE FROM CartItem ci WHERE ci.cart.id = ?1 AND ci.product.id = ?2")
void deleteCartItemByProductIdAndCartId(Long cartId, Long productId);
Out of the 3 blocks of code, only the 3rd one seems to be deleting the cart item from the database. Why is that??
As per my understanding, code block 1 should work because of orphanRemoval = true
in the entity class and code block 2 should work because I am directly deleting the cartItem
that I fetched from the database.
I believe I am missing some fundamental thing. Please help!
So I am trying to implement a short e-commerce kind of project using SpringBoot and everything. As per my model, every user
will have a cart
with a list of items (CartItems
in my case), and these items will consist of a particular product
.
I am adding only the relevant fields in these classes
CartItem.java
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CartItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cart_item_id")
private Long id;
@ManyToOne
@JoinColumn(name = "cart_id")
private Cart cart;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
}
Cart.java
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Cart {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cart_id")
private Long id;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "cart", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, orphanRemoval = true)
private List<CartItem> cartItems = new ArrayList<>();
private Double totalPrice = 0.0;
}
Product.java
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id;
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
@OneToMany(mappedBy = "product", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, fetch = FetchType.EAGER)
private List<CartItem> products = new ArrayList<>();
}
Everything is working fine until I try to delete a product
from the cart
. With the endpoint, what I want should happen is that the cart
's total price should change, and the corresponding cartItem
that was holding that product
in that specific cart
should get deleted.
Below is the controller code -
@DeleteMapping("/carts/{cartId}/product/{productId}")
public ResponseEntity<String> deleteProductFromCart(@PathVariable Long cartId,
@PathVariable Long productId) {
String status = cartService.deleteProductFromCart(cartId, productId);
return new ResponseEntity<>(status, HttpStatus.OK);
}
Below is the service implementation -
@Transactional
@Override
public String deleteProductFromCart(Long cartId, Long productId) {
Cart cart = cartRepository.findById(cartId)
.orElseThrow(() -> new ResourceNotFoundException("Cart", "id", cartId));
CartItem cartItem = cartItemRepository.findCartItemByProductIdAndCartId(cartId, productId);
if (cartItem == null) {
throw new ResourceNotFoundException("Product", "id", productId);
}
cart.setTotalPrice(cart.getTotalPrice() -
(cartItem.getPrice() * cartItem.getQuantity()));
// Code #1
// cart.getCartItems().remove(cartItem);
// cartRepository.save(cart);
// Code #2
// cartItemRepository.delete(cartItem);
// Code #3
// cartItemRepository.deleteCartItemByProductIdAndCartId(cartId, productId);
return "Product " + cartItem.getProduct().getName() + " removed from the cart !!!";
}
The custom repository method -
@Modifying
@Query("DELETE FROM CartItem ci WHERE ci.cart.id = ?1 AND ci.product.id = ?2")
void deleteCartItemByProductIdAndCartId(Long cartId, Long productId);
Out of the 3 blocks of code, only the 3rd one seems to be deleting the cart item from the database. Why is that??
As per my understanding, code block 1 should work because of orphanRemoval = true
in the entity class and code block 2 should work because I am directly deleting the cartItem
that I fetched from the database.
I believe I am missing some fundamental thing. Please help!
The reason, why your code is not working as you expect - is probably because of Hibernate's entity lifecycle, persistence context, and how deletions are cascaded.
// Code #1
cart.getCartItems().remove(cartItem);
cartRepository.save(cart);
It is true, that orphanRemoval = true
should trigger CartItem
deletion from the database. But there can be cases, when the removal won't trigger deletion, for example, if cartItem
is not actually contained in cart.getCartItems()
. It can happen if Hibernate fetches collections lazily or because of differences in persistence context.
// Code #2
cartItemRepository.delete(cartItem);
When .delete(entity)
is called, the entity can be just marked for deletion, but the delete action will happen only at flush time. You can try to break the association explicitly with cartItem.setCart(null);
// Code #3
cartItemRepository.deleteCartItemByProductIdAndCartId(cartId, productId);
This is a JPQL update/delete query, it ignores entity states, associations, or persistence context issues. That is why it works always.
But be careful, JPQL query completely bypasses the Hibernate persistence lifecycle, it is better to be used only when you want an immediate, bulk deletion, but only when entity lifecycle doesn't matter. There can be some negative consequences of using JPQL queries in some cases:
@PreRemove
and @PostRemove
that let you run logic before or after an entity is deleted. But JPQL query won't trigger this events.So, it is always better to use JPA, except few specific cases (like bulk deletion, avoiding unnecessary fetching or for scheduled cleanup jobs).
I suggest you to find the right order for operations to trigger the deletion. For example, this way:
cart.getCartItems().remove(cartItem); // remove from parent collection
cartItem.setCart(null); // prevent Hibernate from reattaching it
cartRepository.save(cart); // persist changes
cartItemRepository.delete(cartItem); // ensure the entity is deleted if orphanRemoval fails
Hope, this will help you.