如何一次性保存父子对象(JPA和Hibernate)

22

我开始向你展示我的情景。

这是我的父对象:

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

    ...
}

这是我的子对象:

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

    ...
}

从数据库中可以看到,在表cart_item(子对象)中,字段cart_id与表cart(父对象)的字段id具有外键关系。

输入图像描述

以下是我保存对象的方法:

1)有一个restController读取JSON对象:

@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,只是一个 接口

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


Translated Text:

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

当我尝试保存一个新的购物车及其购物车商品时,出现以下错误:

当我尝试保存一个新的购物车和它的购物车商品时,我会遇到这个错误:

(Two possible translations depending on how strict the requirement is for keeping the original meaning)
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时,cartid尚不存在!

正确的方法是一次保存父对象及其子对象吗? 谢谢


相关的代码可能是你构建和保存数据的部分。 - Alan Hay
该方法是否具有事务性? - Alien
嗨@AlanHay,我编辑了我的代码,现在应该更清晰。 - MDP
嗨@Alien,我编辑了我的代码。正如您所看到的,CartServiceImpl类是事务性的。 - MDP
如果我使用的是Set而不是List呢?它是Set,而不是HashSet,所以我不能使用mySet.add()。 - Matley
6个回答

51

以下是规则列表,以便能够一次性存储父实体及其子实体:

  • 启用级联类型 PERSISTCascadeType.ALL 也可以)
  • 必须在双方正确设置双向关系。例如,父实体包含其所有子实体的集合字段,每个子实体都有对其父实体的引用。
  • 数据操作应在事务范围内执行,不允许自动提交模式。
  • 仅手动保存父实体(由于级联模式的存在,子实体将自动保存)

映射问题:

  • 从两个实体中删除 @Column(name="id")
  • cartItems 创建 私有 setter。因为 Hibernate 使用自己的 List 实现,所以您不应直接通过 setter 更改它
  • 初始化列表:private List<CartItem> cartItems = new ArrayList<>();
  • @JoinColumn 中使用 @ManyToOne(optional = false) 替代 nullable = false
  • 优先使用 fetch = FetchType.LAZY 来处理集合
  • 最好使用帮助方法来设置关系。例如,类 Cart 应该拥有一个方法:
public void addCartItem(CartItem item){
    cartItems.add(item);
    item.setCart(this);
}

设计问题:

  • 将DTO传递给DAO层并不好。最好在服务层之上进行DTO和实体之间的转换。
  • 使用Spring Data JPA repositories来避免像save这样的样板代码会更好。

1
CascadeType.PERSIST(或CascadeType.ALL)就是我缺少的。收藏了这个答案,因为它包含了所有的陷阱。谢谢。 - granadaCoder
我有一个个人模式,可以解决您的DTO设计问题。将其分离成关注点。public interface InternalMyThingJpaRepository extends JpaRepository<MyThingJpaEntity, Long> { /* 仅限Spring Data方法 */ } ...public class MyThingJpaRepository implements IMyThingRepository { @Inject public MyThingJpaRepository(InternalMyThingJpaRepository springDataDeptRepo, IMyThingEntityDtoConverter / org.modelmapper.ModelMapper */ myThingConverter) { } MyThingJpaRepository接受“Dtos”...转换为Jpa实体并使用InternalMyThingJpaRepository - granadaCoder
使用Spring Data JPA和Repo的简单解决方案可以是: children.stream.forEach(c -> c.setParent(parent)); parent.setChildren(children); repo.save(parent); - Sanjay Amin

12

对于双向关系,请按以下方式操作:

  1. 将级联设置为persist或All
  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;

}

我认为,关于删除(nullable = false)的第四点不是一个好的选择。我认为,为了满足Hibernate算法而删除约束条件是不应该的。这个约束条件存在是有原因的,不应该被删除。有没有一些简洁的方法来保持nullable = false约束条件呢? - Braintertainer

4

在这个问题的讨论中,有一件非常重要但显然被忽略的事情,那就是谁拥有这个关系。你将放在父实体中,这意味着该关系的所有者属于子实体,他必须通过显式设置属性来填写此关系,否则将无法建立该关系。在父类上添加注释可以确保关系所有者是父类,当保存父实体时,他将自动建立此关系。


2

请确保你的方法是事务性的,你可以通过在方法签名的顶部使用@Transactional注解来使方法变为事务性。


嗨@Alien,我编辑了我的代码。正如您所看到的,CartServiceImpl类是事务性的。 - MDP

1

你有查看过这篇文章吗?行已被另一个事务更新或删除(或未保存的值映射不正确)

我认为你可以在这篇文章中找到一个合适的答案,我认为你的问题来自于getCurrentSession,即使你使用了sessions,因为Hibernate不是线程安全的,session仍然是轻量级和非线程安全的对象。你应该从这里开始研究。

实际上,当一个线程/会话将一个对象保存在数据库中时,如果另一个线程尝试进行相同的操作,它将引发此类错误,因为id已经存在,所以操作是不可能的。

干杯!


0

我知道这与问题不直接相关,因为使用了服务,但当我遇到类似问题时,谷歌把我带到了这里。在我的情况下,我正在使用Spring JPA存储库。请确保使用@org.springframework.transaction.annotation.Transactional注释存储库接口。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接