org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.mar

实体配置懒加载时遇到的异常情况

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.markix.entity.po.UserPO.roles, could not initialize proxy - no Session

	at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:602)
	at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
	at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:581)
	at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:148)
	at org.hibernate.collection.internal.PersistentBag.get(PersistentBag.java:540)
	at com.markix.dao.UserDaoTest.find(UserDaoTest.java:28)

用户表和角色表的关联设计:
用户表配置多对多关联,并设置懒加载

@Entity
@Table(name = "SYS_USER")
public class UserPO {
... 其他字段忽略

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "SYS_USER_ROLE",
            joinColumns = {@JoinColumn(name = "ROLE_ID")},
            inverseJoinColumns = {@JoinColumn(name = "USER_ID")},
            foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT),
            inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private List<RolePO> roles;
}

角色表没有特殊配置

@Entity
@Table(name = "SYS_ROLE")
public class RolePO {
    @Id
    @Column(name = "ID")
    private String id;
    @Column(name = "NAME")
    private String name;
}

运行以下测试用例报错

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserDaoTest {

    @Autowired
    private UserDao userDao;

    @Test
    public void find() {
        List<UserPO> userPOList = userDao.findAll();//此时UserPO对象的roles属性是一个懒加载对象
        System.out.println("----------");
        List<RolePO> rolePOList = userPOList.get(0).getRoles();
        String name = rolePOList.get(0).getName();//get(0)会触发懒加载(报错的代码行)
        System.out.println(name);
        System.out.println("----------");
        userPOList.forEach(System.out::println);
    }
}

代码 rolePOList.get(0) 触发了懒加载,然后报错了。通过异常信息可以粗略得知,是session不存在的原因。
那是为什么呢?下面进行探究:
查看异常调用栈,发现异常是由此处抛出 AbstractPersistentCollection.withTemporarySessionIfNeeded ,关键代码如下:

if ( session == null ) {
	if( allowLoadOutsideTransaction ) {
		tempSession = openTemporarySessionForLoading();
	}
	else {
		throwLazyInitializationException( "could not initialize proxy - no Session" );
	}
}

很显然,只有同时满足 session == null 并且 allowLoadOutsideTransaction == false 时,会抛出异常。
那么我们可以使其中任一条件不成立即可避免此异常。

  • 如何在懒加载时令 allowLoadOutsideTransaction != false 呢?
    通过一系列溯源查找,发现,该值的源头是hibernate的一个属性 “hibernate.enable_lazy_load_no_trans”,通过设置该属性为true,可以令 allowLoadOutsideTransaction == true。在spring-boot框架中配置如下:(若是其他框架,请自行查找配置方式)
spring:
  jpa:
    properties:
      hibernate.enable_lazy_load_no_trans: true
  • 如何在懒加载时令 session != null 呢?
    调试发现,当执行完 userDao.findAll()后,会执行 AbstractPersistentCollection.unsetSession 逻辑,该方法会将session 设置为 null。
    具体原因是:我是在测试用例执行,所以 userDao.findAll() 会被当成一个事务,当userDao.findAll() 执行完,事务被提交,相应的session则被设置为null。
    相似的场景在一般项目可能是这样:controller调用了userService.findAll() 获取用户列表,然后再在controller通过用户列表获取角色列表(会触发懒加载)。此时就会发生此异常。原因就是 service层的事务已经提交,相关的session已经关闭,在controller触发懒加载,并无session可用。
    所以,这个只能通过改代码了,最好将懒加载的触发逻辑(获取懒加载对象)和查询方法写在同个事务中。
    (ps:尝试在测试用例加上@Transactional,将方法包裹成一个事务,并不成功,可能是junit实现机制问题,不深究)

总结:

解决方法

  1. 配置hibernate的 hibernate.enable_lazy_load_no_trans 属性为 true,允许在没有事务的情况下懒加载。
  2. 将懒加载的触发逻辑(获取懒加载对象)和查询方法写在同个事务中。
  3. 不使用懒加载,将关联注解(@ManyToMany)的 fetch = FetchType.LAZY 改为 fetch = FetchType.EAGER

end

你可能感兴趣的:(Spring,Data,JPA,异常小记)