Spring Boot JPA 懶加載


最近在使用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


        免責聲明!

        本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



         
        粵ICP備18138465號   © 2018-2025 CODEPRJ.COM