IT序号网

Spring Boot JPA 懒加载知识解答

flyfish 2021年06月14日 数据库 152 0

最近在使用spring jpa 的过程中经常遇到懒加载的错误:“`

org.hibernate.LazyInitializationException: could not initialize proxy [xxxx#18] - no Session
    

    通过查询资料,整理了一下常见的几种解决办法。

    一、spring.jpa.open-in-view 配置

    测试 dao 层或者 service 层时,会出现 no Session 的错误;访问 controller 时,又不会出现上面的错误。查询资料发现,spring boot web 会引入一个一个配置

    spring.jpa.open-in-view=true
        

      这个配置的说明如下:

      spring.jpa.open-in-view 
      java.lang.Boolean 
       
      Default: true 
       
      Register OpenEntityManagerInViewInterceptor.  
      Binds a JPA EntityManager to the thread for the entire processing  
      of the request.
          

      该配置会注册一个OpenEntityManagerInViewInterceptor。在处理请求时,将 EntityManager 绑定到整个处理流程中(model->dao->service->controller),开启和关闭session。这样一来,就不会出现 no Session 的错误了(可以尝试将该配置的值置为 false, 就会出现懒加载的错误了。)

      二、非 web 请求下的懒加载问题解决

      最近遇到一个quartz定时任务处理的,不需要通过 web 请求,就可以直接访问数据库。这种情况下,spring.jpa.open-in-view 这个配置就不起作用了,需要通过其它的方式处理懒加载的问题。

      下面介绍其中两种方式。

      1. spring.jpa.properties.hibernate.enable_lazy_load_no_trans 配置

      这个配置是 hibernate 中的(其它 JPA Provider 中无法使用),当配置的值是 true 的时候,允许在没有 transaction 的情况下支持懒加载。

      下面通过一个用户与权限的多对多的关联的例子来说明。

      用户实体类

      package com.johnfnash.learn.domain; 
       
      import java.util.List; 
       
      import javax.persistence.Column; 
      import javax.persistence.Entity; 
      import javax.persistence.GeneratedValue; 
      import javax.persistence.GenerationType; 
      import javax.persistence.Id; 
      import javax.persistence.JoinColumn; 
      import javax.persistence.JoinTable; 
      import javax.persistence.ManyToMany; 
       
      @Entity 
      public class User { 
       
          @Id 
          @GeneratedValue(strategy = GenerationType.IDENTITY) 
          private Long id; 
       
          @Column(nullable = false, length = 20, unique = true) 
          private String username; // 用户账号,用户登录时的唯一标识 
       
          @Column(length = 100) 
          private String password; // 登录时密码 
       
          @ManyToMany 
          @JoinTable(name = "user_authority", joinColumns = @JoinColumn(name = "user_id"), 
                  inverseJoinColumns = @JoinColumn(name = "authority_id")) 
          //1、关系维护端,负责多对多关系的绑定和解除 
          //2、@JoinTable注解的name属性指定关联表的名字,joinColumns指定外键的名字,关联到关系维护端(User) 
          //3、inverseJoinColumns指定外键的名字,要关联的关系被维护端(Authority) 
          //4、其实可以不使用@JoinTable注解,默认生成的关联表名称为主表表名+下划线+从表表名, 
          //即表名为user_authority 
          //关联到主表的外键名:主表名+下划线+主表中的主键列名,即user_id 
          //关联到从表的外键名:主表中用于关联的属性名+下划线+从表的主键列名,即authority_id 
          //主表就是关系维护端对应的表,从表就是关系被维护端对应的表 
          private List<Authority> authorityList; 
       
          public User() { 
              super(); 
          } 
       
          public User(String username, String password, List<Authority> authorityList) { 
              super(); 
              this.username = username; 
              this.password = password; 
              this.authorityList = authorityList; 
          } 
       
          // getter, setter 
       
          @Override 
          public String toString() { 
              return "User [id=" + id + ", username=" + username + ", password=" + password + "]"; 
          } 
       
      }
          

      注:User 实体类作为多读多关系维护端,里维护了相关的 权限列表。

      权限实体类

      package com.johnfnash.learn.domain; 
       
      import javax.persistence.Column; 
      import javax.persistence.Entity; 
      import javax.persistence.GeneratedValue; 
      import javax.persistence.GenerationType; 
      import javax.persistence.Id; 
       
      @Entity 
      public class Authority { 
       
          @Id 
          @GeneratedValue(strategy = GenerationType.IDENTITY) 
          private Integer id; 
       
          @Column(nullable = false) 
          private String name; //权限名 
       
          public Authority() { 
              super(); 
          } 
       
          public Authority(String name) { 
              super(); 
              this.name = name; 
          } 
       
          // getter, setter 
       
          @Override 
          public String toString() { 
              return "Authority [id=" + id + ", name=" + name + "]"; 
          } 
       
      }
          

      UserRepository.java

      package com.johnfnash.learn.repository; 
       
      import org.springframework.data.jpa.repository.JpaRepository; 
       
      import com.johnfnash.learn.domain.User; 
       
      public interface UserRepository extends JpaRepository<User, Long> { 
       
      }
          

      AuthorityRepository.java

      package com.johnfnash.learn.repository; 
       
      import org.springframework.data.jpa.repository.JpaRepository; 
       
      import com.johnfnash.learn.domain.Authority; 
       
      public interface AuthorityRepository extends JpaRepository<Authority, Integer> { 
       
      }
          

      测试

      package com.johnfnash.learn; 
       
      import java.util.ArrayList; 
      import java.util.List; 
       
      import org.junit.Test; 
      import org.junit.runner.RunWith; 
      import org.springframework.beans.factory.annotation.Autowired; 
      import org.springframework.boot.test.context.SpringBootTest; 
      import org.springframework.test.context.junit4.SpringRunner; 
       
      import com.johnfnash.learn.domain.Authority; 
      import com.johnfnash.learn.domain.User; 
      import com.johnfnash.learn.repository.AuthorityRepository; 
      import com.johnfnash.learn.repository.UserRepository; 
       
      @RunWith(SpringRunner.class) 
      @SpringBootTest 
      public class UserRepositoryTest { 
       
          @Autowired 
          private UserRepository userRepository; 
       
          @Autowired 
          private AuthorityRepository authorityRepository; 
       
          @Test 
          public void saveUser() { 
              Authority authority = new Authority("ROLE_ADMIN"); 
              authorityRepository.save(authority); 
       
              User user = new User(); 
              user.setUsername("admin"); 
              user.setPassword("123456"); 
       
              List<Authority> authorityList = new ArrayList<Authority>(); 
              authorityList.add(authority); 
       
              user.setAuthorityList(authorityList); 
              userRepository.save(user); 
          }     
       
          @Test 
          public void queryUser() { 
              User user = userRepository.getOne(1L); 
              System.out.println(user);        
              //System.out.println(user.getAuthorityList()); 
          } 
       
      }
          

      调用 saveUser 方法插入测试数据后,再执行 queryUser 查询用户数据,报 No Session 的错误。application.properties 中添加如下配置:

      spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
          

        再执行 queryUser 方法,查询成功,sql如下:

        Hibernate: select user0_.id as id1_5_0_, user0_.password as password2_5_0_,  
        user0_.username as username3_5_0_ from user user0_ where user0_.id=?
            

        这个时候由于只访问了 user 的基本信息,所以没有查询 authority 表。

        打开 queryUser 方法里的注释,再执行 queryUser 方法,会执行如下两条sql:

        Hibernate: select user0_.id as id1_5_0_, user0_.password as password2_5_0_,  
        user0_.username as username3_5_0_ from user user0_ where user0_.id=? 
        Hibernate: select authorityl0_.user_id as user_id1_6_0_,  
        authorityl0_.authority_id as authorit2_6_0_,  
        authority1_.id as id1_3_1_, authority1_.name as name2_3_1_  
        from user_authority authorityl0_  
        inner join authority authority1_ on authorityl0_.authority_id=authority1_.id 
        where authorityl0_.user_id=?
            

        通过上面的例子,我们可以看到添加这个配置之后,确实实现了懒加载。

        不过这种方式会产生 N+1 的影响,上面的例子这个一个用户有多个权限,可能会进行 1 + N 次查询。如果这时 Authority 又与多个 Role 关联,使用不当的话,查询次数可能就变成了 1 + N * M 。

        2. 通过在查询中使用 fetch 的方式

        通过再查询中使用 fetch,一次将相关数据查询出来,不会产生 N + 1 的影响。

        继续使用上面的 用户-权限 的例子。先把 spring.jpa.properties.hibernate.enable_lazy_load_no_trans 这个配置去掉,然后在UserRepository 中添加如下方法:

        @Query("from User u join fetch u.authorityList") 
        public User findOne(Long id);
            

        测试类中的 queryUser 代码改为下面的:

        @Test 
        public void queryUser() { 
            User user = userRepository.findOne(2L); 
            System.out.println(user); 
         
            System.out.println(user.getAuthorityList()); 
        }
            

        进行查询,只会执行一条sql:

        Hibernate: select user0_.id as id1_5_0_, authority2_.id as id1_3_1_,  
        user0_.password as password2_5_0_, user0_.username as username3_5_0_,  
        authority2_.name as name2_3_1_, authorityl1_.user_id as user_id1_6_0__,  
        authorityl1_.authority_id as authorit2_6_0__  
        from user user0_  
        inner join user_authority authorityl1_ on user0_.id=authorityl1_.user_id  
        inner join authority authority2_ on authorityl1_.authority_id=authority2_.id
            

        通过 sql 可以看出,实际上就是使用了sql 里的 join 一次查询出来多条数据。

        参考

        [1] Solve Hibernate Lazy-Init issue with hibernate.enable_lazy_load_no_trans

        [2] spring中的懒加载与事务–排坑记录

        [3] hibernate join fetch

        原文地址:https://blog.csdn.net/johnf_nash/article/details/80658626

        发布评论
        IT序号网

        微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

        Spring Boot中使用Spring-data-jpa让数据访问更简单、更优雅知识解答
        你是第一个吃螃蟹的人
        发表评论

        ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。