我这边有个批量插入用户OpenUser和应用OpenApp关联关系数据的操作,由于耗时较长时间,所以准备用线程池异步执行操作,然而却遇到了一个jpa的detached entity passed to persist问题,我这边的操作是批量保存一个OpenAppUser关联关系表,所以需要先获得对应OpenUser和OpenApp的引用,再设置到关联对象OpenAppUser里,然后在保存,我这边是先通过userRepository.findById(userId)获取到OpenUser,然后openAppUser.setOpenUser(openUser),在执行appUserRepository.save(openAppUser);时发生了如标题上的错误,说是OpenUser对象处于游离态,无法保存。

经过排查,我这边是因为OpenAppUser类里设置了@ManyToOne(cascade = CascadeType.ALL)级联OpenUser,所以在保存OpenAppUser的时候会级联操作OpenUser,本来在没有开线程异步的情况下,因为OpenUser之前通过findById查出来了,所以在jpa的PersistenceContext里是有该OpenUser的脱管对象的,这时候就不会报错,而在线程异步的情况下context里确没有该脱管对象了
(这里说明一下,为啥不开线程有,开了线程没有?)因为spring-boot默认jpa.open-in-view=true,会使用ThreadLocal在当前线程里保存EntityManager上下文信息,所以在整个controller里都是使用的同一个context

PersistenceContext持久性上下文有两种类型:

  • 事务范围的持久性上下文;当我们在事务中执行任何操作时,EntityManager 会检查持久性上下文。 如果存在,则将使用它。否则,它将创建一个持久性上下文
  • 扩展范围的持久性上下文;扩展持久性上下文可以跨越多个事务。我们可以在没有事务的情况下持久化实体,但不能在没有事务的情况下刷新它。

在@PersistenceContext注解里type可以指定范围:PersistenceContextType.TRANSACTION;PersistenceContextType.EXTENDED

而当我们用线程池异步的时候,拿不到之前的EntityManager的配置信息,而spring jpa repository默认的方法上都会自带一个事务,所以在执行完userRepository.findById(userId)获取到OpenUser之后,会commit,而commit操作会clear掉EntityManager里保存的脱管对象OpenUser,等到appUserRepository.save(openAppUser);保存的时候,由于引用的OpenUser已经没有在PersistenceContext上下文里了,不是脱管对象了(具体可以看EntityState entityState = getEntityState( entity, entityName, entityEntry, source );里面的实现,有几种判断条件,是不是脱管对象,有没有id、version等等属性),就会报detached entity passed to persist这个异常

所以根据实际情况,我们只要参考open-in-view=true产生对应的OpenEntityManagerInViewInterceptor拦截器改造一下自己线程里的PersistenceContext上下文生效范围,就可以解决该异常了


我不是码农
3 声望1 粉丝

java开发码农