学习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
posted @ 2019-11-21 00:44  caofanqi  阅读(3523)  评论(2编辑  收藏  举报