• 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
        • 相关阅读:
          报表自动化: 商业智能背后的秘密
          谈谈个人对 TDD (测试驱动开发) 的理解
          初识 Inception
          从软件生命周期看应用安全(网络安全)
          Spring JPA save 实现主键重复抛异常
          QMdiArea及QMdiSubWindow实现父子窗口及布局方法
          QTcpServer实现多客户端连接
          C++设计模式
          QTreeView/QTableView中利用QStandardItem实现复选框三种形态变化
          Qt富文本编辑器QTextDocument
        • 原文地址:https://www.cnblogs.com/jpfss/p/11058617.html
        Copyright © 2020-2023  润新知