• 学习Spring-Data-Jpa(十一)---抓取策略与实体图


    1、抓取策略

      在前面说到的关联关系注解中,都有一个fetch属性,@OneToOne、@ManyToOne中都默认是FetchType.EAGER,立即获取。@OneToMany、@ManyToMany默认值是FetchType.LAZY,延迟获取。这些注解的的fetch属性定义的是合适获取,至于如何获取,对与FetchType.EAGER,使用的是JOIN。FetchType.LAZY使用的是SELECT。JPA并没有提供我们设置如何获取的方式,如果想要进行修改要使用Hibernate提供的Fetch注解配置FetchMode。里面提供了三种方式SELECT、JOIN、SUBSELECT。(大多数情况下,我们不需要进行设置如何加载,使用默认的即可)
      但是对于JPA的fetch,使用起来只有在使用Spring-Data-Jpa为我们提供的findById方法时,配置的fetch=FetchType.EAGER才会生效。而我们根据Spring-Data-Jpa规则定义的方法查询则不生效,还是会进行延迟加载。

      1.1、执行findById会进行关联查询

        /**
         * 对于fetch= FetchType.EAGER ,使用findById会执行关联查询。
         */
        @Test
        void testFindById(){
            Optional<Book> bookOptional = bookRepository.findById(1L);
            if (bookOptional.isPresent()) {
                Book book = bookOptional.get();
                System.out.println(book.getCategory().getCategoryName());
            }
        }

      findById控制台的打印信息

    Hibernate: select book0_.id as id1_4_0_, book0_.book_name as book_nam2_4_0_, book0_.category_id as category4_4_0_, book0_.publish_date as publish_3_4_0_, category1_.id as id1_6_1_, category1_.category_name as category2_6_1_, category1_.parent_id as parent_i3_6_1_ from cfq_jpa_book book0_ left outer join cfq_jpa_category category1_ on book0_.category_id=category1_.id where book0_.id=?
    Java  

      1.2、执行findByBookName不会进行关联查询

        /**
         * 根据书名进行查询书籍
         * @param bookName bookName
         * @return book
         */
         Optional<Book> findByBookName(String bookName);
        /**
         * 对于fetch= FetchType.EAGER ,使用我们自己定义的查询方法,则不生效,会使用懒加载的方式
         */
        @Test
        void findByBookName(){
            Optional<Book> bookOptional = bookRepository.findByBookName("java编程思想");
            if (bookOptional.isPresent()) {
                Book book = bookOptional.get();
                System.out.println(book.getCategory().getCategoryName());
            }
        }

      findByBookName控制台的打印信息

    Hibernate: select book0_.id as id1_4_, book0_.book_name as book_nam2_4_, book0_.category_id as category4_4_, book0_.publish_date as publish_3_4_ from cfq_jpa_book book0_ where book0_.book_name=?
    Hibernate: select category0_.id as id1_6_0_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_ from cfq_jpa_category category0_ where category0_.id=?
    Java

      这样的话,如果我们对于图书(Book)来说,我们使用findById方法时,是可以直接拿到门类(Category)信息的。但是通过findByBookName进行查询时,只有我们使用到门类的时候,才会发送一条查询门类的SQL,只是对于一条记录还好。但是如果我们查询一个图书列表(N本图书)的时候,这时就会执行N+1条SQL。如下所示,根据出版时间进行查询,一共有3条记录,执行了4句SQL。

        /**
         * 对于fetch= FetchType.EAGER ,使用我们自己定义的查询方法,则不生效,会使用懒加载的方式,执行 N+1条SQL。
         *
         */
        @Test
        void findByPublishDate(){
            List<Book> books = bookRepository.findByPublishDate(LocalDate.of(2019,11,17));
            books.forEach(b -> System.out.println(b.getCategory().getCategoryName()));
        }
    Hibernate: select book0_.id as id1_4_, book0_.book_name as book_nam2_4_, book0_.category_id as category4_4_, book0_.publish_date as publish_3_4_ from cfq_jpa_book book0_ where book0_.publish_date=?
    Hibernate: select category0_.id as id1_6_0_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_ from cfq_jpa_category category0_ where category0_.id=?
    Hibernate: select category0_.id as id1_6_0_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_ from cfq_jpa_category category0_ where category0_.id=?
    Hibernate: select category0_.id as id1_6_0_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_ from cfq_jpa_category category0_ where category0_.id=?
    Java
    数据结构
    数据库

      对于这个问题,我们怎么来解决呢?

    2、使用@Query自己写JPQL语句进行解决N+1条SQL问题。

        /**
         * 使用@Query,JPQL中 声明要查询category属性,减少子查询。
         * @param publishDate publishDate
         * @return list
         */
         @Query(value = "select b,b.category from Book b where b.publishDate = :publishDate ")
    //     @Query(value = "select b,c from Book b inner join Category c on b.category = c where b.publishDate = :publishDate ")
        List<Book> findByPublishDateWithQuery(LocalDate publishDate);
        /**
         * 对于fetch= FetchType.EAGER ,使用@Query,自己写查询语句,解决N+1条SQL问题。
         */
        @Test
        void findByPublishDateWithQuery(){
            List<Book> books = bookRepository.findByPublishDateWithQuery(LocalDate.of(2019, 11, 17));
            books.forEach(b -> System.out.println(b.getCategory().getCategoryName()));
        }

      findByPublishDateWithQuery控制台打印的信息

    Hibernate: select book0_.id as id1_4_0_, category1_.id as id1_6_1_, book0_.book_name as book_nam2_4_0_, book0_.category_id as category4_4_0_, book0_.publish_date as publish_3_4_0_, category1_.category_name as category2_6_1_, category1_.parent_id as parent_i3_6_1_ from cfq_jpa_book book0_ inner join cfq_jpa_category category1_ on book0_.category_id=category1_.id where book0_.publish_date=?
    Java
    数据结构
    数据库

      在很多情况下,我们使用Spring-Data-Jpa,一些简单的查询,我们都喜欢用定义方法查询,而不是写JPQL。JPA为我们提供了一组注解:使用Spring-Data-Jpa为我们提供的@EntityGraph,或@EntityGraph和@NamedEntityGraph进行解决。

    3、@NamedEntityGraphs、@NamedEntityGraph、@EntityGraph

      3.1、@NamedEntityGraphs:用于对@NamedEntityGraph注解进行分组。

      3.2、@NamedEntityGraph:用于指定查找操作或查询的路径和边界。   

        属性name:(可选) 实体图的名称。 默认为根实体的实体名。
        属性attributeNodes:(可选) 包含在该图中的实体属性列表。
        属性:includeAllAttributes:(可选)将注释实体类的所有属性作为属性节点包含在NamedEntityGraph中,而无需显式列出它们。包含的属性仍然可以由引用子图的属性节点完全指定。默认为false。一般不需要设置。
        属性subgraphs:(可选)包含在实体图中的子图列表。这些是从NamedAttributeNode定义中按名称引用的。
        属性subclassSubgraphs:(可选) 子图列表 这些子图将向实体图添加注释实体类的子类的附加属性。超类中的指定属性包含在子类中。

      3.3、@EntityGraph: 注解用于配置 JPA 2.1规范支持的javax.persistence.EntityGraph,应该使用在repository的方法上面。从1.9开始,我们支持动态EntityGraph定义,允许通过attributePaths()配置自定义fetch-graph。如果指定了attributePaths(),则忽略entity-graph的name(也就是配置的value()),并将EntityGraph视为动态的。

        属性value:要使用的名称。如果为空,则返回JpaQueryMethod.getNamedQueryName()作为value。一般为@NamedEntityGraph的name值,或者不填使用自己的attributePaths属性。
        属性type:要使用的EntityGraphType,默认为EntityGraphType.FETCH。
        属性attributePaths:要使用的属性路径,默认为空。可以直接引用实体属性,也可以通过roperty.nestedProperty引用嵌套属性。
        枚举EntityGraphType:
          LOAD("javax.persistence.loadgraph"):当javax.persistence.loadgraph属性用于指定实体图时,由实体图的attributePaths指定的属性将被视为FetchType.EAGER,未指定的属性,将根据其设置的或默认的FetchType来进行处理。
          FETCH("javax.persistence.fetchgraph"):当javax.persistence.fetchgraph属性用于指定实体图时,由实体图的attributePaths指定的属性将被视为FetchType.EAGER,而未指定的属性被视为FetchType.LAZY。

      3.4、使用方法1:

       3.4.1、在实体上定义一个NamedEntityGraph

        

      3.4.2、在Repository的查询方法上引用实体图。

        

       3.4.3、测试根据出版时间进行查询,由4条SQL变为3条。

        

       3.5、使用方法2:也可以不用再实体上定义NamedEntityGraph,直接使用@EntityGraph的attributePaths属性来设置,效果是一样的。只不过如果有多个属性都要一起查出来,而且有多个方法都用到了,使用@EntityGraph的attributePaths属性修改起来就不是那么方便了,结合自己的情况进行选择。

        

     4、对于具有父子关系的处理

       场景:门类(Category),常常具有父子关系,比如说,文学类图书下面可能有小说分类,而小说分类下,又分为长、中、短篇小说。我们怎么一次查出需要的树形结果呢?

      准备工作:

      4.1、Category实体:

    /**
     * 类别
     * @author caofanqi
     */
    @Data
    @Entity
    @Builder
    @Table(name = "jpa_category")
    @NoArgsConstructor
    @AllArgsConstructor
    public class Category {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private  String categoryName;
    
        /**
         *  父门类,通过parent_id来维护父子关系。
         *  使用@ToString.Exclude,解决lombok的toString方法循环引用问题。
         */
        @ToString.Exclude
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "parent_id",referencedColumnName = "id")
        private Category parent;
    
        /**
         * 子门类列表,交由parent来维护两者之间关系。
         */
        @OneToMany(mappedBy = "parent",cascade = CascadeType.ALL)
        private List<Category> children;
    
        /**
         * 门类和书是一对多的关系
         * 由多的一方来维护关联关系
         */
        @OneToMany(mappedBy = "category")
        @OrderBy("bookName DESC")
        private List<Book> books;
    
    }

      4.2、数据准备

      

      4.3、对于数据量比较小,我们可以重写JpaRepository的findAll方法,并添加@EntityGraph注解,抓取子节点,如下所示:

        @Override
        @EntityGraph(attributePaths = "children")
        List<Category> findAll();

      测试用例:

        /**
         * 测试 一次查询树形结构
         */
        @Test
        void findAll(){
            List<Category> categories = categoryRepository.findAll();
            categories.stream().filter(c -> c.getParent() == null).forEach(c -> printName(c,null));
        }
    
    
        private void printName(Category category,String prefix){
    
            if (StringUtils.isEmpty(prefix)){
                prefix = "---";
            }
    
            System.out.println(prefix + category.getCategoryName());
    
            List<Category> children = category.getChildren();
            if (!CollectionUtils.isEmpty(children)){
                for (Category c : children){
                    printName(c,prefix + "---");
                }
            }
    
        }

      控制台输出信息:

    Hibernate: select category0_.id as id1_6_0_, children1_.id as id1_6_1_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_, children1_.category_name as category2_6_1_, children1_.parent_id as parent_i3_6_1_, children1_.parent_id as parent_i3_6_0__, children1_.id as id1_6_0__ from cfq_jpa_category category0_ left outer join cfq_jpa_category children1_ on category0_.id=children1_.parent_id
    ---计算机科学图书
    ------Java
    ------数据库
    ------数据结构
    ---文学图书
    ------小说类
    ---------长篇小说
    ---------中篇小说
    ---------短篇小说

    这种方式的优点是,不管层级多深,只有一次join。缺点是需要查询出来全部的门类,然后再代码中过滤出顶级门类,出给前端使用。而且,对于只查询某一门类,和下面的子门类不适用。

      4.4、根据父门类,一次性查询子门类及子门类的所有子节点。

         4.4.1、findByParent

        /**
         * 查询根据父节点查询门类
         * @return list
         */
        @EntityGraph(attributePaths = {"children"})
        List<Category> findByParent(Category category);

       4.4.2、这时我们测试发现,只是第一层的门类不用再执行SQL了,而下面的门类一样要执行。

        @Test
        void findByParent(){
            List<Category> categories = categoryRepository.findByParent(null);
            categories.forEach(c -> printName(c,null));
        }
    Hibernate: select category0_.id as id1_6_0_, children1_.id as id1_6_1_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_, children1_.category_name as category2_6_1_, children1_.parent_id as parent_i3_6_1_, children1_.parent_id as parent_i3_6_0__, children1_.id as id1_6_0__ from cfq_jpa_category category0_ left outer join cfq_jpa_category children1_ on category0_.id=children1_.parent_id where category0_.parent_id is null
    ---计算机科学图书
    ------Java
    Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=?
    ------数据库
    Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=?
    ------数据结构
    Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=?
    ---文学图书
    ------小说类
    Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=?
    ---------长篇小说
    Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=?
    ---------中篇小说
    Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=?
    ---------短篇小说
    Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=?
    

      4.4.3、解决多次查询问题,上面说到@EntityGraph的attributePaths是支持属性嵌套的,我们写一个children就会关联一次,如果我们知道层级的话,可以用.进行连接children,如下图,就会与自己关联三次有几层,就要至少有几个children,也就会进行几次关联。(层级越多,关联的次数越多)

      

       也可以使用@NamedEntityGraph(感觉不如attributePaths简介),写法如下:

    @NamedEntityGraph(name = "Category.findByParent",
            attributeNodes = {@NamedAttributeNode(value = "children", subgraph = "son")}, //第一层
            subgraphs = {@NamedSubgraph(name = "son", attributeNodes = @NamedAttributeNode(value = "children", subgraph = "grandson")), //第二层
                    @NamedSubgraph(name = "grandson", attributeNodes = @NamedAttributeNode(value = "children"))//第三层
            })

      但是现在光做这些还不够,执行测试用例,会抛出 org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:异常;想知道为啥的可以点击这里

    我推荐两个解决办法:

      ①将List集合修改为Set,并使用@EqualsAndHashCode.Exclude解决lombok的hashcode方法引入的异常。

        

      ②使用@OrderColumn,这样jpa会在数据库中多出一列,用于自己维护关系。(一开始就要这样哦,半路改的,会有问题)

        

      以上任意一种修改后,执行测试用例,控制台输出结果如下:

    Hibernate: select category0_.id as id1_6_0_, children1_.id as id1_6_1_, children2_.id as id1_6_2_, children3_.id as id1_6_3_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_, children1_.category_name as category2_6_1_, children1_.parent_id as parent_i3_6_1_, children1_.parent_id as parent_i3_6_0__, children1_.id as id1_6_0__, children1_.children_order as children4_0__, children2_.category_name as category2_6_2_, children2_.parent_id as parent_i3_6_2_, children2_.parent_id as parent_i3_6_1__, children2_.id as id1_6_1__, children2_.children_order as children4_1__, children3_.category_name as category2_6_3_, children3_.parent_id as parent_i3_6_3_, children3_.parent_id as parent_i3_6_2__, children3_.id as id1_6_2__, children3_.children_order as children4_2__ from cfq_jpa_category category0_ left outer join cfq_jpa_category children1_ on category0_.id=children1_.parent_id left outer join cfq_jpa_category children2_ on children1_.id=children2_.parent_id left outer join cfq_jpa_category children3_ on children2_.id=children3_.parent_id where category0_.parent_id is null
    ---文学图书
    ------小说类
    ---------长篇小说
    ---------中篇小说
    ---------短篇小说
    ---计算机科学图书
    ------Java
    ------数据库
    ------数据结构 

    源码地址:https://github.com/caofanqi/study-spring-data-jpa
  • 相关阅读:
    202103226-1 编程作业
    阅读任务
    1 20210309-1 准备工作
    20210405-1 案例分析作业
    第一周作业
    20210309-2 阅读任务
    20210309-1 准备工作
    编程作业
    阅读任务
    准备工作
  • 原文地址:https://www.cnblogs.com/caofanqi/p/11902159.html
Copyright © 2020-2023  润新知