如何一次性保存 parent 和 child(JPA 和 Hibernate)

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

我开始向您展示我的场景。

这是我的 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; 

    ...
}

这是我的 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;

    ...
}

正如您查看数据库所见,在 table cart_item (child object) 字段中cart_id 有一个外键到 table cart[=60= 的 id 字段] (parent object).

我就是这样保存 object:

1) 有一个 restController 读取 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) 这是 CartService,它只是一个 Interface:

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

这是 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只是另一个接口,我只展示它的实现:

@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);                  
    }
}

当我尝试保存新的 购物车 及其 cart_item 时,出现此错误:

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]

我想错误取决于这样一个事实,即当 Hibernate 尝试保存 cart_item 时,[= 购物车 的 46=]id 还不存在!

在镜头中保存 parent object 及其 child 的正确方法是什么? 谢谢

你检查过这个post吗? Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

你可能会在这找到一个合适的答案,我认为你的问题来自你的getCurrentSession,即使你使用session因为hibernate不是线程安全的,A session仍然是轻量级和非线程安全的目的。你应该从这里挖掘一些东西。

事实上,当一个thread/session在数据库中保存一个对象时,如果另一个尝试相同的操作,它会引发这种错误,因为id已经存在,所以操作是不可能的。

干杯!

确保您的方法是事务性的。您可以在方法签名之上使用 @Transactional 注释使方法成为事务性的。

这是您应该遵循的规则列表,以便能够将 parent 实体与其 children 一起存储在一个射击:

  • 应启用级联类型 PERSISTCascadeType.ALL 也可以)
  • 一个双向关系应该在双方上正确设置。例如。 parent 在其 collection 字段中包含所有 children,并且每个 child 都有对其 parent.
  • 的引用
  • 数据操作是在事务范围内执行的。 不允许自动提交模式。
  • 只有parent个实体需要手动保存(children因为级联模式会自动保存)

映射问题:

  • 从两个实体中删除 @Column(name="id")
  • cartItems私有setter。由于 Hibernate 使用其自己的 List 实现,因此您永远不应通过 setter
  • 直接更改它
  • 初始化你的列表private List<CartItem> cartItems = new ArrayList<>();
  • @JoinColumn
  • 中使用 @ManyToOne(optional = false) 而不是 nullable = false
  • 更喜欢 fetch = FetchType.LAZY collections
  • 设置关系最好使用辅助方法。例如。 class Cart 应该有一个方法:

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

设计问题:

  • 把DTO传给DAO层不好。最好在服务层之上进行DTO和实体之间的转换。
  • 最好避免像方法 saveSpring Data JPA repositories
  • 这样的样板文件

讨论中显然遗漏了一件对这个问题非常重要的重要事情,即谁拥有这种关系。 您将 mappedBy 放在父实体中,这意味着此关系的所有者转到子实体,他必须通过显式设置 属性 来填充此关系,否则将不会建立此关系船。将 JoinColumn 注解放在 Parent 之上,它将确保关系所有者是父实体,他将在自动保存父实体时建立此关系

我知道这与问题没有直接关系,因为使用了服务,但是当我遇到类似问题时 google 把我带到了这里。就我而言,我使用的是 Spring JPA 存储库。确保使用 @org.springframework.transaction.annotation.Transactional

注释存储库接口

双向关系如下:

  1. 将级联设置为持续或全部
  2. 删除@OnToMany 中的mappedBy 属性
  3. 在两边写上@JoinCloumn(否则创建同名的Join Table)
  4. 去掉@JoinColumn中的(nullable = false)(因为Hibernate先插入父记录再插入子记录,最后更新子记录中的外键)

示例代码如下:

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;

}