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

6

23

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

enter image description here

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

Mallorie answered 6/12, 2018 at 8:52 Comment(5)
The pertinent code is probably that where you construct and save the data.Cecillececily
is method transactional?Bail
Hi @AlanHay, I edited my code, now should be clearerMallorie
Hi @Alien, I edited my code. As you can see the class CartServiceImpl is transactionalMallorie
and what if I have Set instead of List ? Its Set, not HashSet so I can't dp mySet.add()Damsel
D
51

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
Dorsman answered 6/12, 2018 at 12:41 Comment(3)
CascadeType.PERSIST (or CascadeType.ALL) is what I was missing. Bookmarking this answer because it has all the gotchas. Thx.Canopus
I have a personal pattern that addresses your dto-design-issue. To SepOfConcerns the translation. public interface InternalMyThingJpaRepository extends JpaRepository<MyThingJpaEntity, Long> { /* spring data ONLY methods / } ... public class MyThingJpaRepository implements IMyThingRepository { @Inject public MyThingJpaRepository(InternalMyThingJpaRepository springDataDeptRepo, IMyThingEntityDtoConverter / org.modelmapper.ModelMapper */ myThingConverter) { } MyThingJpaRepository accepts "Dtos"..converts to Jpa entities and uses InternalMyThingJpaRepositoryCanopus
simple solution with spring data jpa and repo can be as: children.stream.forEach(c -> c.setParent(parent)); parent.setChildren(children); repo.save(parent);Thrombo
B
12

For bidirectional relation act like below:

  1. Set cascade to persist or All
  2. Remove mappedBy attribute in @OnToMany if there is
  3. Write @JoinCloumn at both sides (otherwise it creates Join Table) with the same name
  4. Remove (nullable = false) in @JoinColumn (because Hibernate first inserts the parent record then inserts child records and after all, updates the foreign key in child records)

Here is the sample code:

public class Parent {

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "fk_parent")
    private List<Child> children;

}

public class Child {

    @ManyToOne
    @JoinColumn(name = "fk_parent")
    private Parent parent;

}
Barytes answered 24/5, 2021 at 6:42 Comment(1)
The 4th point about removing the (nullable = false) isn't a good option in my opinion. I think that a constraint shouldn't be removed just to satisfy the algorithm of Hibernate. That constraint is there for some reason and shouldn't be removed. Is there some neat way how to do it while maintaining the nullable = false constraint?Tragedienne
W
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

Wimberly answered 19/4, 2019 at 22:41 Comment(0)
B
2

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

Bail answered 6/12, 2018 at 9:35 Comment(1)
Hi @Alien, I edited my code. As you can see the class CartServiceImpl is transactionalMallorie
A
1

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!

Adaptable answered 6/12, 2018 at 9:16 Comment(0)
K
0

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

Kucik answered 2/3, 2020 at 12:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.