How to save parent and child in one shot (JPA & Hibernate)

本小妞迷上赌 提交于 2020-12-29 11:57:45

问题


I start showing you my scenario.

This is my parent object:

@Entity
@Table(name="cart")
public class Cart implements Serializable{  

    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Id
    @Column(name="id")
    private Integer id; 

    @OneToMany(mappedBy="cart", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private List<CartItem> cartItems; 

    ...
}

This is my child object:

@Entity
@Table(name="cart_item")
public class CartItem implements Serializable{  

    @GeneratedValue(strategy=GenerationType.IDENTITY)   
    @Id
    @Column(name="id")
    private Integer id;     

    @ManyToOne
    @JoinColumn(name="cart_id", nullable=false)
    private Cart cart;

    ...
}

As you can see looking at the database, in the table cart_item (child object) the field cart_id has a foreign key to the field id of the table cart (parent object).

This is how I save the object:

1) there's a restController that reads a JSON object:

@RestController
@RequestMapping(value = "rest/cart")
public class CartRestController {

    @Autowired
    private CartService cartService;    

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(value = HttpStatus.CREATED)
    public void create(@RequestBody CartDto cartDto) {
        cartService.create(cartDto);
    }
}

2) This is the CartService, that's just an Interface:

public interface CartService {  
    void create(CartDto cartDto); 
}

This is the implementation of CartService:

import org.springframework.transaction.annotation.Transactional;

    @Service
    @Transactional
    public class CartServiceImpl implements CartService {   
        @Autowired
        private CartDao cartDao;

        @Override
        public void create(CartDto cartDto) {
            cartDao.create(cartDto);
        }
    }

CartDao is just another interface, I show you only its implementation:

@Repository
public class CartDaoImpl implements CartDao {

    @Autowired 
    private SessionFactory sessionFactory;

    // in this method I save the parent and its children
    @Override
    public void create(CartDto cartDto) {       

        Cart cart = new Cart(); 

        List<CartItem> cartItems = new ArrayList<>();                   

        cartDto.getCartItems().stream().forEach(cartItemDto ->{     
            //here I fill the CartItem objects;     
            CartItem cartItem = new CartItem();         
            ... 
            cartItem.setCart(cart);
            cartItems.add(cartItem);                
        });
        cart.setCartItems(cartItems);

        sessionFactory.getCurrentSession().save(cart);                  
    }
}

When I try to save a new cart and its cart_items I get this error:

SEVERE: Servlet.service() for servlet [dispatcher] in context with path [/webstore] threw 
exception [Request processing failed; nested exception is 
org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: Object of 
class     
[com.depasmatte.webstore.domain.CartItem] with identifier [7]: optimistic locking failed; 
nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by 
another transaction (or unsaved-value mapping was incorrect) : 
[com.depasmatte.webstore.domain.CartItem#7]] with root cause
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction
 (or unsaved-value mapping was incorrect) : [com.depasmatte.webstore.domain.CartItem#7]

I suppose the error depends on the fact that when Hibernate try to save the a cart_item, the id of the cart doesn't exist yet!

What's the correct way to save a parent object and its childer in on shot? Thank you


回答1:


Here's the list of rules you should follow, in order to be able to store a parent entity along with its children in a one shot:

  • cascade type PERSIST should be enabled (CascadeType.ALL is also fine)
  • a bidirectional relationship should be set correctly on both sides. E.g. parent contains all children in its collection field and each child has a reference to its parent.
  • data manipulation is performed in the scope of a transaction. NO AUTOCOMMIT MODE IS ALLOWED.
  • only parent entity should be saved manually (children will be saved automatically because of the cascade mode)

Mapping issues:

  • remove @Column(name="id") from both entities
  • make setter for cartItems private. Since Hibernate is using its own implementation of the List, and you should never change it directly via setter
  • initialize you list private List<CartItem> cartItems = new ArrayList<>();
  • use @ManyToOne(optional = false) instead of nullable = false inside the @JoinColumn
  • prefer fetch = FetchType.LAZY for collections
  • it's better to use helper method for setting relationships. E.g. class Cart should have a method:

    public void addCartItem(CartItem item){
        cartItems.add(item);
        item.setCart(this);
    }
    

Design issues:

  • it's not good to pass DTOs to the DAO layer. It's better to do the conversion between DTOs and entities even above the service layer.
  • it's much better to avoid such boilerplate like method save with Spring Data JPA repositories



回答2:


Make sure that your method is Transactional. you can make method Transactional using @Transactional annotation on top of method signature.




回答3:


Did you checked this post? Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

You may find an appropriate answer in this one, I think your problem is coming from your getCurrentSession, even if you use sessions because hibernate is not thread-safe, A session is still a light weight and a non-threadsafe object. You should dig something from here.

In fact when one thread/session save an object in database, if another one try the same operation it will raise this kind of error because id's already exists so the operations is impossible.

Cheers!




回答4:


One important thing is clearly missing from the discussion that is very important to this question that is who is owning this relation. you are putting mappedBy in the parent entity that means the owner of this relation goes to child entity, he has to fill up this relation by explicitly setting property otherwise this relation ship won't be built. Put JoinColumn annotation on top of Parent, it will ensure relation owner is parent, he will establish this relation when the parent entity is saved automatically




回答5:


I know this is not directly relevant to the question, since services are used, but google brought me here when I had a similar problem. In my case I was using Spring JPA repositories. Make sure you annotate repository interface with @org.springframework.transaction.annotation.Transactional



来源:https://stackoverflow.com/questions/53647672/how-to-save-parent-and-child-in-one-shot-jpa-hibernate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!