Reputation: 4891
I have OneToMany bidirectional mapping for two entities Cart and CartProduct. Whenever we insert a Cart object with cart products, CartProduct table should fill with cart_id
. Here is the problem, when I insert cart object, everything seems to be fine except, JoinColumn(card_id)
which results in a null value in CartProduct table. Am I doing this right?
Cart.Java
package com.springtesting.model.cart;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.springtesting.model.AbstractAuditingEntity;
import com.springtesting.model.user.UserProfile;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Entity
@Data
@Table(name = "cart")
public class Cart extends AbstractAuditingEntity
{
private static final long serialVersionUID = 6294902210705780249L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@OneToOne
@JoinColumn(name = "user_profile_id")
@JsonIgnoreProperties(value = {"addresses"})
private UserProfile userProfile;
@ManyToOne
@JoinColumn(name = "cart_status")
private CartStatus cartStatus;
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL,fetch = FetchType.EAGER)
//@ElementCollection(targetClass = CartProduct.class)
private List<CartProduct> cartProducts=new ArrayList<>();
}
CartProduct.Java
package com.springtesting.model.cart;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.springtesting.model.AbstractAuditingEntity;
import com.springtesting.model.product.Product;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.*;
@EqualsAndHashCode(callSuper = true)
@Entity
@Data
@Table(name = "cart_product")
public class CartProduct extends AbstractAuditingEntity
{
private static final long serialVersionUID = 6498067041321289048L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@OneToOne
@JoinColumn(name = "product_id")
private Product product;
@Column(name = "quantity")
private Integer quantity;
@ManyToOne
@JoinColumn(name = "cart_id",referencedColumnName = "id")
@JsonIgnoreProperties(value = {"userProfile","cartStatus","cartProducts"})
private Cart cart;
}
TestCase.java
@Test
public void insertCart()
{
Cart cart=new Cart();
cart.setUserProfile(userProfileRepository.findAllByUserId(1L).get());
cart.setCartStatus(cartStatusRepository.findById(1L).get());
List<CartProduct> cartProducts=new ArrayList<>();
CartProduct cartProduct=new CartProduct();
cartProduct.setProduct(productRepository.findById(1L).get());
cartProduct.setQuantity(2);
cartProducts.add(cartProduct);
cartProduct=new CartProduct();
cartProduct.setProduct(productRepository.findById(2L).get());
cartProduct.setQuantity(1);
cartProducts.add(cartProduct);
cart.setCartProducts(cartProducts);
cartRepository.saveAndFlush(cart);
}
Upvotes: 1
Views: 3406
Reputation: 11561
Yes, your fix is the addition of cartProduct.setCart(cart);
This is because the CartProduct is the owning entity and is the keeper of the foreignKey. The above statement sets the FK.
The way to think about this is the concept of owning entity
. When you have mappedBy="cart"
you are saying that the CartProduct
class owns the relationship. This means that only the CartProduct
class is doing the persisting. This tells JPA to create a FK in the CartProduct
table. However, we notice that save is not being called on CartProduct
but rather on Cart
and yet cartProducts
is still being saved. This is because you have the cascade = CascadeType.ALL
annotation. This tells JPA to cascade certain operations when they are done to Cart
, in this case the save
operation.
You should have SQL statements printed and examine the differences with different configurations and test cases. This will help you understand better.
You also have FetchType.EAGER. This is generally a bad habit and usually leads to endless problems.
A good way to think about a bidirectional mapping is that the List<CartProducts> cartProducts
is a query only field. In order to save a CartProduct
you would call save on the cartProductRepository
directly. E.g.
CartProduct cartProduct=new CartProduct();
cartProduct.setProduct(productRepository.findById(1L).get());
cartProduct.setQuantity(2);
cartProduct.setCart(cart);
cartProductRepository.save(cartProduct);
and then
cart.getCartProducts().add(cartProduct);
and remove all the cascade and eager fetch annotations. When hibernate says that you must management both sides of the relationship this is what is meant.
Doing it this way will result in one query for the save. By using a cascade annotation you will find that as you add items to the cart and call save on it the sql generated will first delete all the existing cartProducts
items from the database and re-add them along with the new one every time you call save. For a cart with 10 items instead of a single save you will have a delete and 10 new saves. Definitely less desirable. If you have to reload the cart from scratch the most efficient method is to get the cart and then cart.setCartProducts(cartProductRepository.findAllByCart(cart));
which is what FetchType.EAGER is doing anyway. When you understand all this then you realize that you don't need a = new ArrayList<>();
for your cartProducts.
Upvotes: 5
Reputation: 4891
I think I figured out the solution. Based Hibernate docs
Whenever a bidirectional association is formed, the application developer must make sure both sides are in-sync at all times.
So I manually added the cart
object to cartProduct object, which saves cart_id
in CartProduct table
CartController.java
import com.pj.springsecurity.model.cart.Cart;
import com.pj.springsecurity.model.cart.CartProduct;
import com.pj.springsecurity.repo.CartRepository;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/v1/cart")
public class CartController
{
private final CartRepository cartRepository;
public CartController(CartRepository cartRepository)
{
this.cartRepository = cartRepository;
}
@GetMapping(path = "/list")
public List<Cart> getAllCarts()
{
return cartRepository.findAll();
}
@GetMapping(path = "/find/user/{id}")
public Optional<Cart> getCartBasedOnUserId(@PathVariable Long id)
{
return cartRepository.findAllByUserProfileUserId(id);
}
@PostMapping(path = "/product/add")
public Cart addProductToCart(@RequestBody Cart cart)
{
List<CartProduct> cartProducts=cart.getCartProducts();
for(CartProduct cartProduct: cartProducts)
{
cartProduct.setCart(cart);
}
return cartRepository.saveAndFlush(cart);
}
@PutMapping(path = "/update")
public Cart updateCart(@RequestBody Cart cart)
{
return cartRepository.saveAndFlush(cart);
}
@DeleteMapping(path = "/delete")
public Cart createEmptyCart(@RequestBody Cart cart)
{
return cartRepository.saveAndFlush(cart);
}
@DeleteMapping(path = "/product/delete")
public void deleteProductFromCart(@RequestBody Cart cart)
{
List<CartProduct> cartProducts=cart.getCartProducts();
for(CartProduct cartProduct: cartProducts)
{
cartProduct.setCart(null);
}
cartRepository.delete(cart);
}
}
and Test case updated with the same
@Test
public void insertCart()
{
Cart cart=new Cart();
cart.setUserProfile(userProfileRepository.findAllByUserId(1L).get());
cart.setCartStatus(cartStatusRepository.findById(1L).get());
List<CartProduct> cartProducts=new ArrayList<>();
CartProduct cartProduct=new CartProduct();
cartProduct.setProduct(productRepository.findById(1L).get());
cartProduct.setQuantity(2);
cartProduct.setCart(cart);
cartProducts.add(cartProduct);
cartProduct=new CartProduct();
cartProduct.setProduct(productRepository.findById(2L).get());
cartProduct.setQuantity(1);
cartProduct.setCart(cart);
cartProducts.add(cartProduct);
cart.setCartProducts(cartProducts);
cartRepository.saveAndFlush(cart);
}
Upvotes: 3